在Java具有Lambda 表达式之后,编写API 的最佳实践也做了相应的改变。例如在模板方法(Tmplate Method)模式[Gamma95]中,用一个子类覆盖基本类型方法(primitive method),来限定其超类的行为,这是最不讨人喜欢的。现在的替代方法是提供一个接受函数对象的静态工厂或者构造器,便可达到同样的效果。在大多数情况下,需要编写更多的构造器和方法,以函数对象作为参数。需要非常谨慎地选择正确的函数参数类型。
以LinkedHashMap为例。每当有新的键添加到映射中时,put 就会调用其受保护的 removeEldestEntry方法。如果覆盖该方法,便可以用这个类作为缓存。当该方法返回true,映射就会删除最早传入方法的条目。下列覆盖代码允许映射增长到100个条目,然后每添加一个新的键,就会删除最早的那个条目,始终保持最新的100个条目:
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > 100;
}
这个方法很好用,但是用Lambda 可以完成得更漂亮。假如现在编写LinkedHashMap,它会有一个带函数对象的静态工厂或者构造器。看一下 removeEldestEntry 的声明,你可能会以为该函数对象应该带一个Map.Entry
// Unnecessary functional interface; use a standard one instead
@FunctionalInterface
public interface EldestEntryRemovalFunction {
boolean remove(Map map,Map.Entry eldest);
}
这个接口可以正常工作,但是不应该使用,因为没必要为此声明。java.util.function 包已经为此提供了大量标准的函数接口。 只要标准的函数接口能够满足需求,通常应该优先考虑,而不是专门再构建一个新的函数接口。这样会使API 更加容易学习,通常减少它的概念内容,显著提升互操作性优势,因为许多标准的函数接口都提供了有用的默认方法。如 Predicate 接口提供了合并断言的方法。对于上述 LinkedHashMap范例,应该优先使用标准的 BiPredicate
java.util.function 中共有43个接口。别指望能够全部记住它们,但是如果能记住其中6个基础接口,必要时就可以推断出其余接口了。基础接口作用域对象引用类型。Operator 接口代表其结果与参数类型一致的函数。Predicate 接口代表带有一个参数的并返回一个boolean 的函数。Function 接口 代表其参数与返回的类型不一致的函数。 Supplier 接口代表没有参数并且返回(或“提供”)一个值的函数。最后,Consumer 代表的是带有一个函数但不返回任何值的函数,相当于消费掉了其参数。这6个基础函数表述如下
这6个基础接口各自还有3种变体,分别可以作用于基本类型 int、long、double。它们的命名方式是在其基础接口前面加上基本类型。因此,以带有 int 的predicate接口为例,其变体名称应该是 IntPredicate。 这些变体接口的类型都不是参数化的,除了Function 变体外,后者是以返回类型为参数。例如,LongFunction
Function接口还有9种变体,用于结果类型为基本类型的情况。源类型和结果类型始终不一样,因为从类型到自身的函数就是UnaryOperator。如果源类型和结果类型均为基本类型,就是在Function前面添加格式如 SrcToResult 如 LongToIntFunction(有6种变体)。如果源类型为基本类型,结果类型是一个对象参数,则要在Function前面添加
这3种基础函数接口还有带两个参数的版本,如BiPredicate
最后,还有BooleanSupplier接口,它是Supplier 接口的一种变体,返回boolean值。这是在所有的标准函数接口名称中唯一显式提到boolean 类型的,但boolean返回值是通过Predicate 及其4种变体来支持的。BooleanSupplier接口和上述提到的42个接口,总计43个标准函数接口。显然,这是个大数字,但是他们之间并非纵横交错。另一方面,你需要的接口函数都替你写好了,它们的名称都是循规蹈矩的,需要的时候并不难找到。
现有的大多数标准函数接口都只支持基本类型。千万不要用带包装类型的基础函数接口来代替函数接口。虽然可行,但它破坏了第61条的规则“基本类型优于装箱基本类型”。使用装箱基本类型进行批量操作处理,最终会导致致命的性能问题。
现在知道了,通常应该优先使用标准的函数接口,而不是用自己编写的接口。但什么时候一听该自己编写接口呢?当然是在如果没有任何标准的函数接口能满足你的需求之时,如果需要一个带有3个参数的Predicate接口,或者需要一个抛出受检异常的接口时,当然就需要自己编写啦。但是也有这样的情况:有结构相同的标准函数接口可用,却还是应该自己编写函数接口。
还是以咱们的老朋友 Comparator
如果你所需要的函数接口与Comparator一样具有一项或者多项以下特征,则必须认真考虑自己编写专用的函数接口,而不是使用标准的函数接口:
如果决定自己编写函数接口,一定要记住,它是一个接口,因而设计时应当万分谨慎(详见第21条)。
注意,EldestEntryRemovalFunction接口是用 @FunctionalInterface 注解进行标注的。这个注解类型本质上与@Override类似。这是一个标注了程序员设计意图的语句,它有3个目的:告诉这个类及其文档的读者,这个接口是针对Lambda设计的;这个接口不会进行编译,除非它只有一个抽象方法;避免后续维护人员不小心给该接口添加抽象方法。必须始终用 @FunctionalInterface 注解对自己编写的函数接口进行标注。
最后一点是关于函数接口在API中的使用。不要在相同的参数位置,提供不同的函数接口来进行多次重载的方法,否则可能在客户端导致歧义。这不仅仅是理论上的问题。比如ExecutorService的submit方法就可能带有Callable
总而言之,既然Java有了Lambda,就必须时刻谨记用Lambda来设计API。输入时接受函数接口类型,并在输出时返回之。一般来说,最好使用java.util.function.Function中提供的标准接口,但是必须警惕在相对罕见的几种情况下,最好还是自己编写专用的函数接口。
页面更新:2024-05-07
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号