effective-java-3rd-chinese/docs/notes/42. lambda表达式优于匿名类.md
2019-05-27 16:50:23 +08:00

108 lines
8.3 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 42. lambda 表达式优于匿名类
  在 Java 8 中,添加了函数式接口,`lambda` 表达式和方法引用,以便更容易地创建函数对象。 Stream API 随着其他语言的修改一同被添加进来,为处理数据元素序列提供类库支持。 在本章中,我们将讨论如何充分利用这些功能。
  以往,使用单一抽象方法的接口(或者很少使用抽象类)被用作函数类型。 它们的实例称为函数对象表示函数functions或行动actions。 自从 JDK 1.1 于 1997 年发布以来,创建函数对象的主要手段就是匿名类(详见第 24 条)。 下面是一段代码片段,按照字符串长度顺序对列表进行排序,使用匿名类创建排序的比较方法(强制排序顺序):
```java
// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
```
  匿名类适用于需要函数对象的经典面向对象设计模式,特别是策略模式[Gamma95]。 比较器接口表示排序的抽象策略; 上面的匿名类是排序字符串的具体策略。 然而,匿名类的冗长,使得 Java 中的函数式编程成为一种吸引人的前景。
  在 Java 8 中,语言形式化了这样的概念,即使用单个抽象方法的接口是特别的,应该得到特别的对待。 这些接口现在称为函数式接口,并且该语言允许你使用 **lambda** 表达式或简称 **lambda** 来创建这些接口的实例。 **Lambdas** 在功能上与匿名类相似,但更为简洁。 下面的代码使用 **lambdas** 替换上面的匿名类。 样板不见了,行为清晰明了:
```java
// Lambda expression as function object (replaces anonymous class)
Collections.sort(words,
(s1, s2) -> Integer.compare(s1.length(), s2.length()));
```
  请注意,代码中不存在 **lambda**`Comparator <String>`其参数s1 和 s2都是 `String` 类型及其返回值int的类型。 编译器使用称为类型推断的过程从上下文中推导出这些类型。 在某些情况下,编译器将无法确定类型,必须指定它们。 类型推断的规则很复杂:他们在 JLS 中占据了整个章节[JLS18]。 很少有程序员详细了解这些规则,但没关系。 除非它们的存在使你的程序更清晰,否则省略所有 **lambda** 参数的类型。 如果编译器生成一个错误,告诉你它不能推断出 **lambda** 参数的类型,那么指定它。 有时你可能不得不强制转换返回值或整个 **lambda** 表达式,但这很少见。  
  关于类型推断需要注意一点。 条目 26 告诉你不要使用原始类型,条目 29 告诉你偏好泛型类型,条目 30 告诉你偏向泛型方法。 当使用 **lambda** 表达式时,这个建议是非常重要的,因为编译器获得了大部分允许它从泛型进行类型推断的类型信息。 如果你没有提供这些信息,编译器将无法进行类型推断,你必须在 **lambdas** 中手动指定类型,这将大大增加它们的冗余度。 举例来说,如果变量被声明为原始类型 `List` 而不是参数化类型 `List<String>`,则上面的代码片段将不会编译。  
  顺便提一句,如果使用比较器构造方法代替 **lambda**,则代码中的比较器可以变得更加简洁(详见第 14 和 43 条):
```java
Collections.sort(words, comparingInt(String::length));
```
  实际上,通过利用添加到 Java 8 中的 `List` 接口的 `sort` 方法,可以使片段变得更简短:
```java
words.sort(comparingInt(String::length));
```
  将 **lambdas** 添加到该语言中,使得使用函数对象在以前没有意义的地方非常实用。例如,考虑条目 34 中的 `Operation` 枚举类型。由于每个枚举都需要不同的应用程序行为,所以我们使用了特定于常量的类主体,并在每个枚举常量中重写了 `apply` 方法。为了刷新你的记忆,下面是之前的代码:
