Effective Java 第三版读书笔记——条款 44:优先使用标准的函数式接口
现在 Java 已经有了 lambda 表达式,编写 API 的最佳实践已经发生了很大的变化。例如模板方法模式——其中一个子类重写原始方法以专门化其父类的行为——变得没有那么吸引人。现代替代的选择是提供一个静态工厂或构造方法来接受函数对象以达到相同的效果。通常地说,可以编写更多以函数对象为参数的构造方法和一般方法。
考虑 LinkedHashMap
。可以通过重写其受保护的 removeEldestEntry
方法将此类用作缓存,每次将新的 key 值加入到 map 时都会调用该方法。当此方法返回 true 时,map 将删除传递给该方法的最“老”的条目。以下代码重写允许 map 增长到一百个条目,然后在每次添加新 key 值时删除最老的条目,并保留最近的一百个条目:
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return size() > 100;
}
这种技术很有效,但是你可以用 lambdas 做得更好。如果 LinkedHashMap
是现在编写的,那么它将有一个静态工厂或构造方法来获取函数对象。查看 removeEldestEntry
方法的声明,你可能会认为函数对象应该接受一个 Map.Entry <K,V>
并返回一个布尔值,但是这并不完全是这样: removeEldestEntry
方法调用 size()
方法来获取条目的数量,因为 removeEldestEntry
是 map 上的一个实例方法。传递给构造方法的函数对象不是 map 上的实例方法并且无法捕获,因为在调用其工厂或构造方法时 map 还不存在。因此,map 必须将自己传递给函数对象,函数对象把 map 以及最老的条目作为输入参数。如果要声明这样一个函数式接口,应该是这样的:
// Unnecessary functional interface; use a standard one instead.
@FunctionalInterface interface EldestEntryRemovalFunction<K,V>{
boolean remove(Map<K,V> map, Map.Entry<K,V> eldest);
}
这个接口可以正常工作,但是你不应该使用它,因为你不需要为此目的声明一个新的接口。java.util.function
包提供了大量标准函数式接口供你使用。如果其中一个标准函数式接口完成这项工作,则通常应该优先使用它,而不是专门构建函数式接口。这将使你的 API 更容易理解,减少其不必要的概念,并将提供重要的互操作性好处,因为许多标准函数式接口提供了有用的默认方法。例如,Predicate
接口提供了组合判断的方法。在我们的 LinkedHashMap
示例中,标准的 BiPredicate<Map<K,V>, Map.Entry<K,V>>
接口应优先于自定义的 EldestEntryRemovalFunction
接口的使用。
在 java.util.Function 中有 43 个接口。如果记住了六个基本接口,就可以在需要它们时派生出其余的接口。基本接口操作于对象引用类型。Operator
接口表示方法的结果和参数类型相同。Predicate
接口表示其方法接受一个参数并返回一个布尔值。Function
接口表示方法的参数和返回类型不同。Supplier
接口表示一个不接受参数但是返回(或“供应”)一个值的方法。最后,Consumer
表示该方法接受一个参数而不返回任何东西。六种基本函数式接口概述如下:
接口 | 函数签名 | 示例 |
---|---|---|
UnaryOperator<T> |
T apply(T t) |
String::toLowerCase |
BinaryOperator<T> |
T apply(T t1, T t2) |
BigInteger::add |
Predicate<T> |
boolean test(T t) |
Collection::isEmpty |
Function<T,R> |
R apply(T t) |
Arrays::asList |
Supplier<T> |
T get() |
Instant::now |
Consumer<T> |
void accept(T t) |
System.out::println |
在处理基本类型 int,long 和 double 的操作上,六个基本接口还有三个变体。它们的名字是通过在基本接口前加一个基本类型而得到的。因此,一个接受 int 的 Predicate
是一个 IntPredicate
,而一个接受两个 long 值并返回一个 long 的二元运算符是一个 LongBinaryOperator
。除 Function
接口变体通过返回类型进行了参数化,其他变体类型都没有参数化。例如,LongFunction<int[]>
使用 long 类型作为参数并返回了 int []
类型。
Function
接口还有九个额外的变体,当结果类型为基本类型时使用。Function
接口的源和结果类型总是不同,因为从一个类型到它自身的函数是UnaryOperator
。如果源类型和结果类型都是基本类型,则使用带有SrcToResult
前缀的 Function
,例如 LongToIntFunction
(六个变体)。如果源是一个基本类型,返回结果是一个对象引用,那么使用带有 <Src>ToObj
前缀的 Function
,例如 DoubleToObjFunction
(三种变体)。
还有三个基本函数式接口的包含两个参数的版本:BiPredicate <T,U>
,BiFunction <T,U,R>
和 BiConsumer <T,U>
。也有返回三种相关基本类型的 BiFunction
变体:ToIntBiFunction <T,U>
,ToLongBiFunction <T,U>
和 ToDoubleBiFunction <T,U>
。Consumer
有两个变体,它们带有一个对象引用和一个基本类型:ObjDoubleConsumer <T>
,ObjIntConsumer <T>
和 ObjLongConsumer <T>
。总共有九个两个参数版本的基本接口。
最后,还有一个 BooleanSupplier
接口,它是 Supplier
的一个变体,它返回布尔值。这是所有标准函数式接口里名称中唯一明确提及的布尔类型,但 Predicate
及其四种变体支持布尔返回值。前面段落中介绍的 BooleanSupplier
接口和 42 个接口组成了所有 43 个标准函数式接口。
大多数标准函数式接口仅用于提供对基本类型的支持。不要试图将基本函数式接口与基本类型的包装类一起使用而不是基本类型。虽然它也可以工作,但它违反了条款 61 中的建议:“优先使用基本类型而不是基本类型的包装类”。使用基本类型的包装类进行批量操作的性能后果可能是致命的。
现在你知道应该通常使用标准的函数式接口而不是自己编写的接口。但是,你应该什么时候写自己的接口?当然,如果没有一个标准函数式接口能够满足你的需求,例如,如果需要一个带有三个参数的 Predicate
,或者一个抛出检查异常的 Predicate
,那么需要编写自己的代码。但有时候你应该编写自己的函数式接口,即使它与其中一个标准函数式接口的结构相同。
考虑我们的老朋友 Comparator <T>
,它的结构与 ToIntBiFunction <T, T>
接口相同。即使将前者添加到类库时后者的接口已经存在,使用后者也是错误的。Comparator
值得拥有自己的接口有以下几个原因。首先,它的名称每次在 API 中使用时都会提供优秀的文档,并且使用了很多。其次,Comparator
接口对构成有效实例的构成有强大的要求,这些要求构成了它的普遍契约。通过实现接口,就要承诺遵守契约。第三,接口配备了有用的默认方法来转换和组合多个比较器。
如果需要一个函数式接口与 Comparator
共享以下一个或多个特性,应该认真考虑编写一个专用函数式接口,而不是使用标准函数式接口:
- 它将被广泛使用,并且可以从描述性名称中受益。
- 它拥有强大的契约。
- 它会受益于自定义的默认方法。
如果选择编写你自己的函数式接口,请记住它是一个接口,因此应非常小心地设计(条款 21)。
请注意,EldestEntryRemovalFunction
接口标有 @FunctionalInterface
注解。这种注解类似于 @Override
。这是一个对程序员意图的陈述,它有三个目的:它告诉该类的读者和它的文档,该接口是为了实现 lambda 表达式而设计的;它使你保持可靠,因为除非只有一个抽象方法,否则接口不会编译;它可以防止维护人员在接口发生变化时不小心地将抽象方法添加到接口中。始终使用@FunctionalInterface注解标注你的函数式接口。
总之,现在 Java 已经有了 lambda 表达式,因此必须考虑使用 lambda 表达式来设计你的 API。在输入上接受函数式接口类型并在输出中返回它们。一般来说,最好使用 java.util.function.Function
中提供的标准接口,但请注意,在相对罕见的情况下,最好编写自己的函数式接口。