ITEM 42: PREFER LAMBDAS TO ANONYMOUS CLASSES
历史上,带有单个抽象方法的接口(或抽象类)被用作函数类型。它们的实例(称为函数对象)表示函数或操作。自从 JDK 1.1 在1997年发布以来,创建函数对象的主要方法就是匿名类(item 24)。下面是一个代码片段,排序的字符串列表的长度,使用一个匿名类创建排序的比较函数:
// 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 表达式(或简称 lambdas)创建这些接口的实例。Lambdas 在功能上与匿名类相似,但更简洁。上面的代码片段用lambda替换匿名类之后是这样的,样板代码已经没有了,行为很明显:
// Lambda expression as function object (replaces anonymous class)
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
注意,lambda方法 (Comparator<String>)、它的参数(s1和s2) 以及它的返回值(int)的类型在代码中不存在。编译器使用称为类型推断的过程,从上下文推断出这些类型。在某些情况下,编译器无法确定类型,您必须指定它们。类型推断的规则很复杂:它们在JLS [JLS, 18]中占了整整一章。很少有程序员能详细地理解这些规则,但这没有关系。忽略所有lambda参数的类型,除非它们的存在使您的程序更清晰。如果编译器生成一个错误,告诉您它不能推断lambda参数的类型,那么指定它。有时您可能不得不强制转换返回值或整个 lambda 表达式,但这种情况很少见。
关于类型推断,item 26建议不要使用原始类型,item 29 建议使用泛型类型,item 30 建议使用泛型方法。当您使用 lambdas 时,这些建议是加倍重要的,因为编译器可以获得大多数类型信息,从而允许它从泛型中执行类型推断。如果不提供此信息,编译器将无法进行类型推断,您将不得不在 lambdas 中手动指定类型,这将极大地增加它们的冗长。举例来说,如果将变量词声明为原始类型列表而不是参数化类型列表,则上面的代码片段不会编译。
顺便说一句,如果用比较器构造方法代替 lambda,代码段中的比较器可以变得更加简洁(item 14.43):
Collections.sort(words, comparingInt(String::length));
事实上,利用Java 8中添加到List接口的sort方法,代码段可以变得更短:
words.sort(comparingInt(String::length));
向语言中添加 lambdas 使得在以前没有意义的地方使用函数对象变得更加实际。例如,考虑 item 34 中的操作 enum 类型。因为每个 enum 的 apply 方法需要不同的行为,所以我们使用特定于常量的类主体,并在每个 enum 常量中覆盖 apply 方法。为了唤醒你的记忆,这里再次展示代码:
// Enum type with constant-specific class bodies & data (Item 34)
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);
}
item 34 说 enum 实例字段比特定于常量的类主体更好。Lambdas 使得使用前者来实现特定于常量的行为变得很容易。只是将实现每个 enum 常量行为的 lambda 传递给它的构造函数。构造函数将 lambda 存储在一个实例字段中,而 apply 方法将调用转发给 lambda。生成的代码比原始版本更简单、更清晰:
// Enum with function object fields & constant-specific behavior
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);}
}
请注意,我们使用 DoubleBinaryOperator 接口来表示表示 enum 常量行为的 lambdas。这是java.util 中许多预定义的功能接口之一(Item 44)。它表示一个接受两个双参数并返回双结果的函数。
查看基于 lambda 的操作 enum,您可能会认为特定于常量的方法主体已经不再有用,但事实并非如此。与方法和类不同,lambdas 缺少名称和文档;如果一个计算不是自解释的,或者超过几行,不要把它放在lambda中。对于lambda来说,一行是最理想的,三行是合理的最大值。如果你违反了这个规则,它会对你的程序的可读性造成严重的伤害。如果一个lambda很长或者很难读,要么找到一种方法来简化它,要么重构你的程序来消除它。此外,传递给 enum 构造函数的参数是在静态上下文中求值的。因此,enum 构造函数中的 lambdas 不能访问 enum 的实例成员。如果 enum 类型具有难以理解的、不能在几行中实现的、或者需要访问实例字段或方法的、特定于常量的行为,那么特定于常量的类主体仍然是一种选择。
Lambdas 与匿名类同样无法跨实现可靠地序列化和反序列化它们的属性。因此,如果可能的话,您应该很少序列化 lambda (或匿名类实例)。如果您有一个想要序列化的函数对象,比如一个比较器,那么使用一个私有静态嵌套类的实例(item 24)。
总之,与Java 8一样,lambdas 是迄今为止表示小函数对象的最佳方式。不要为函数对象使用匿名类,除非必须创建非函数接口的类型实例。另外,请记住 lambdas 使表示小函数对象变得非常容易,这为以前在Java中不实用的函数式编程技术打开了大门。
网友评论