Tips

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

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

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.方法引用优于lambda表达式

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

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

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

  3. Effective Java 第三版——58. 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. 优先考虑静态成员类

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

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

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

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

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

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

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

随机推荐

  1. (NO.00001)iOS游戏SpeedBoy Lite成形记(二)

    打开SpriteBuilder生成的Xcode项目文件,首先在MainScene添加play回调函数: -(void)play{ CCScene *gameScene = [CCBReader loa ...

  2. 开发资源库(repositiory)

    1.. 52研发网MTK软件 2.一流研发 3. android+MTK/华为的源代码及资料库(CryToCry96) 点击打开链接 4.android+MTK/华为/联想的源代码及资料库(lucka ...

  3. html5 jqueryrotate插件实现旋转动画

    CSS3 提供了多种变形效果,比如矩阵变形.位移.缩放.旋转和倾斜等等,让页面更加生动活泼有趣,不再一动不动.然后 IE10 以下版本的浏览器不支持 CSS3 变形,虽然 IE 有私有属性滤镜(fil ...

  4. DB Query Analyzer 6.02 is released, 71 articles concerned have been published

    DB Query Analyzer is presented by Master Genfeng, Ma from Chinese Mainland. It has English version n ...

  5. Redis服务信息

    想要获得下面的redis服务器信息,只需要在命令行中输入:info server 部分记录了 Redis 服务器的信息,它包含以下域: redis_version : Redis 服务器版本 redi ...

  6. android EventBus详解(三)

    post()方法调用流程 我们继续来看EventBus类,的另一个入口方法post() //已省略部分代码 public void post(Object event) { PostingThread ...

  7. 开源项目AndroidReview学习小结(2)

    读书破万卷下笔如有神 作为入门级的android码农的我,还是需要多多研读开源代码 下面继续接着上一篇的分析,这一篇主要介绍第一个tab,ReviewFragment的分析,界面看起来简单,背后的逻辑 ...

  8. mysql 无法插入中文

    MySQL数据库默认编码已经是utf8了, default-character-set = utf8,可是向数据库中表中插入中文时,却老是出现 ....\xB5\xA5\xD1\xA1 for col ...

  9. SQLSERVER 执行过的语句查询

    total_worker_time AS [总消耗CPU 时间(ms)], execution_count [运行次数], qs.total_worker_time AS [平均消耗CPU 时间(ms ...

  10. 配置使用dwr完成收邮件提示

    DWR(Direct Web Remoting)是一个用于改善web页面与Java类交互的远程服务器端Ajax开源框架,可以帮助开发人员开发包含AJAX技术的网站.它可以允许在浏览器里的代码使用运行在 ...