说说 Java 中的函数式编程

221 阅读6分钟

Java8 提供了以下两种方式,来支持函数式编程。

  1. Lambda 表达式
  2. 方法引用 (MethodReferences)

Bruce Eckel 举了一个策略模式的示例,来比较传统写法与函数式编程写法之间的区别。

首先定义一个策略接口,然后我们就可以实现不同的策略,以供其它业务类使用。在此可以看到,传统的匿名内部类代码量最多, lambda 表达式代码居中,而方法引用最精简。

1 Lambda 表达式

Lambda 表达式( lambda expression )是一个匿名函数, Lambda 表达式基于数学中的 λ 演算得名,直接对应于其中的 lambda 抽象( lambda abstraction),是一个匿名函数,即没有函数名的函数。

λ 演算, λ (Lambda(大写 Λ ,小写 λ )读音: ['læ;mdə]) 演算是一套用于研究函数定义 、 函数应用和递归的形式系统。它由 Alonzo Church 和 Stephen Cole Kleene 在 20 世纪三十年代引入, Church 运用 lambda 演算在 1936 年给出判定性问题的一个否定的答案。

找不到 Alonzo Church 和 Stephen Cole Kleene 的照片,这张大家看看就好 O(∩_∩)O

1.1 各种 Lambda 表达式

首先先定义好结果接口:

interface Description {
  String brief();
}

interface Body {
  String detailed(String head);
}

interface Multi {
  String twoArg(String head, Double d);
}

然后定义 Lambda 表达式,最后调用接口所定义的方法:

示例演示了各种情况,比如带括号 、 空括号 、 带多个参数 、 方法体多行并且写在括号中等等。除了最后一行,其它的 Lambda 表达式方法体都是单行的,单行表达式的结果会自动成为 Lambda 表达式的返回值。如果在 Lambda 表达式中需要多行,那么必须将这些行放在花括号中。在这种情况下,需要使用 return。


可以看到 Lambda 表达式最大的特点就是简洁、易读。

1.2 Lambda 表达式的基本语法

  1. 参数。
  2. 接着 ->,可视为“产出”。
  3. -> 之后的内容都是方法体。
  • 当只用一个参数,可以不需要括号 ()。 这是特例。
  • 正常情况括号 () 包裹参数。 为了保持一致性,也可以使用括号 () 包裹单个参数,虽使用然这种情况并不常见。
  • 如果没有参数,则必须使用括号 () 表示空参数列表。
  • 对于多个参数,将参数列表放在括号 () 中。
  1. Lambda 表达式方法体都是单行的,单行表达式的结果会自动成为 Lambda 表达式的返回值。
  2. 如果在 Lambda 表达式中需要多行,那么必须将这些行放在花括号中。 在这种情况下,需要使用 return。

2 Java 8 方法引用

Java 8 方法引用语法是:类名或对象名,后面跟 ::,然后跟方法名称。

这里首先定义了一个 Callable 接口,内含一个带 String 入参的 call() 方法;接着定义了一个类,内含一个带 String 入参的 show() 方法;最后定义了了一个类,内含一个带 String 入参的静态 hello() 方法;在使用方法引用时, Java 会认为这三个方法的签名相同(也可以说是与接口方法同名的方法引用),所以都可以使用方法引用语法,赋给 Callable 对象。当调用c.call() 方法时,Java 会根据实际的实例对象,调用实际的方法。比如 c.call(“Bob”) 方法,实际调用的是 MethodReferences 的 hello() 方法。

3 函数式接口

方法引用和 Lambda 表达式必须被赋值,赋值的对象类型会告诉编译器,编译器会保障类型正确。

x 和 y 可以是任何支持 + 运算符连接的数据类型,可以是两个不同的数值类型或者是1个 String 加任意一种可自动转换为 String 的数据类型(这包括了大多数类型)。但是,当 Lambda 表达式被赋值时,编译器必须确定 x 和 y 的确切类型以生成正确的代码。

3.1 标准函数式接口

Java8 引入了 java.util.function 包。它包含一组接口,这些接口是 Lambda 表达式和方法引用的目标类型。每个接口只包含一个抽象方法,称为函数式方法。

之前所描述的6个基本的接口,每一个都有3个变种,分别用于操作原生类型 int , long 和 double。

其名字衍生自这些基本的接口,只不过每一个都在前面加上了一个原生类型。

Function 接口还定义了 9 个变种,用在结果类型为原生类型的场景。如果源与结果类型都是原生类型,那就在 Function 前加上 SrcToResult。 如果源是对象引用,结果是原生类型,那就在 Function 前加上 ToResult。

Function 接口还定义了两参数版本,它们以 Bi 作为接口名前缀:

如果一个 Consumer 函数接口有两个参数,一个是对象引用,另一个是原生类型。那么会以 ObjXXXConsumer 作为函数接口名称。其中的 XXX 表示原生类型。

最后还有一个 Boolean Supplier 接口,它是 Supplier 的变种,返回 boolean 值。这是在所有标准的函数式接口名当中,唯一一个显式使用 boolean 类型的;我们也可以通过 Predicate 及其4个变种来得到 boolean 返回值。


Java8 总共定义了 43 个标准的函数式接口。确实有些多,不过并不太可怕,大多数标准的函数式接口存在的唯一目的在于为原生类型提供支持。

3.2 赋值

首先自定义类:

class Foo {}

class Bar {
  Foo f;
  Bar(Foo f) { this.f = f; }
}

class IBaz {
  int i;
  IBaz(int i) {
    this.i = i;
  }
}

class LBaz {
  long l;
  LBaz(long l) {
    this.l = l;
  }
}

class DBaz {
  double d;
  DBaz(double d) {
    this.d = d;
  }
}

然后通过 Lambda 表达式赋值给函数式接口:

最后调用 Function 接口中不同类型的 apply() 方法,就可以调用与其关联的 Lambda 表达式:

3.3 函数组合( FunctionComposition )

函数组合指的是把多个函数组合起来,生成新函数。2个是执行先后顺序组合,另外3个是逻辑组合。

3.4 自定义函数式接口

如果标准函数式接口没有我们所需要的接口怎么办?没关系,JDK8 提供了可自定义接口的方式。比如,我们可以这样定义三个入参的函数接口。示例演示了采用方法引用与 lambdas 方式。

关键点是使用 @FunctionalInterface 注解来自定义函数式接口。