pursue wind pursue wind
首页
Java
Python
数据库
框架
Linux
中间件
前端
计算机基础
DevOps
项目
面试
书
关于
归档
MacOS🤣 (opens new window)
GitHub (opens new window)
首页
Java
Python
数据库
框架
Linux
中间件
前端
计算机基础
DevOps
项目
面试
书
关于
归档
MacOS🤣 (opens new window)
GitHub (opens new window)
  • EffectiveJava

    • 考虑使用静态工厂方法替代构造方法
    • 当构造方法参数过多时使用builder模式
    • 使用私有构造方法或枚类实现Singleton属性
    • 使用私有构造方法执行非实例化
    • 依赖注入优于硬连接资源(hardwiring resources)
    • 避免创建不必要的对象
    • 消除过期的对象引用
    • 避免使用Finalizer和Cleaner机制
    • 使用try-with-resources语句替代try-finally语句
    • 重写equals方法时遵守通用约定
    • 重写equals方法时同时也要重写hashcode方法
    • 始终重写 toString 方法
    • 谨慎地重写 clone 方法
    • 考虑实现Comparable接口
    • 使类和成员的可访问性最小化
    • 在公共类中使用访问方法而不是公共属性
    • 最小化可变性
    • 组合优于继承
    • 要么设计继承并提供文档说明,要么禁用继承
    • 接口优于抽象类
    • 为后代设计接口
    • 接口仅用来定义类型
    • 类层次结构优于标签类
    • 支持使用静态成员类而不是非静态类
    • 将源文件限制为单个顶级类
    • 不要使用原始类型
    • 消除非检查警告
    • 列表优于数组
    • 优先考虑泛型
    • 优先使用泛型方法
    • 使用限定通配符来增加API的灵活性
    • 合理地结合泛型和可变参数
    • 优先考虑类型安全的异构容器
    • 使用枚举类型替代整型常量
    • 使用实例属性替代序数
    • 使用EnumSet替代位属性
    • 使用EnumMap替代序数索引
    • 使用接口模拟可扩展的枚举
    • 注解优于命名模式
    • 始终使用Override注解
    • 使用标记接口定义类型
    • lambda表达式优于匿名类
    • 方法引用优于lambda表达式
    • 优先使用标准的函数式接口
    • 明智审慎地使用Stream
    • 优先考虑流中无副作用的函数
    • 优先使用Collection而不是Stream来作为方法的返回类型
    • 谨慎使用流并行
    • 检查参数有效性
    • 必要时进行防御性拷贝
    • 仔细设计方法签名
    • 明智审慎地使用重载
    • 明智审慎地使用可变参数
    • 返回空的数组或集合,不要返回 null
    • 明智审慎地返回 Optional
    • 为所有已公开的 API 元素编写文档注释
    • 最小化局部变量的作用域
    • for-each 循环优于传统 for 循环
    • 了解并使用库
    • 若需要精确答案就应避免使用 float 和 double 类型
    • 基本数据类型优于包装类
    • 当使用其他类型更合适时应避免使用字符串
    • 当心字符串连接引起的性能问题
    • 通过接口引用对象
    • 接口优于反射
    • 明智审慎地本地方法
    • 明智审慎地进行优化
    • 遵守被广泛认可的命名约定
    • 只针对异常的情况下才使用异常
    • 对可恢复的情况使用受检异常,对编程错误使用运行时异常
    • 避免不必要的使用受检异常
    • 优先使用标准的异常
    • 抛出与抽象对应的异常
    • 每个方法抛出的异常都需要创建文档
    • 在细节消息中包含失败一捕获信息
    • 保持失败原子性
    • 不要忽略异常
    • 同步访问共享的可变数据
    • 避免过度同步
    • executor 、task 和 stream 优先于线程
    • 并发工具优于 wait 和 notify
    • 文档应包含线程安全属性
    • 明智审慎的使用延迟初始化
    • 不要依赖线程调度器
    • 优先选择 Java 序列化的替代方案
    • 非常谨慎地实现 Serializable
    • 考虑使用自定义的序列化形式
    • 保护性的编写 readObject 方法
    • 对于实例控制,枚举类型优于 readResolve
    • 考虑用序列化代理代替序列化实例
  • On Java 8

  • 书
  • EffectiveJava
pursuewind
2020-11-22

优先使用标准的函数式接口

# 44. 优先使用标准的函数式接口

现在 Java 已经有 lambda 表达式,编写 API 的最佳实践已经发生了很大的变化。 例如,模板方法模式[Gamma95],其中一个子类重写原始方法以专门化其父类的行为,变得没有那么吸引人。 现代替代的选择是提供一个静态工厂或构造方法来接受函数对象以达到相同的效果。 通常地说,可以编写更多以函数对象为参数的构造方法和方法。 选择正确的函数式参数类型需要注意。

考虑 LinkedHashMap。 可以通过重写其受保护的 removeEldestEntry 方法将此类用作缓存,每次将新的 key 值加入到 map 时都会调用该方法。 当此方法返回 true 时,map 将删除传递给该方法的最久条目。 以下代码重写允许 map 增长到一百个条目,然后在每次添加新 key 值时删除最老的条目,并保留最近的一百个条目:

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
   return size() > 100;
}
1
2
3

这种技术很有效,但是你可以用 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);
}
1
2
3
4
5

这个接口可以正常工作,但是你不应该使用它,因为你不需要为此目的声明一个新的接口。 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 中提供的标准接口,但请注意,在相对罕见的情况下,最好编写自己的函数式接口。

Last Updated: 2023/01/30, 11:01:00
方法引用优于lambda表达式
明智审慎地使用Stream

← 方法引用优于lambda表达式 明智审慎地使用Stream→

Theme by Vdoing | Copyright © 2019-2023 pursue-wind | 粤ICP备2022093130号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
  • 飙升榜
  • 新歌榜
  • 云音乐民谣榜
  • 美国Billboard榜
  • UK排行榜周榜
  • 网络DJ