Effective Java 第三版——42.lambda表达式优于匿名类

时间:2022-02-22 10:03:23

Tips

《Effective Java, Third Edition》一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化。

在这里第一时间翻译成中文版。供大家学习分享之用。

Effective Java 第三版——42.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());

    }

});

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

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

// Lambda expression as function object (replaces anonymous class)

Collections.sort(words,

        (s1, s2) -> Integer.compare(s1.length(), s2.length()));

请注意,代码中不存在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));

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

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

将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);

}

第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);

    }

}

请注意,我们使用表示枚举常量行为的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中以前并不实用。

Effective Java 第三版——42.lambda表达式优于匿名类的更多相关文章

  1. Effective Java 第三版——43&period;方法引用优于lambda表达式

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  2. Effective Java 第三版——61&period; 基本类型优于装箱的基本类型

    Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,所 ...

  3. Effective Java 第三版——58&period; for-each循环优于传统for循环

    Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,所 ...

  4. 《Effective Java 第三版》目录汇总

    经过反复不断的拖延和坚持,所有条目已经翻译完成,供大家分享学习.时间有限,个别地方翻译得比较仓促,希望有疑虑的地方指出批评改正. 第一章简介 忽略 第二章 创建和销毁对象 1. 考虑使用静态工厂方法替 ...

  5. 《Effective Java 第三版》新条目介绍

    版权声明:本文为博主原创文章,可以随意转载,不过请加上原文链接. https://blog.csdn.net/u014717036/article/details/80588806前言 从去年的3月份 ...

  6. Effective Java 第三版——24&period; 优先考虑静态成员类

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  7. Effective Java 第三版——44&period; 优先使用标准的函数式接口

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  8. Effective Java 第三版——14&period;考虑实现Comparable接口

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  9. Effective Java 第三版——21&period; 为后代设计接口

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

随机推荐

  1. jQuery 消息提示&sol;通知插件

    常见消息提醒,类似于Chrome notification,易于使用,用户体验赞. // Simple $.sticky('hi, every body rock!'); // Advantage $ ...

  2. GitHub Linux下使用方法

    1. 在网站注册帐号,创建工程 test 进入工程,右下角会有一个项目仓库的地址.https://github.com/braverior/test.git 2.Linux下 安装github sud ...

  3. HOG OpenCV 代码片段

    直接上代码: #include <opencv2/opencv.hpp> using namespace cv; #include <cmath> using namespac ...

  4. 【Java编程思想笔记】注解--元注解

    参考文章:(小白的小小白的白 )https://blog.csdn.net/weixin_42315600/article/details/80630669 https://www.cnblogs.c ...

  5. 【转】这五种方法前四种方法只支持IE浏览器,最后一个方法支持当前主流的浏览器(火狐,IE,Chrome,Opera,Safari)

    <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8&quo ...

  6. 遇到返回键会退到页面的问题(window&period;location)

    我的需求是a全局列表页->b展示列表页->c新增页(编辑页)我从b展示列表页,通过编辑进入c编辑页,保存回到b展示列表页. 重,我的b展示列表页,返回要返回的其实是a全局列表页*使用rep ...

  7. kbmmw 的远程桌面功能

    kbmmw 内置了远程桌面控制功能好几年了,好多同学居然不知道这特性,因为kbmmw 默认没有开放这个特性, 今天我就给大家说一下如何开放这个功能,并用官方自带例子说一下使用方法. 首先要开放这个特性 ...

  8. php 导出excel文件

    out_excel.php <?phperror_reporting(E_ALL);date_default_timezone_set('Asia/Shanghai');require_once ...

  9. CHEMISTS DISCOVER A SAFE&comma; GREEN METHOD TO PROCESS RED PHOSPHORUS

                   When it comes to making phosphorus compounds, chemists have traditionally relied on w ...

  10. Java 注解全面解析

    1.基本语法 注解定义看起来很像接口的定义.事实上,与其他任何接口一样,注解也将会编译成class文件. @Target(ElementType.Method) @Retention(Retentio ...