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

lambda表达式优于匿名类

# 42. lambda 表达式优于匿名类

在 Java 8 中,添加了函数式接口,lambda 表达式和方法引用,以便更容易地创建函数对象。 Stream API 随着其他语言的修改一同被添加进来,为处理数据元素序列提供类库支持。 在本章中,我们将讨论如何充分利用这些功能。

以往,使用单一抽象方法的接口(或者很少使用的抽象类)被用作函数类型。 它们的实例(称为函数对象)表示函数(functions)或行动(actions)。 自从 JDK 1.1 于 1997 年发布以来,创建函数对象的主要手段就是匿名类(详见第 24 条)。 下面是一段代码片段,按照字符串长度顺序对列表进行排序,使用匿名类创建排序的比较方法(强制排序顺序):

// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return Integer.compare(s1.length(), s2.length());
    }
});
1
2
3
4
5
6

匿名类适用于需要函数对象的经典面向对象设计模式,特别是策略模式[Gamma95]。 比较器接口表示排序的抽象策略; 上面的匿名类是排序字符串的具体策略。 然而,匿名类的冗长,使得 Java 中的函数式编程成为一种不吸引人的设想。

在 Java 8 中,语言形式化了这样的概念,即使用单个抽象方法的接口是特别的,应该得到特别的对待。 这些接口现在称为函数式接口,并且该语言允许你使用 lambda 表达式或简称 lambdas 来创建这些接口的实例。 Lambdas 在功能上与匿名类相似,但更为简洁。 下面的代码使用 lambdas 替换上面的匿名类。 样板不见了,行为清晰明了:

// Lambda expression as function object (replaces anonymous class)
Collections.sort(words,
        (s1, s2) -> Integer.compare(s1.length(), s2.length()));
1
2
3

请注意,代码中不存在 lambda(Comparator <String>),其参数(s1 和 s2,都是 String 类型)及其返回值(int)的类型。 编译器使用称为类型推断的过程从上下文中推导出这些类型。 在某些情况下,编译器将无法确定类型,必须指定它们。 类型推断的规则很复杂:他们在 JLS 中占据了整个章节[JLS,18]。 很少有程序员详细了解这些规则,但没关系。 除非它们的存在使你的程序更清晰,否则省略所有 lambda 参数的类型。 如果编译器生成一个错误,告诉你它不能推断出 lambda 参数的类型,那么指定它。 有时你可能不得不强制转换返回值或整个 lambda 表达式,但这很少见。

关于类型推断需要注意一点。 条目 26 告诉你不要使用原始类型,条目 29 告诉你偏好泛型类型,条目 30 告诉你偏向泛型方法。 当使用 lambda 表达式时,这个建议是非常重要的,因为编译器获得了大部分允许它从泛型进行类型推断的类型信息。 如果你没有提供这些信息,编译器将无法进行类型推断,你必须在 lambdas 中手动指定类型,这将大大增加它们的冗余度。 举例来说,如果变量被声明为原始类型 List 而不是参数化类型 List<String>,则上面的代码片段将不会编译。

顺便提一句,如果使用比较器构造方法代替 lambda,则代码中的比较器可以变得更加简洁(详见第 14 和 43 条):

Collections.sort(words, comparingInt(String::length));
1

实际上,通过利用添加到 Java 8 中的 List 接口的 sort 方法,可以使片段变得更简短:

words.sort(comparingInt(String::length));
1

将 lambdas 添加到该语言中,使得使用函数对象在以前没有意义的地方非常实用。例如,考虑条目 34 中的 Operation 枚举类型。由于每个枚举都需要不同的应用程序行为,所以我们使用了特定于常量的类主体,并在每个枚举常量中重写了 apply 方法。为了刷新你的记忆,下面是之前的代码:

// Enum type with constant-specific class bodies & data 
public enum Operation {
    PLUS("+") {
        public double apply(double x, double y) { return x + y; }
    },

    MINUS("-") {
        public double apply(double x, double y) { return x - y; }
    },

