[译]通往 Java 函数式编程的捷径

以声明式的思想在你的 Java 程序中使用函数式编程技术

Java™ 开发人员习惯于面向命令式和面向对象的编程,由于这些特性自 Java 语言首次发布以来一直受到支持。在 Java 8 中,咱们得到了一组新的强大的函数式特性和语法。函数式编程已经存在了数十年,与面向对象编程相比,函数式编程一般更加简洁和达意,不易出错,而且更易于并行化。因此有很好的理由将函数式编程特性引入到 Java 程序中。尽管如此,在使用函数式特性进行编程时,就如何设计你的代码这一点上须要进行一些改变。前端

关于本文java

Java 8 是 Java 语言自诞生以来最重要的更新,它包含如此多的新特性,以致于你可能想知道应该从哪开始了解它。在本系列中,身为做家和教育家的 Venkat Subramaniam 提供了一种符合 Java 语言习惯的 Java 8 学习方式。邀请你进行简短的探索后,从新思考你认为理所固然的 Java 一向用法和规范,同时逐渐将新技术和语法集成到你的程序中去。android

我认为,以声明式的思想而不是命令式的思想来编程,能够更加轻松地向更加函数化的编程风格过渡。在 Java 8 idioms series 这个系列的第一篇文章中,我解释了命令式、声明式和函数式编程风格之间的异同。而后,我将向你展现如何使用声明式的思想逐渐将函数式编程技术集成到你的 Java 程序中。ios

命令式风格(面向过程)

受命令式编程风格训练的开发者习惯于告诉程序须要作什么以及如何去作。这里是一个简单的例子:git

清单 1. 以命令式风格编写的 findNemo 方法
import java.util.*;

public class FindNemo {
  public static void main(String[] args) {
    List<String> names = 
      Arrays.asList("Dory", "Gill", "Bruce", "Nemo", "Darla", "Marlin", "Jacques");

    findNemo(names);
  }                 
  
  public static void findNemo(List<String> names) {
    boolean found = false;
    for(String name : names) {
      if(name.equals("Nemo")) {
        found = true;
        break;
      }
    }
    
    if(found)
      System.out.println("Found Nemo");
    else
      System.out.println("Sorry, Nemo not found");
  }
}
复制代码

方法 findNemo() 首先初始化一个可变变量 flag,也称为垃圾变量(garbage variable)。开发者常常会给予某些变量一个临时性的名字,例如 fttemp 以代表它们根本不该该存在。在本例中,这些变量应该被命名为 foundgithub

接下来,程序会循环遍历给定的 names 列表,每次都会判断当前遍历的值是否和待匹配值相同。在这个例子中,待匹配值为 Nemo,若是遍历到的值匹配,程序会将标志位设为 true,并执行流程控制语句 "break" 跳出循环。编程

这是对于广大 Java 开发者最熟悉的编程风格 —— 命令式风格的程序,所以你能够定义程序的每一步:你告诉程序遍历每个元素,和待匹配值进行比较,设置标志位,以及跳出循环。命令式编程风格让你能够彻底控制程序,有的时候这是一件好事。可是,换个角度来看,你作了不少机器能够独立完成的工做,这势必致使生产力降低。所以,有的时候,你能够经过少作事来提升生产力。后端

声明式风格

声明式编程意味着你仍然须要告诉程序须要作什么,可是你能够将实现细节留给底层函数库。让咱们看看使用声明式编程风格重写清单 1 中的 findNemo 方法时会发生什么:bash

清单 2. 以声明式风格编写的 findNemo 方法
public static void findNemo(List<String> names) {
  if(names.contains("Nemo"))
    System.out.println("Found Nemo");
  else
    System.out.println("Sorry, Nemo not found");
}
复制代码

首先须要注意的是,此版本中没有任何垃圾变量。你也不须要在遍历集合中浪费精力。相反,你只须要使用内建的 contains() 方法来完成这项工做。你仍然要告诉程序须要作什么,集合中是否包含咱们正在寻找的值,但此时你已经将细节交给底层的方法来实现了。函数式编程

在命令式编程风格的例子中,你控制了遍历的流程,程序能够彻底按照指令进行;在声明式的例子中,只要程序可以完成工做,你彻底不须要关注它是如何工做的。contains() 方法的实现可能会有所不一样,但只要结果符合你的指望,你就会对此感到满意。更少的工做可以获得相同的结果。

训练本身以声明式的编程风格来进行思考将更加轻松地向更加函数化的编程风格过渡。缘由在于,函数式编程风格是创建在声明式风格之上的。声明式风格的思惟可让你逐渐从命令式编程转换到函数式编程。

