emsp;emsp;现在Java有了lamdba,编写api的最佳实践已经发生了很大的变化。比如, 模板方法模式 [Gamma95],其中子类覆盖原语方法以专门化其超类的行为,则吸引力要小得多。现代替代方法是提供一个静态工厂或构造方法来接受一个函数对象来实现相同的效果。更一般地,您将编写更多的构造函数和方法,这些构造函数和方法将函数对象作为参数。选择正确的函数参数类型需要谨慎。
emsp;emsp;考虑LinkedHashMap。你可以使用这个类重写它的protected removeEldestEntry方法来作为一个缓存,该方法在每次向映射添加新键时由put调用。当这个方法返回true的时候,map移除了它最老的条目,传入这个方法。下面的覆盖允许映射增长到100个条目,然后在每次添加一个新键时删除最老的条目,维护最近的100个条目:
emsp;emsp;这个技术工作正常,但是你可以用lambda做得更好。如果LinkedHashMap在今天编写,它将有一个静态工厂或构造方法传入函数对象。看removeEldestEntry的声明,你可能会认为方法对象应该传入Map.Entry<K,V>并返回一个boolean值,但这并不意味着::removeEldestEntry方法调用size()来获取映射中的条目数量,这是因为removeEldestEntry是映射上的一个实例方法。传递给构造函数的函数对象不是映射上的实例方法,因此无法捕获它,因为在调用其工厂或构造函数时映射还不存在,因此,映射必须将自己传递给函数对象,函数对象因此必须在输入时接收映射及其最老的条目。如果您要声明这样一个函数接口,它应该是这样的:
emsp;emsp;这个接口能正常工作,但是你不应该去使用它,因为你不需要为这个目的声明一个新的接口。java.util.function包提供了一个标准函数接口的大集合来给你使用。 如果标准函数式接口中的一个做了这个工作,你通常应该使用它而不是使用一个特定的函数式接口。这将使你的API更容易学习,通过减少其概念表面积,并将提供重要的互操作性好处,因为许多标准功能接口提供了有用的默认方法。Predicate接口,为实例而用,提供方法来连接判断。在我们的LinkedHashMap例子中的情况,标准BiPredicate<Map<K,V>,Map.Entry<K,V>接口应该比自定义的EldestEntryRemovalFunction接口更好。
emsp;emsp;在java.util.Function中有四十三个接口。你不可能期望能记住所有的它们,但是如果你六个基本接口,你就可以在需要的时候获取其他。基本接口在对象引用类型上操作。Operator接口表示结果和参数类型相同的函数。Predicate接口表示接受参数并返回布尔值的函数。谓词接口表示接受参数并返回布尔值的函数,Function接口表示参数和返回类型不同的函数.Supplier接口表示不接受参数并返回(或“提供”)值的函数。最后,接口表示不接受参数并返回(或“提供”)值的函数。Consumer表示接受参数但不返回任何值的函数,本质上是消费它的参数。这六个基本函数接口总结如下:
emsp;emsp;在对基本类型int、long和double进行操作的六个基本接口中,每个接口都有三个变体。它们的名称由基本接口派生而来,方法是在它们前面加上一个基本类型前缀。例如,接受int的谓词是IntPredicate,接受两个long值并返回long的二进制操作符是LongBinaryOperator。除了由返回类型参数化的函数变量之外,这些变量类型都没有参数化。例如,LongFunction<int[]>接受long并返回int[]。
emsp;emsp;函数接口还有9个附加变体,用于结果类型为基本类型时使用。源类型和结果类型总是不同的,因为从类型到自身的函数都是UnaryOperator。如果源类型和结果类型都是基本类型,则使用SrcToResult作为前缀函数,例如LongToIntFunction(6变种)。如果源是基元,结果是对象引用,则使用前缀函数具有<Src>ToObj,例如DoubleToObjFunction(三个变体)。
emsp;emsp;有两个参数版本的三个基本功能接口是有意义的:BiPredicate<T,U>,BiPredicate<T,U,R>,和BiPredicate<T,U>。还有返回三个相关原始类型的双函数变体:ToIntBiFunction<T、U>、ToLongBiFunction<T、U>和ToDoubleBiFunction < T U >。Consumer有两个参数变体,它们接受一个对象引用和一个基本类型:ObjDoubleConsumer<T>,ObjIntConsumer < T >, ObjLongConsumer < T >。总的来说,基本接口有9个两个参数版本。
emsp;emsp;最后,还有BooleanSupplier接口,它是Supplier的变体,返回布尔值。这是标准函数接口名中唯一明确提到布尔类型的地方,但是通过Predicate及其四种变体形式支持布尔返回值。BooleanSupplier接口和前面段落中描述的42个接口构成了所有43个标准功能接口。诚然,这是一件难以接受的事情,而且并不完全正交。另一方面,您将需要的大部分功能接口都是为您编写的,它们的名称非常规则,因此您在需要时应该不会遇到太多麻烦。
emsp;emsp;大多数标准功能接口的存在只是为了提供对基本类型的支持。 不要试图使用带有装箱基元的基本函数接口而不是基元函数接口。虽然它能正常工作,但它违反了item61的建议,”相比较装箱原生类型,最好使用原生类型“。在批量操作中使用装箱原语的性能后果可能是致命的。
emsp;emsp;现在你知道了你应该使用标准函数式接口而不是编写你自己的。但是什么时候你应该编写自己的呢?当然是如果标准库中没有你想要的你就需要去编写你自己的。,比如,如果你需要一个断言,传递三个挖参数,或者其中一个抛出一个检查异常,但是,有时您应该编写自己的函数接口,即使其中一个标准接口在结构上是相同的。
emsp;emsp;考虑到我们的老朋友 Comparator<T>,与ToIntBiFunction<T,T>接口结构类似。即使在前者被添加到标准库的时候后者接口早已存在,但使用它是错误的。这里有一些原因说明ToIntBiFunction值得它作为一个自有的接口。首先,它的名字每次在API中使用中提供了极好的文档,并且它使用很多。第二,Comparator接口对有效实例的构成有严格的要求,有效实例包括它的一般契约。通过实现这个接口,你就承诺遵守它的契约。第三,该接口大量配备了有用的缺省方法来转换和组合比较器。
emsp;emsp;如果你需要与Comparator共享一个或多个以下特性的功能接口,你应该认真考虑编写一个专用的功能接口,而不是使用标准接口:
- 它将被广泛使用,并且可以从描述性名称中获益
- 它与之有很强的联系
- 它将受益于自定义默认方法
emsp;emsp;如果你选择去编写你自己的函数式接口,记住他是一个接口并且在设计的时候要很小心( item21 )
emsp;emsp;注意到EldestEntryRemovalFunction接口被打上了 @FunctionalInterface注解的标签。这个注解类型在本质上与@Override类似。:这是一个程序员意图的声明,有三个目的:它告诉类及其文档的读者,该接口是为了启用lambdas而设计的;它让你保持诚实,因为接口不会编译,除非它有一个抽象的方法;它还可以防止维护人员在接口发展过程中意外地向接口添加抽象方法。经常在你的函数式接口上注解@FunctionalInterface 注解。
emsp;emsp;应该指出的最后一点是关于函数接口的使用api。不要提供具有多个重载的方法,如果该方法可能在客户机中造成歧义,则该方法将在相同的参数位置上使用不同的功能接口。这不只是一个理论问题。ExecutorService的submit方法可以调用Callable<T>或者一个Runnable,也可以编写一个客户机程序,该程序需要强制转换来指示正确的重载( item52 )。最简单的方法来避免这个问题就是不要编写可以重载的函数式接口在同样参数的情况下。这是item52的特别建议。谨慎使用重载。
emsp;emsp;总之,现在Java有了lambda,在你的API的设计中使用lambda是有必要的。在输入接受函数式接口类型在输出返回它们。通常使用标准接口提供的java.util,function.Function是最好的,但是请注意相对少见的情况,在这种情况下,您最好编写自己的函数接口。
本文写于2019.7.12,历时1天