Java8 提供了以下两种方式,来支持函数式编程。
- Lambda 表达式
- 方法引用 (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 表达式的基本语法
- 参数。
- 接着 ->,可视为“产出”。
- -> 之后的内容都是方法体。
- 当只用一个参数,可以不需要括号 ()。 这是特例。
- 正常情况括号 () 包裹参数。 为了保持一致性,也可以使用括号 () 包裹单个参数,虽使用然这种情况并不常见。
- 如果没有参数,则必须使用括号 () 表示空参数列表。
- 对于多个参数,将参数列表放在括号 () 中。
- Lambda 表达式方法体都是单行的,单行表达式的结果会自动成为 Lambda 表达式的返回值。
- 如果在 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 注解来自定义函数式接口。
网友评论