```java
// Enum type with constant-specific class bodies & data
public enum Operation {
PLUS("+") {
public double apply(double x, double y) { return x + y; }
},
MINUS("-") {
public double apply(double x, double y) { return x - y; }
},
TIMES("*") {
public double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
public double apply(double x, double y) { return x / y; }
};
private final String symbol;
Operation(String symbol) { this.symbol = symbol; }
@Override
public String toString() { return symbol; }
public abstract double apply(double x, double y);
}
```
  第 34 条目说,枚举实例属性比常量特定的类主体更可取。 **Lambdas** 可以很容易地使用前者而不是后者来实现常量特定的行为。 仅仅将实现每个枚举常量行为的 **lambda** 传递给它的构造方法。 构造方法将 **lambda** 存储在实例属性中,`apply` 方法将调用转发给 **lambda**。 由此产生的代码比原始版本更简单,更清晰:
```java
public enum Operation {
PLUS ("+", (x, y) -> x + y),
MINUS ("-", (x, y) -> x - y),
TIMES ("*", (x, y) -> x * y),
DIVIDE("/", (x, y) -> x / y);
private final String symbol;
private final DoubleBinaryOperator op;
Operation(String symbol, DoubleBinaryOperator op) {
this.symbol = symbol;
this.op = op;
}
@Override
public String toString() { return symbol; }
public double apply(double x, double y) {
return op.applyAsDouble(x, y);
}
}
```
  请注意,我们使用表示枚举常量行为的 **lambdas**`DoubleBinaryOperator` 接口。 这是 `java.util.function` 中许多预定义的函数接口之一(详见第 44 条)。 它表示一个函数,它接受两个 **double** 类型参数并返回 **double** 类型的结果。
  看看基于 **lambda**`Operation` 枚举,你可能会认为常量特定的方法体已经失去了它们的用处,但事实并非如此。 与方法和类不同,**lambda 没有名称和文档; 如果计算不是自解释的,或者超过几行,则不要将其放入 lambda 表达式中。** 一行代码对于 **lambda** 说是理想的,三行代码是合理的最大值。 如果违反这一规定,可能会严重损害程序的可读性。 如果一个 **lambda** 很长或很难阅读,要么找到一种方法来简化它或重构你的程序来消除它。 此外,传递给枚举构造方法的参数在静态上下文中进行评估。 因此,枚举构造方法中的 **lambda** 表达式不能访问枚举的实例成员。 如果枚举类型具有难以理解的常量特定行为,无法在几行内实现,或者需要访问实例属性或方法,那么常量特定的类主体仍然是行之有效的方法。
  同样,你可能会认为匿名类在 **lambda** 时代已经过时了。 这更接近事实,但有些事情你可以用匿名类来做,而却不能用 **lambdas** 做。 **Lambda** 仅限于函数式接口。 如果你想创建一个抽象类的实例,你可以使用匿名类来实现,但不能使用 **lambda**。 同样,你可以使用匿名类来创建具有多个抽象方法的接口实例。 最后,**lambda** 不能获得对自身的引用。 在 **lambda** 中,`this` 关键字引用封闭实例,这通常是你想要的。 在匿名类中,`this` 关键字引用匿名类实例。 如果你需要从其内部访问函数对象,则必须使用匿名类。
  **Lambdas** 与匿名类共享无法可靠地序列化和反序列化实现的属性。**因此,应该很少 (如果有的话) 序列化一个 lambda(或一个匿名类实例)。** 如果有一个想要进行序列化的函数对象,比如一个 `Comparator`,那么使用一个私有静态嵌套类的实例(详见第 24 条)。
  综上所述,从 Java 8 开始,**lambda** 是迄今为止表示小函数对象的最佳方式。 **除非必须创建非函数式接口类型的实例,否则不要使用匿名类作为函数对象。** 另外,请记住,**lambda** 表达式使代表小函数对象变得如此简单,以至于它为功能性编程技术打开了一扇门,这些技术在 Java 中以前并不实用。