【笔记44】优先使用标准的函数式接口

  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 接口还有九个额外的变体,当结果类型为基本类型时使用。 源和结果类型总是不同,因为从类型到它自身的函数是 UnaryOperator。 如果源类型和结果类型都是基本类型,则使用带有 SrcToResult的前缀 Function,例如 LongToIntFunction(六个变体)。如果源是一个基本类型,返回结果是一个对象引用,那么带有 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 个接口占所有四十三个标准功能接口。 无可否认,这是非常难以接受的,并且不是非常正交的。 另一方面,你所需要的大部分功能接口都是为你写的,而且它们的名字是经常性的,所以在你需要的时候不应该有太多的麻烦。

  大多数标准函数式接口仅用于提供对基本类型的支持。 不要试图使用基本的函数式接口来装箱基本类型的包装类而不是基本类型的函数式接口。 虽然它起作用,但它违反了第 61 条中的建议:「优先使用基本类型而不是基本类型的包装类」。使用装箱基本类型的包装类进行批量操作的性能后果可能是致命的。

  现在你知道你应该通常使用标准的函数式接口来优先编写自己的接口。 但是,你应该什么时候写自己的接口? 当然,如果没有一个标准模块能够满足您的需求,例如,如果需要一个带有三个参数的 Predicate,或者一个抛出检查异常的 Predicate,那么需要编写自己的代码。 但有时候你应该编写自己的函数式接口,即使与其中一个标准的函数式接口的结构相同。

  考虑我们的老朋友 Comparator <T>,它的结构与 ToIntBiFunction <T, T> 接口相同。 即使将前者添加到类库时后者的接口已经存在,使用它也是错误的。 Comparator 值得拥有自己的接口有以下几个原因。 首先,它的名称每次在 API 中使用时都会提供优秀的文档,并且使用了很多。 其次,Comparator 接口对构成有效实例的构成有强大的要求,这些要求构成了它的普遍契约。 通过实现接口,就要承诺遵守契约。 第三,接口配备很多了有用的默认方法来转换和组合多个比较器。

  如果需要一个函数式接口与 Comparator 共享以下一个或多个特性,应该认真考虑编写一个专用函数式接口,而不是使用标准函数式接口:

  • 它将被广泛使用,并且可以从描述性名称中受益。
  • 它拥有强大的契约。
  • 它会受益于自定义的默认方法。

  如果选择编写你自己的函数式接口,请记住它是一个接口,因此应非常小心地设计(详见第 21 条)。

  请注意,EldestEntryRemovalFunction 接口(第 199 页)标有 @FunctionalInterface 注解。 这种注解在类型类似于 @Override。 这是一个程序员意图的陈述,它有三个目的:它告诉读者该类和它的文档,该接口是为了实现 lambda 表达式而设计的;它使你保持可靠,因为除非只有一个抽象方法,否则接口不会编译; 它可以防止维护人员在接口发生变化时不小心地将抽象方法添加到接口中。 始终使用 @FunctionalInterface注解标注你的函数式接口。

  最后一点应该是关于在 api 中使用函数接口的问题。不要提供具有多个重载的方法,这些重载在相同的参数位置上使用不同的函数式接口,如果这样做可能会在客户端中产生歧义。这不仅仅是一个理论问题。ExecutorService 的 submit 方法可以采用 Callable<T> 或 Runnable 接口,并且可以编写需要强制类型转换以指示正确的重载的客户端程序(详见第 52 条)。避免此问题的最简单方法是不要编写在相同的参数位置中使用不同函数式接口的重载。这是条目 52 中建议的一个特例,「明智地使用重载」。

  总之,现在 Java 已经有了 lambda 表达式,因此必须考虑 lambda 表达式来设计你的 API。 在输入上接受函数式接口类型并在输出中返回它们。 一般来说,最好使用 java.util.function.Function 中提供的标准接口,但请注意,在相对罕见的情况下,最好编写自己的函数式接口。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值