    TIMES("*") {
        public double apply(double x, double y) { return x * y; }
    },

    DIVIDE("/") {
        public double apply(double x, double y) { return x / y; }
    };

    private final String symbol;

    Operation(String symbol) { this.symbol = symbol; }

    @Override 
    public String toString() { return symbol; }

    public abstract double apply(double x, double y);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

第 34 条目说,枚举实例属性比常量特定的类主体更可取。 Lambdas 可以很容易地使用前者而不是后者来实现常量特定的行为。 仅仅将实现每个枚举常量行为的 lambda 传递给它的构造方法。 构造方法将 lambda 存储在实例属性中,apply 方法将调用转发给 lambda。 由此产生的代码比原始版本更简单,更清晰:

public enum Operation {
    PLUS  ("+", (x, y) -> x + y),
    MINUS ("-", (x, y) -> x - y),
    TIMES ("*", (x, y) -> x * y),
    DIVIDE("/", (x, y) -> x / y);

    private final String symbol;
    private final DoubleBinaryOperator op;

    Operation(String symbol, DoubleBinaryOperator op) {
        this.symbol = symbol;
        this.op = op;
    }

    @Override 
    public String toString() { return symbol; }
    
    public double apply(double x, double y) {
        return op.applyAsDouble(x, y);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

请注意,我们使用表示枚举常量行为的 lambdas 的 DoubleBinaryOperator 接口。 这是 java.util.function 中许多预定义的函数接口之一(详见第 44 条)。 它表示一个函数,它接受两个 double 类型参数并返回 double 类型的结果。

看看基于 lambda 的 Operation 枚举,你可能会认为常量特定的方法体已经失去了它们的用处,但事实并非如此。 与方法和类不同,lambda 没有名称和文档; 如果计算不是自解释的,或者超过几行,则不要将其放入 lambda 表达式中。 一行代码对于 lambda 说是理想的,三行代码是合理的最大值。 如果违反这一规定,可能会严重损害程序的可读性。 如果一个 lambda 很长或很难阅读,要么找到一种方法来简化它或重构你的程序来消除它。 此外,传递给枚举构造方法的参数在静态上下文中进行评估。 因此,枚举构造方法中的 lambda 表达式不能访问枚举的实例成员。 如果枚举类型具有难以理解的常量特定行为,无法在几行内实现,或者需要访问实例属性或方法,那么常量特定的类主体仍然是行之有效的方法。

同样,你可能会认为匿名类在 lambda 时代已经过时了。 这更接近事实,但有些事情你可以用匿名类来做,而却不能用 lambdas 做。 Lambda 仅限于函数式接口。 如果你想创建一个抽象类的实例,你可以使用匿名类来实现,但不能使用 lambda。 同样,你可以使用匿名类来创建具有多个抽象方法的接口实例。 最后,lambda 不能获得对自身的引用。 在 lambda 中,this 关键字引用封闭实例,这通常是你想要的。 在匿名类中,this 关键字引用匿名类实例。 如果你需要从其内部访问函数对象,则必须使用匿名类。

Lambdas 与匿名类共享无法可靠地序列化和反序列化实现的属性。因此,应该很少 (如果有的话) 序列化一个 lambda(或一个匿名类实例)。 如果有一个想要进行序列化的函数对象,比如一个 Comparator,那么使用一个私有静态嵌套类的实例(详见第 24 条)。

综上所述,从 Java 8 开始,lambda 是迄今为止表示小函数对象的最佳方式。 除非必须创建非函数式接口类型的实例,否则不要使用匿名类作为函数对象。 另外,请记住,lambda 表达式使代表小函数对象变得如此简单,以至于它为功能性编程技术打开了一扇门,这些技术在 Java 中以前并不实用。

Last Updated: 2023/01/30, 11:01:00
使用标记接口定义类型
方法引用优于lambda表达式

← 使用标记接口定义类型 方法引用优于lambda表达式→

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