函数式编程风格

虽然函数式风格的编程老是声明式的,可是简单地使用声明式风格编程并不等同与函数式编程。这是由于函数式编程时将声明式编程和高阶函数结合在了一块儿。图 1 显示了命令式,声明式和函数式编程风格之间的关系。

图 1. 命令式、声明式和函数式编程风格之间的关系

A logic diagram showing how the imperative, declarative, and functional programming styles differ and overlap.

Java 中的高阶函数

在 Java 中,你能够将对象传递给方法,在方法中建立对象,也能够从方法中返回对象。同时你也能够用函数作相同的事情。也就是说,你能够将函数传递给方法,在方法中建立函数,也能够从方法中返回函数。

在这种状况下,方法是类的一部分(静态或实例),可是函数能够是方法的一部分,而且不能有意地与类或实例相关联。一个能够接收、建立、或者返回函数的方法或函数称之为高阶函数

一个函数式编程的例子

采用新的编程风格须要改变你对程序的见解。这是一个从简单例子的练习开始,到构建更加复杂程序的过程。

清单 3. 命令式编程风格下的 Map
import java.util.*;

public class UseMap {
  public static void main(String[] args) {
    Map<String, Integer> pageVisits = new HashMap<>();            
    
    String page = "https://agiledeveloper.com";
    
    incrementPageVisit(pageVisits, page);
    incrementPageVisit(pageVisits, page);
    
    System.out.println(pageVisits.get(page));
  }
  
  public static void incrementPageVisit(Map<String, Integer> pageVisits, String page) {
    if(!pageVisits.containsKey(page)) {
       pageVisits.put(page, 0);
    }
    
    pageVisits.put(page, pageVisits.get(page) + 1);
  }
}
复制代码

清单 3 中,main() 函数建立了一个 HashMap 来保存网站访问次数。同时,incrementPageVisit() 方法增长了每次访问给定页面的计数。咱们将聚焦此方法。

以命令式编程风格写的 incrementPageVisit() 方法:它的工做是为给定页面增长一个计数,并存储在 Map 中。该方法不知道给定页面是否已经有计数值,因此会先检查计数值是否存在,若是不存在,会为该页面插入一个值为"0"的计数值。而后再获取该计数值,递增它,并将新的计数值存储在 Map 中。

以声明式的方式思考须要你将方法的设计从 "how" 转变到 "what"。当 incrementPageVisit() 方法被调用时,你须要将给定的页面计数值初始化为 1 或者计数值加 1。这就是 what

由于你是经过声明式编程的,那么下一步就是在 JDK 库中寻找能够完成这项工做且实现了 Map 接口的方法。换言之,你须要找到一个知道如何完成你指定任务的内建方法。

事实证实 merge() 方法很是适合你的而目的。清单 4 使用新的声明式方法对清单 3 中的 incrementPageVisit() 方法进行修改。可是,在这种状况下,你不只仅只是选择更智能的方法来写出更具声明性风格的代码,由于 merge() 是一个更高阶的函数。因此说,新的代码其实是一个体现函数式风格的很好的例子:

清单 4. 函数式编程风格下的 Map
public static void incrementPageVisit(Map<String, Integer> pageVisits, String page) {
    pageVisits.merge(page, 1, (oldValue, value) -> oldValue + value); 
}
复制代码

在清单 4 中,page 做为第一个参数传递给 merge():map 中键对应的值将会被更新。第二个参数做为初始值,若是 Map 中不存在指定键的值,那么该值将会赋值给 Map 中键对应的值(在本例中为"1")。第三个参数为一个 lambda 表达式,接受当前 Map 中键对应的值和该函数中第二个参数对应的值做为参数。lambda 表达式返回其参数的总和,实际上增长了计数值。(编者注:感谢 István Kovács 修正了代码错误)

清单 4incrementPageVisit() 方法中的单行代码与清单 3 中的多行代码进行比较。虽然清单 4 中的程序是函数式编程风格的一个例子,但经过声明性地思想去思考问题帮助可以咱们实现飞跃。

总结

在 Java 程序中采用函数式编程技术和语法有不少好处:代码更简洁,更富有表现力,移动部分更少,实现并行化更容易,而且一般比面向对象的代码更易理解。 目前面临的挑战是,如何将你的思惟从绝大多数开发人员所熟悉的命令式编程风格转变为以声明式的方式进行思考。

虽然函数式编程并无那么简单或直接,可是你能够学习专一于你但愿程序作什么而不是如何作这件事,来取得巨大的飞跃。经过容许底层函数库管理执行,你将逐渐直观地了解用于构建函数式编程模块的高阶函数。

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索