我花了相当多的阅读和编码时间才最终理解 Java Lambdas 如何在概念上正常工作的. 我阅读的大多数教程和介绍都遵循自顶向下的方法, 从用例开始, 最后以概念性问题结束. 在这篇文章中, 我想提供一个自下而上的解释, 从其他已建立的 Java 概念中推导出 Lambdas 的概念.
首先介绍下方法的类型化, 这是支持方法作为一流公民的先决条件. 基于此, Lambdas 的概念是被以匿名类用法的进化和特例提出的. 所有这一切都通过实现和使用高阶函数映射 https://en.wikipedia.org/wiki/Map_(higher-order_function) 来说明.
这篇文章的主要受众是那些已掌握函数式编程基础的人, 以及那些想从概念上理解 Lambdas 如何嵌入 Java 语言的人.
方法类型
从 Java 8 起方法就是一等公民 https://en.wikipedia.org/wiki/First-class_citizen 了. 按照标准的定义, 编程语言中的一等公民是一个具有下列功能的实体,
可以作为参数进行传递,
可以作为方法的返回值
可以赋值给一个变量.
在 Java 中, 每一个参数, 返回值或变量都是有类型的, 因此每个一等公民都必须是有类型的. Java 中的一种类型可以是以下内容之一:
一种内建类型 (比如 int 或者 double)
一个类 (比如 ArrayList)
一个接口 (比如 Iterable)
方法是通过接口进行定义类型的. 它们不隐式的实现特定接口, 但是在必要的时候, 如果一个方法符合一个接口, 那么在编译期间, Java 编译器会对其进行隐式的检查. 举个例子说明:
- class LambdaMap {
- static void oneStringArgumentMethod(String arg) {
- System.out.println(arg);
- }
- }
关于 oneStringArgumentMethod 函数的类型, 与之相关的有: 它的的函数是静态的, 返回类型是 void, 它接受一个 String 类型的参数. 一个静态函数符合包含一个 apply 函数的接口, apply 函数的签名相应地符合这个静态函数的签名. oneStringArgumentMethod 函数对应的接口因此必须符合下列标准.
它必须包含一个名为 apply 的函数.
函数返回类型必须是 void.
函数必须接受一个 String 类型可以转换到的对象的参数.
在符合这个标准的接口之中, 下面的这个是最明确的:
- interface OneStringArgumentInterface {
- void apply(String arg);
- }
利用这个接口, 函数可以分配给一个变量:
OneStringArgumentInterface meth = LambdaMap::oneStringArgumentMethod;
用这种方法使用接口作为类型, 函数可以借此被分配给变量, 传递参数并且从函数返回:
- static OneStringArgumentInterface getWriter() {
- return LambdaMap::oneStringArgumentMethod;
- }
- static void write(OneStringArgumentInterface writer, String msg) {
- writer.apply(msg);
- }
最终函数是一等公民.
泛型函数类型
就像使用集合一样, 泛型为函数类型增加了大量的功能和灵活性. 实现功能上的算法而不考虑类型相关信息, 泛型函数类型使其变为可能. 在对 map 函数的实现中, 会在下面用到这种功能.
在这提供的 OneStringArgumentInterface 一个泛型版本:
- interface OneArgumentInterface<T> {
- void apply(T arg);
- }
OneStringArgumentInterface 函数可以被分配给它:
OneArgumentInterface<String> meth = LambdaMap::oneStringArgumentMethod;
通过使用泛型函数类型, 它现在可以以一种通用的方法实现算法, 就像它在集合中使用的一样:
- static <T> void applyArgument(OneArgumentInterface<T> meth, T arg) {
- meth.apply(arg);
- }
上面的函数并没有什么用, 然而它至少可以提出一个想法: 对函数作为第一个类成员的支持怎样可以形成非常简洁且灵活的代码:
applyArgument(Lambda::oneStringArgumentMethod, "X");
实现 map
在诸多高阶函数中, map 是最经典的. map 的第一个参数是函数, 该函数可以接收一个参数并返回一个值; 第二个参数是值列表. map 使用传入的函数处理值列表的每一项, 然后返回一个新的值列表. 下面 Python 的代码片段, 可以很好的说明 map 的用法:
- >>> map(math.sqrt, [1, 4, 9, 16])
- [1.0, 2.0, 3.0, 4.0]
在本节的后续内容中, 将给出该函数的 Java 实现. Java 8 已经通过 Stream 提供了该函数. 因为主要出于教学目的, 所以, 本节中给出的实现特意保持简单, 仅限于 List 对象使用.
与 Python 不同, 在 Java 中必须首先考虑 map 第一个参数的类型: 一个可以接收一个参数并返回一个值的方法. 参数的类型和返回值的类型可以不同. 下面接口符合这个预期, 显然, I 表示参数 (入参),O 表示返回值 (出参):
- interface MapFunction<I, O> {
- O apply(I in);
- }
泛型 map 方法的实现, 变得惊人的简单明了:
- static <I, O> List<O> map(MapFunction<I, O> func, List<I> input) {
- List<O> out = new ArrayList<>();
- for (I in : input) {
- out.add(func.apply(in));
- }
- return out;
- }
创建新的返回值列表 out(用于保存 O 类型的对象).
通过遍历 input,func 处理列表的每一项, 并将返回值添加到 out 中.
返回 out.
下面是实际使用 map 方法的实例:
- MapFunction<Integer, Double> func = Math::sqrt;
- List<Double> output = map(func, Arrays.asList(1., 4., 9., 16.));
- System.out.println(output);
在 Python one-liner 的推动下, 可以用更简洁的方法表达:
System.out.println(map(Math::sqrt, Arrays.asList(1., 4., 9., 16.)));
Java 毕竟不是 Python...
Lambdas 来了!
读者可能会注意到, 还没有提到 Lambdas. 这是由于采用了 "自下而上" 的方式描述, 现在基础已基本建立, Lambdas 将在后续的章节中介绍.
下面的用例作为基础: 一个 double 类型的 list, 表示半径, 然后得到一个列表, 表示圆面积. map 方法就是为此任务预先准备的. 计算圆面积的公式是众所周知的:
A=r2π
应用这个公式的方法很容易实现:
- static Double circleArea(Double radius) {
- return Math.pow(radius, 2) * Math.PI;
- }
这个方法现在可以用作 map 方法的第一个参数:
- System.out.println(
- map(LambdaMap::circleArea,
- Arrays.asList(1., 4., 9., 16.)));
如果 circleArea 方法只需要这一次, 没有道理把类接口被他弄得乱七八糟, 也没有道理将实现和真正使用它的地方分离. 最佳实践是使用用匿名内部类. 可以看到, 实例化一个实现 MapFunction 接口的匿名内部类可以很好的完成这个任务:
- System.out.println(
- map(new MapFunction<Double, Double>() {
- public Double apply(Double radius) {
- return Math.sqrt(radius) * Math.PI;
- }
- },
- Arrays.asList(1., 2., 3., 4.)));
这看起来很漂亮, 但是很多人会认为函数式的解决方案更清晰, 更具可读性:
- List<Double> out = new ArrayList<>();
- for (Double radius : Arrays.asList(1., 2., 3., 4.)) {
- out.add(Math.sqrt(radius) * Math.PI);
- }
- System.out.println(out);
到目前为止, 最后是使用 Lambda 表达式. 读者应该注意 Lambda 如何取代上面提到的匿名类:
- System.out.println(
- map(radius -> { return Math.sqrt(radius) * Math.PI; },
- Arrays.asList(1., 2., 3., 4.)));
这看起来简洁明了 - 请注意 Lambda 表达式如何缺省任何明确的类型信息. 没有显式模板实例化, 没有方法签名.
Lambda 表达式由两部分组成, 这两部分被 -> 分隔. 第一部分是参数列表, 第二部分是实际实现.
Lambda 表达式和匿名内部类作用完全相同, 然而它摒弃了许多编译器可以自动推断的样板代码. 让我们再次比较这两种方式, 然后分析编译器为开发人员节省了哪些工作.
- MapFunction<Double, Double> functionLambda =
- radius -> Math.sqrt(radius) * Math.PI;
- MapFunction<Double, Double> functionClass =
- new MapFunction<Double, Double>() {
- public Double apply(Double radius) {
- return Math.sqrt(radius) * Math.PI;
- }
- };
对于 Lambda 实现来说, 只有一个表达式, 返回语句和花括号可以省略. 这使得代码更简短.
Lambda 表达式的返回值类型是从 Lambda 实现推断出来的.
对于参数类型, 我不完全确定, 但我认为必须从 Lambda 表达式所处的上下文中推断出参数类型.
最后编译器必须检查返回值类型是否与 Lambda 的上下文匹配, 以及参数类型是否与 Lambda 实现匹配.
这一切都可以在编译期间完成, 根本没有运行时开销.
结语
总而言之, Java 中的 Lambdas 的概念是整洁的. 我支持编写更简洁, 更清晰的代码, 并让程序员免于编写可由编译器自动推断的架手架代码. 它是语法糖, 如上所述, 它只不过是使用匿名类也能实现的功能. 然而, 我会说它是非常甜的语法糖.
另一方面, Lambdas 还支持更加混淆以及难以调试的代码. Python 社区很早就意识到了这一点 - 虽然 Python 也有 Lambda, 但它若被广泛使用则通常被认为是不好的风格 (当嵌套函数可以被使用时, 它并不难于规避). 对于 Java 来说, 我会给出类似的建议. 毫无疑问, 在某些情况下, 使用 Lambdas 会导致代码大大缩减并更易读, 尤其在与流有关时. 在其他情况下, 如果采取更保守的做法和最佳实践, 另外一种方法可能会是更好的替代.
链接
Java Lambdas 的官方文档 https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html .
java.util.function https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html 包中所包含许多不同的 Lambda 接口, 并且就像使用上述 MapFunction 完成的功能一样, 可以让程序员免于引入自己的接口.
来源: https://juejin.im/entry/5b2a0ac76fb9a00e487e8f91