Page d'accueil 图灵程序设计丛书:Java进阶高手(套装全8册 Java 8函数式编程 Java技术手册(第6版)..

图灵程序设计丛书:Java进阶高手(套装全8册 Java 8函数式编程 Java技术手册(第6版) Java性能权威指南 Java编程思维 Java攻略:Java常见问题的简单解法 精通Java并发编程(第2版) Java实战(第2版) Java虚拟机基础教程)

, , , , , , , , , ,
4.0 / 0
Avez-vous aimé ce livre?
Quelle est la qualité du fichier téléchargé?
Veuillez télécharger le livre pour apprécier sa qualité
Quelle est la qualité des fichiers téléchargés?
作者简介
Richard Warburton 一位经验丰富的技术专家,善于解决复杂深奥的技术问题,拥有华威大学计算机科学专业博士学位。近期他一直从事高性能计算方面的数据分析工作。他是英国伦敦Java社区的领 导者,组织过面向Java 8中Lambda表达式、日期和时间的Adopt-a-JSR项目,以及Openjdk Hackdays活动。Richard还是知名的会议演讲嘉宾,曾在JavaOne、DevoxxUK和JAX London等会议上演讲。

Benjamin J. Evans是jClarity公司的联合创始人,伦敦Java用户组的组织者,JCP执行委员会委员。Java Champion和JavaOne Rockstar荣誉得主。与人合著有《Java程序员修炼之道》。他经常就Java平台、性能、并发和相关主题发表公开演讲。 David Flanagan是Mozilla的高 级前端软件工程师,著有《JavaScriptquanwei指南》《Ruby编程语言》等。

Scott Oaks是Oracle公司的一位架构师,专注研究Oracle中间件软件的性能。加入Oracle之前,他曾于Sun Microsystem公司任职多年,在多个技术领域都有建树,包括SunOS的内核、网络程序设计、Windows系统的远程方法调用(RPC)以及OPEN LOOK虚拟窗口管理器。1996年,Scott成为Sun公司的Java布道师,并于2001年加入Sun公司的Java性能小组——从那时起他就一直专注于Java的性能提升。此外,Scott也在O’Reilly出版社出版了多部书籍,包括Java Security、Java Threads、JXTA in a Nutshell和Jini in a Nutshell。

Allen B. Downey 欧林学院的计算机教授。曾任教于韦尔斯利女子学院、科尔比学院和加州大学伯克利分校;拥有加州大学伯克利分校计算机博士学位以及麻省理工学院学士和硕士学位。 Chris Mayfield 詹姆斯麦迪逊大学的计算机助理教授,致力于计算机教育和职业发展的研究;拥有普渡大学计算机博士学位以及犹他大学计算机和德语学士学位。

[美]肯·寇森(Ken Kousen) 独立咨询师与培训讲师,Kousen IT公司总裁;对Spring、Hibernate、Groovy、Grails等语言和框架颇有研究;荣膺2013年和2016年JavaOne Rock Star大奖;毕业于MIT并取得了普林斯顿大学博士学位。

哈维尔·费尔南德斯·冈萨雷斯(Javier Fernández González):软件架构师,拥有十余年Java研发经验,对J2EE、Struts框架和使用Java开发大规模数据处理的应用程序颇有心得,为保险、医疗、交通等领域客户开发了许多J2EE Web应用程序。

拉乌尔–加布里埃尔·乌尔玛(Raoul-Gabriel Urma),剑桥大学计算机科学博士,软件工程师,培训师,现任Cambridge Spark公司CEO。在谷歌、eBay、甲骨文和高盛等大公司工作过,并参与过多个创业项目。活跃在技术社区,经常撰写技术文章,多次受邀在国际会议上做技术讲座。 马里奥·富斯科(Mario Fusco),Red Hat高级软件工程师,负责JBoss规则引擎Drools的核心开发。拥有丰富的Java开发经验,曾领导媒体公司、金融部门等多个行业的企业级项目开发。对函数式编程和领域特定语言等有浓厚兴趣,并创建了开放源码库lambdaj。 艾伦·米克罗夫特(Alan Mycroft),剑桥大学计算机实验室计算学教授,剑桥大学罗宾逊学院研究员,欧洲编程语言和系统协会联合创始人,树莓派基金会联合创始人和理事。发表过大约100篇研究论文,指导过20多篇博士论文。他的研究主要关注编程语言及其语义、优化和实施。他与业界联系紧密,曾于学术休假期间在AT&T实验室和英特尔工作,还创立了Codemist公司,该公司设计了最初的ARM C编译器Norcroft。
Vincent van der Leun
全栈工程师,Oracle数据库认证专家。8岁开始编程,熟悉多种语言和平台,维护着JVM Fanboy博客。目前就职于致力于现代电子商务解决方案的CloudSuite公司。

内容简介
本套装共包含《Java 8函数式编程》、《Java技术手册(第6版)》、《Java性能权威指南》、《Java编程思维》、《Java攻略:Java常见问题的简单解法》、《精通Java并发编程(第2版)》、《Java实战(第2版)》、《Java虚拟机基础教程》8本书

《Java实战(第2版)》全面介绍了Java 8、9、10版本的新特性,包括Lambda表达式、方法引用、流、默认方法、Optional、CompletableFuture以及新的日期和时间API,是程序员了解Java新特性的经典指南。全书共分六个部分:基础知识、使用流进行函数式数据处理、使用流和Lambda进行高效编程、无所不在的Java、提升Java的并发性、函数式编程以及Java未来的演进。

多年以来,函数式编程被认为是少数人的游戏,不适合推广给普罗大众。写作《Java 8函数式编程》的目的就是为了挑战这种思想。本书将探讨如何编写出简单、干净、易读的代码;如何简单地使用并行计算提高性能;如何准确地为问题建模,并且开发出更好的领域特定语言;如何写出不易出错,并且更简单的并发代码;如何测试和调试Lambda表达式。 如果你已经掌握Java SE,想尽快了解Java 8新特性,写出简单干净的代码,那么本书不容错过。

《Java技术手册 第6版》为《Java 技术手册》的升级版,涵盖全新的Java 7 和Java 8。第 1部分介绍Java 编程语言和Java 平台,主要内容有Java 环境、Java 基本句法、Java 面向对象编程、Java 类型系统、Java的面向对象设计、Java 实现内存管理和并发编程的方式。第 2部分通过大量示例来阐述如何在Java 环境中完成实际的编程任务,主要内容有编程和文档约定,使用Java 集合和数组,处理常见的数据格式,处理文件和I/O,类加载、反射和方法句柄,Nashorn,以及平台工具和配置。

《Java性能权威指南》对Java 7和Java 8中影响性能的因素展开了全面深入的介绍,讲解传统上影响应用性能的JVM特征,包括即时编译器、垃圾收集、语言特征等。内容包括:用G1垃圾收集器应用的吞吐量;使用Java飞行记录器查看性能细节,而不必借助专业的分析工具;堆内存与原生内存实践;线程与同步的性能,以及数据库性能实践等。

《Java编程思维》从基本的编程术语入手,用代码示例诠释计算机科学概念,旨在教会读者像计算机科学家那样思考,并掌握解决问题这一重要技能。书中内容共分为14章、3个附录,每章末都附有术语表和练习。 本书适合想学习计算机科学和编程相关内容的初学者。

《Java攻略:Java常见问题的简单解法》旨在让读者迅速掌握Java 8和Java 9相关特性,并给出了70余个可以用于实际开发的示例,介绍了如何利用这些新特性解决这些问题,从而以更自然的方式让开发人员掌握Java。 本书适合Java开发人员阅读。

Java 提供了一套非常强大的并发API,可以轻松实现任何类型的并发应用程序。《精通Java并发编程(第2版)》讲述Java 并发API 最重要的元素,包括执行器框架、Phaser 类、Fork/Join 框架、流API、并发数据结构、同步机制,并展示如何在实际开发中使用它们。此外,本书还介绍了设计并发应用程序的方法论、设计模式、实现良好并发应用程序的提示和技巧、测试并发应用程序的工具和方法,以及如何使用面向Java 虚拟机的其他编程语言实现并发应用程序。

《Java虚拟机基础教程》概述Java 虚拟机(JVM)及其特性,并用大量示例详细介绍了Java、Scala、Clojure、Kotlin 和Groovy 这5 种基于JVM 的语言。具体而言,首先概述了Java 平台,紧接着详细阐述了JVM,然后分别介绍了上述各种语言的基础知识和核心概念,并运用它们开发项目、创建应用程序。
Année:
2019
Editeur::
人民邮电出版社有限公司
Langue:
chinese
ISBN 13:
9787115477798
ISBN:
2147483647
Fichier:
EPUB, 19,15 MB
Télécharger (epub, 19,15 MB)

Cela peut vous intéresser Powered by Rec2Me

 

Mots Clefs

 
0 comments
 

To post a review, please sign in or sign up
Vous pouvez laisser un commentaire et partager votre expérience. D'autres lecteurs seront intéressés de connaitre votre opinion sur les livres lus. Qu'un livre vous plaise ou non, si vous partagez honnêtement votre opinion à son sujet, les autres pourront découvrir de nouveaux livres qui pourraient les intéresser.
1

Viable Project Business: A Bionic Management System for Large Enterprises

İl:
2020
Dil:
english
Fayl:
PDF, 13,50 MB
0 / 0
2

Algebra for JEE (Advanced)

Dil:
english
Fayl:
PDF, 35,85 MB
0 / 0
总目录


Java实战(第2版)

Java虚拟机基础教程

Java性能权威指南

精通Java并发编程(第2版)

Java 8函数式编程

Java编程思维

Java技术手册(第6版)

Java攻略:Java常见问题的简单解法





版权信息


书名:Java实战(第2版)

作者:[英] 拉乌尔-加布里埃尔 • 乌尔玛 [意] 马里奥 • 富斯科 [英] 艾伦 • 米克罗夫特

译者:陆明刚 劳佳

ISBN:978-7-115-52148-4

本书由北京图灵文化发展有限公司发行数字版。版权所有,侵权必究。



* * *



您购买的图灵电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。



* * *



091507240605ToBeReplacedWithUserId





版权声明

对本书上一版的赞誉

前言

致谢

乌尔玛的致谢词

富斯科的致谢词

米克罗夫特的致谢词

关于本书

本书结构

关于代码

本书论坛

电子书

关于封面图片

第一部分 基础知识

第 1 章 Java 8、9、10以及11的变化

1.1 为什么要关心Java的变化

1.2 Java怎么还在变

1.2.1 Java在编程语言生态系统中的位置

1.2.2 流处理

1.2.3 用行为参数化把代码传递给方法

1.2.4 并行与共享的可变数据

1.2.5 Java需要演变

1.3 Java中的函数

1.3.1 方法和Lambda作为一等值

1.3.2 传递代码:一个例子

1.3.3 从传递方法到Lambda

1.4 流

多线程并非易事

1.5 默认方法及Java模块

1.6 来自函数式编程的其他好思想

1.7 小结

第 2 章 通过行为参数化传递代码

2.1 应对不断变化的需求

2.1.1 初试牛刀:筛选绿苹果

2.1.2 再展身手:把颜色作为参数

2.1.3 第三次尝试:对你能想到的每个属性做筛选

2.2 行为参数化

第四次尝试:根据抽象条件筛选

2.3 对付啰唆

2.3.1 匿名类

2.3.2 第五次尝试:使用匿名类

2.3.3 第六次尝试:使用Lambda表达式

2.3.4 第七次尝试:将List类型抽象化

2.4 真实的例子

2.4.1 用Comparator来排序

2.4.2 用Runnable执行代码块

2.4.3 通过Callable返回结果

2.4.4 GUI事件处理

2.5 小结

第 3 章 Lambda表达式

3.1 Lambda管中窥豹

3.2 在哪里以及如何使用Lambda

3.2.1 函数式接口

3.2.2 函数描述符

3.3 把Lambda付诸实践:环绕执行模式

3.3.1 第1步:记得行为参数化

3.3.2 第2步:使用函数式接口来传递行为

3.3.3 第3步:执行一个行为

3.3.4 第4步:传递Lambda

3.4 使用函数式接口

3.4.1 Predicate

3.4.2 Consumer

3.4.3 Function

3.5 类型检查、类型推断以及限制

3.5.1 类型检查

3.5.2 同样的Lambda,不同的函数式接口

3.5.3 类型推断

3.5.4 使用局部变量

3.6 方法引用

3.6.1 管中窥豹

3.6.2 构造函数引用

3.7 Lambda和方法引用实战

3.7.1 第1步:传递代码

3.7.2 第2步:使用匿名类

3.7.3 第3步:使用Lambda表达式

3.7.4 第4步:使用方法引用

3.8 复合Lambda表达式的有用方法

3.8.1 比较器复合

3.8.2 谓词复合

3.8.3 函数复合

3.9 数学中的类似思想

3.9.1 积分

3.9.2 与Java 8的Lambda联系起来

3.10 小结

第二部分 使用流进行函数式数据处理

第 4 章 引入流

4.1 流是什么

4.2 流简介

4.3 流与集合

4.3.1 只能遍历一次

4.3.2 外部迭代与内部迭代

4.4 流操作

4.4.1 中间操作

4.4.2 终端操作

4.4.3 使用流

4.5 路线图

4.6 小结

第 5 章 使用流

5.1 筛选

5.1.1 用谓词筛选

5.1.2 筛选各异的元素

5.2 流的切片

5.2.1 使用谓词对流进行切片

5.2.2 截短流

5.2.3 跳过元素

5.3 映射

5.3.1 对流中每一个元素应用函数

5.3.2 流的扁平化

5.4 查找和匹配

5.4.1 检; 查谓词是否至少匹配一个元素

5.4.2 检查谓词是否匹配所有元素

5.4.3 查找元素

5.4.4 查找第一个元素

5.5 归约

5.5.1 元素求和

5.5.2 最大值和最小值

5.6 付诸实践

5.6.1 领域:交易员和交易

5.6.2 解答

5.7 数值流

5.7.1 原始类型流特化

5.7.2 数值范围

5.7.3 数值流应用:勾股数

5.8 构建流

5.8.1 由值创建流

5.8.2 由可空对象创建流

5.8.3 由数组创建流

5.8.4 由文件生成流

5.8.5 由函数生成流:创建无限流

5.9 概述

5.10 小结

第 6 章 用流收集数据

6.1 收集器简介

6.1.1 收集器用作高级归约

6.1.2 预定义收集器

6.2 归约和汇总

6.2.1 查找流中的最大值和最小值

6.2.2 汇总

6.2.3 连接字符串

6.2.4 广义的归约汇总

6.3 分组

6.3.1 操作分组的元素

6.3.2 多级分组

6.3.3 按子组收集数据

6.4 分区

6.4.1 分区的优势

6.4.2 将数字按质数和非质数分区

6.5 收集器接口

6.5.1 理解Collector接口声明的方法

6.5.2 全部融合到一起

6.6 开发你自己的收集器以获得更好的性能

6.6.1 仅用质数做除数

6.6.2 比较收集器的性能

6.7 小结

第 7 章 并行数据处理与性能

7.1 并行流

7.1.1 将顺序流转换为并行流

7.1.2 测量流性能

7.1.3 正确使用并行流

7.1.4 高效使用并行流

7.2 分支/合并框架

7.2.1 使用RecursiveTask

7.2.2 使用分支/合并框架的最佳做法

7.2.3 工作窃取

7.3 Spliterator

7.3.1 拆分过程

7.3.2 实现你自己的Spliterator

7.4 小结

第三部分 使用流和Lambda进行高效编程

第 8 章 Collection API的增强功能

8.1 集合工厂

8.1.1 List工厂

8.1.2 Set工厂

8.1.3 Map工厂

8.2 使用List和Set

8.2.1 removeIf方法

8.2.2 replaceAll方法

8.3 使用Map

8.3.1 forEach方法

8.3.2 排序

8.3.3 getOrDefault方法

8.3.4 计算模式

8.3.5 删除模式

8.3.6 替换模式

8.3.7 merge方法

8.4 改进的ConcurrentHashMap

8.4.1 归约和搜索

8.4.2 计数

8.4.3 Set视图

8.5 小结

第 9 章 重构、测试和调试

9.1 为改善可读性和灵活性重构代码

9.1.1 改善代码的可读性

9.1.2 从匿名类到Lambda表达式的转换

9.1.3 从Lambda表达式到方法引用的转换

9.1.4 从命令式的数据处理切换到Stream

9.1.5 增加代码的灵活性

9.2 使用Lambda重构面向对象的设计模式

9.2.1 策略模式

9.2.2 模板方法

9.2.3 观察者模式

9.2.4 责任链模式

9.2.5 工厂模式

9.3 测试Lambda表达式

9.3.1 测试可见Lambda函数的行为

9.3.2 测试使用Lambda的方法的行为

9.3.3 将复杂的Lambda表达式分为不同的方法

9.3.4 高阶函数的测试

9.4 调试

9.4.1 查看栈跟踪

9.4.2 使用日志调试

9.5 小结

第 10 章 基于Lambda的领域特定语言

10.1 领域特定语言

10.1.1 DSL的优点和弊端

10.1.2 JVM中已提供的DSL解决方案

10.2 现代Java API中的小型DSL

10.2.1 把Stream API当成DSL去操作集合

10.2.2 将Collectors作为DSL汇总数据

10.3 使用Java创建DSL的模式与技巧

10.3.1 方法链接

10.3.2 使用嵌套函数

10.3.3 使用Lambda表达式的函数序列

10.3.4 把它们都放到一起

10.3.5 在DSL中使用方法引用

10.4 Java 8 DSL的实际应用

10.4.1 jOOQ

10.4.2 Cucumber

10.4.3 Spring Integration

10.5 小结

第四部分 无所不在的Java

第 11 章 用Optional取代null

11.1 如何为缺失的值建模

11.1.1 采用防御式检查减少NullPointerException

11.1.2 null带来的种种问题

11.1.3 其他语言中null的替代品

11.2 Optional类入门

11.3 应用Optional的几种模式

11.3.1 创建Optional对象

11.3.2 使用map从Optional对象中提取和转换值

11.3.3 使用flatMap链接Optional对象

11.3.4 操纵由Optional对象构成的Stream

11.3.5 默认行为及解引用Optional对象

11.3.6 两个Optional对象的组合

11.3.7 使用filter剔除特定的值

11.4 使用Optional的实战示例

11.4.1 用Optional封装可能为null的值

11.4.2 异常与Optional的对比

11.4.3 基础类型的Optional对象,以及为什么应该避免使用它们

11.4.4 把所有内容整合起来

11.5 小结

第 12 章 新的日期和时间API

12.1 LocalDate、LocalTime、LocalDateTime、Instant、Duration以及Period

12.1.1 使用LocalDate和LocalTime

12.1.2 合并日期和时间

12.1.3 机器的日期和时间格式

12.1.4 定义Duration或Period

12.2 操纵、解析和格式化日期

12.2.1 使用TemporalAdjuster

12.2.2 打印输出及解析日期–时间对象

12.3 处理不同的时区和历法

12.3.1 使用时区

12.3.2 利用和UTC/格林尼治时间的固定偏差计算时区

12.3.3 使用别的日历系统

12.4 小结

第 13 章 默认方法

13.1 不断演进的API

13.1.1 初始版本的API

13.1.2 第二版API

13.2 概述默认方法

13.3 默认方法的使用模式

13.3.1 可选方法

13.3.2 行为的多继承

13.4 解决冲突的规则

13.4.1 解决问题的三条规则

13.4.2 选择提供了最具体实现的默认方法的接口

13.4.3 冲突及如何显式地消除歧义

13.4.4 菱形继承问题

13.5 小结

第 14 章 Java模块系统

14.1 模块化的驱动力:软件的推理

14.1.1 关注点分离

14.1.2 信息隐藏

14.1.3 Java软件

14.2 为什么要设计Java模块系统

14.2.1 模块化的局限性

14.2.2 单体型的JDK

14.2.3 与OSGi的比较

14.3 Java模块:全局视图

14.4 使用Java模块系统开发应用

14.4.1 从头开始搭建一个应用

14.4.2 细粒度和粗粒度的模块化

14.4.3 Java模块系统基础

14.5 使用多个模块

14.5.1 exports子句

14.5.2 requires子句

14.5.3 命名

14.6 编译及打包

14.7 自动模块

14.8 模块声明及子句

14.8.1 requires

14.8.2 exports

14.8.3 requires的传递

14.8.4 exports to

14.8.5 open和opens

14.8.6 uses和provides

14.9 通过一个更复杂的例子了解更多

14.10 小结

第五部分 提升Java的并发性

第 15 章 CompletableFuture及反应式编程背后的概念

15.1 为支持并发而不断演进的Java

15.1.1 线程以及更高层的抽象

15.1.2 执行器和线程池

15.1.3 其他的线程抽象:非嵌套方法调用

15.1.4 你希望线程为你带来什么

15.2 同步及异步API

15.2.1 Future风格的API

15.2.2 反应式风格的API

15.2.3 有害的睡眠及其他阻塞式操作

15.2.4 实战验证

15.2.5 如何使用异步API进行异常处理

15.3 “线框–管道”模型

15.4 为并发而生的CompletableFuture和结合器

15.5 “发布–订阅”以及反应式编程

15.5.1 示例:对两个流求和

15.5.2 背压

15.5.3 一种简单的真实背压

15.6 反应式系统和反应式编程

15.7 路线图

15.8 小结

第 16 章 CompletableFuture:组合式异步编程

16.1 Future接口

16.1.1 Future接口的局限性

16.1.2 使用CompletableFuture构建异步应用

16.2 实现异步API

16.2.1 将同步方法转换为异步方法

16.2.2 错误处理

16.3 让你的代码免受阻塞之苦

16.3.1 使用并行流对请求进行并行操作

16.3.2 使用CompletableFuture发起异步请求

16.3.3 寻找更好的方案

16.3.4 使用定制的执行器

16.4 对多个异步任务进行流水线操作

16.4.1 实现折扣服务

16.4.2 使用Discount服务

16.4.3 构造同步和异步操作

16.4.4 将两个CompletableFuture对象整合起来,无论它们是否存在依赖

16.4.5 对Future和CompletableFuture的回顾

16.4.6 高效地使用超时机制

16.5 响应CompletableFuture的completion事件

16.5.1 对最佳价格查询器应用的优化

16.5.2 付诸实践

16.6 路线图

16.7 小结

第 17 章 反应式编程

17.1 反应式宣言

17.1.1 应用层的反应式编程

17.1.2 反应式系统

17.2 反应式流以及Flow API

17.2.1 Flow类

17.2.2 创建你的第一个反应式应用

17.2.3 使用Processor转换数据

17.2.4 为什么Java并未提供Flow API的实现

17.3 使用反应式库RxJava

17.3.1 创建和使用Observable

17.3.2 转换及整合多个Observable

17.4 小结

第六部分 函数式编程以及Java未来的演进

第 18 章 函数式的思考

18.1 实现和维护系统

18.1.1 共享的可变数据

18.1.2 声明式编程

18.1.3 为什么要采用函数式编程

18.2 什么是函数式编程

18.2.1 函数式Java编程

18.2.2 引用透明性

18.2.3 面向对象的编程和函数式编程的对比

18.2.4 函数式编程实战

18.3 递归和迭代

18.4 小结

第 19 章 函数式编程的技巧

19.1 无处不在的函数

19.1.1 高阶函数

19.1.2 柯里化

19.2 持久化数据结构

19.2.1 破坏式更新和函数式更新的比较

19.2.2 另一个使用Tree的例子

19.2.3 采用函数式的方法

19.3 Stream的延迟计算

19.3.1 自定义的Stream

19.3.2 创建你自己的延迟列表

19.4 模式匹配

19.4.1 访问者模式

19.4.2 用模式匹配力挽狂澜

19.5 杂项

19.5.1 缓存或记忆表

19.5.2 “返回同样的对象”意味着什么

19.5.3 结合器

19.6 小结

第 20 章 面向对象和函数式编程的混合:Java和Scala的比较

20.1 Scala简介

20.1.1 你好,啤酒

20.1.2 基础数据结构:List、Set、Map、Tuple、Stream以及Option

20.2 函数

20.2.1 Scala中的一等函数

20.2.2 匿名函数和闭包

20.2.3 柯里化

20.3 类和trait

20.3.1 更加简洁的Scala类

20.3.2 Scala的trait与Java 8的接口对比

20.4 小结

第 21 章 结论以及Java的未来

21.1 回顾Java 8的语言特性

21.1.1 行为参数化(Lambda以及方法引用)

21.1.2 流

21.1.3 CompletableFuture

21.1.4 Optional

21.1.5 Flow API

21.1.6 默认方法

21.2 Java 9的模块系统

21.3 Java 10的局部变量类型推断

21.4 Java的未来

21.4.1 声明处型变

21.4.2 模式匹配

21.4.3 更加丰富的泛型形式

21.4.4 对不变性的更深层支持

21.4.5 值类型

21.5 让Java发展得更快

21.6 写在最后的话

附录 A 其他语言特性的更新

A.1 注解

A.1.1 重复注解

A.1.2 类型注解

A.2 通用目标类型推断

附录 B 其他类库的更新

B.1 集合

B.1.1 其他新增的方法

B.1.2 Collections类

B.1.3 Comparator

B.2 并发

B.2.1 原子操作

B.2.2 ConcurrentHashMap

B.3 Arrays

B.3.1 使用parallelSort

B.3.2 使用setAll和parallelSetAll

B.3.3 使用parallelPrefix

B.4 Number和Math

B.4.1 Number

B.4.2 Math

B.5 Files

B.6 Reflection

B.7 String

附录 C 如何以并发方式在同一个流上执行多种操作

C.1 复制流

C.1.1 使用ForkingStreamConsumer实现Results接口

C.1.2 开发ForkingStreamConsumer和BlockingQueueSpliterator

C.1.3 将StreamForker运用于实战

C.2 性能的考量

附录 D Lambda表达式和JVM字节码

D.1 匿名类

D.2 生成字节码

D.3 用InvokeDynamic力挽狂澜

D.4 代码生成策略





版权声明


Original English language edition, entitled Modern Java in Action, 2nd Edition by Raoul-Gabriel Urma, Mario Fusco, Alan Mycroft, published by Manning Publications. 178 South Hill Drive, Westampton, NJ 08060 USA. Copyright © 2019 by Manning Publications.

Simplified Chinese-language edition copyright © 2019 by Posts & Telecom Press. All rights reserved.



本书中文简体字版由Manning Publications授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

版权所有,侵权必究。





对本书上一版的赞誉


“这是一本介绍Java 8新特性的简明指南,书中提供了大量的示例,可以帮助读者快速掌握Java 8。”

——Jason Lee,Oracle公司



“这本书是最优秀的Java 8指南!”

——William Wheeler,ProData计算机系统公司



“书中新的Stream API和Lambda示例特别有用。”

——Steve Rogers,CGTek公司



“这是学习Java 8函数式编程的必备材料。”

——Mayur S. Patil,麻省理工学院工程学院



“这本书以实战为宗旨,简明扼要地介绍了Java 8激动人心的新特性,对掌握Java 8的新功能非常有帮助。我尤其钟爱函数式接口和spliterator的相关内容。”

——Will Hayworth,开发者,Atlassian公司





前言


1998年,八岁的我拿起了我此生第一本计算机书,那本书讲的是JavaScript和HTML。我当时怎么也想不到,打开那本书会让我见识到编程语言和它们能够创造的神奇世界,并彻底改变我的生活。我被它深深地吸引了。如今,编程语言的某个新特性还会时不时地让我感到兴奋,因为它让我花更少的时间就能够写出更清晰、更简洁的代码。我希望本书探讨的Java 8、9以及10中那些来自函数式编程的新思想,同样能够给你启迪。

那么,你可能会问,这本书以及它的上一版是由何而来的呢?

2011年,Oracle公司的Java语言架构师Brian Goetz分享了一些在Java中添加Lambda表达式的提议,以期获得业界的参与。这重新燃起了我的兴趣,于是我开始传播这些想法,在各种开发者会议上组织Java 8讨论班,并为剑桥大学的学生开设讲座。

到了2013年4月,消息不胫而走,Manning出版社的编辑给我发了封邮件,问我是否有兴趣写一本关于Java 8中Lambda的书。当时我只是个“不起眼”的二年级博士研究生,写书似乎并不是一个好主意,因为它会耽误我提交论文。另一方面,所谓“只争朝夕”,我想写一本小书不会有太多工作量,对吧?(后来我才意识到自己大错特错了!)于是我咨询我的博士生导师米克罗夫特教授,结果他十分支持我写书(甚至愿意为这种与博士学位无关的工作提供帮助,我永远感谢他)。几天后,我们见到了Java 8的布道者富斯科,他有着非常丰富的专业经验,并且因在重大开发者会议上所做的函数式编程演讲而享有盛名。

我们很快就认识到,如果将大家的能量和背景融合起来,就不仅仅可以写出一本关于Java 8 Lambda的小书,而是可以写出(我们希望)一本五年或十年后,在Java领域仍然有人愿意阅读的书。我们有了一个非常难得的机会来深入讨论许多话题,它们不但有益于Java程序员,还打开了通往一扇通往新世界的大门:函数式编程。

现在是2018年,截至今天,本书的上一版已在全世界售出两万本,Java 9已经发布,Java 10也即将发布。经历了无数个漫漫长夜的辛苦工作、无数次的编辑和永生难忘的体验后,我们这本全新修订的包含Java 8、9以及10的《Java实战(第2版)》终于送到了你的手上。希望你会喜欢它!



拉乌尔-加布里埃尔·乌尔玛

于剑桥大学





致谢


如果没有许多杰出人士的支持,这本书是不可能完成的。

自愿提供宝贵审稿建议的朋友:Richard Walker、Jan Saganowski、Brian Goetz、Stuart Marks、Cem Redif、Paul Sandoz、Stephen Colebourne、Íñigo Mediavilla、Allahbaksh Asadullah、Tomasz Nurkiewicz和Michael Müller。

在MEAP(Manning Early Access Program)的作者在线论坛上发表评论的读者。

在编撰过程中提供有益反馈的审阅者:Antonio Magnaghi、Brent Stains、Franziska Meyer、Furkan Kamachi、Jason Lee、Jörn Dinkla、Lochana Menikarachchi、Mayur Patil、Nikolaos Kaintantzis、Simone Bordet、Steve Rogers、Will Hayworth和William Wheeler。

Manning出版社编辑Kevin Harreld耐心地回答了我们所有的问题和疑虑,为每一章的初稿提供了详尽的反馈,并尽其所能地支持我们。

本书付印前,Dennis Selinger和Jean-François Morin进行了全面的技术审阅,Al Scherer则在编撰过程中提供了技术帮助。





乌尔玛的致谢词


首先,我要感谢我的父母在生活中给予我无尽的爱和支持。我写一本书的小小梦想如今成真了!其次,我要向信任并且支持我的博士生导师和合著者米克罗夫特表达无尽的感激。我也要感谢合著者富斯科陪我走过这段有趣的旅程。最后,我要感谢在生活中为我提供指导、有用建议,给予我鼓励的朋友们:Sophia Drossopoulou、Aidan Roche、Alex Buckley、Haadi Jabado和Jaspar Robertson。你们真是太棒啦!





富斯科的致谢词


我要特别感谢我的妻子Marilena,她无尽的耐心让我可以专注于写作本书;还有我们的女儿Sofia,因为她能够创造出无尽的混乱,让我可以从本书的写作中暂时抽身。你在阅读本书时将发现,Sofia还用只有小孩子才会的方式,告诉我们内部迭代和外部迭代之间的差异。我还要感谢乌尔玛和米克罗夫特,他们与我一起分享了写作本书的(巨大)喜悦和(小小)痛苦。





米克罗夫特的致谢词


我要感谢我的太太Hilary和其他家庭成员在本书写作期间容忍我,我常常说“再稍微弄弄就好了”,结果一弄就是好几个小时。我还要感谢多年来的同事和学生,他们让我知道了怎么去教授知识。最后,感谢富斯科和乌尔玛这两位非常高效的合著者,特别是乌尔玛在苛求“周五再交出一部分稿件”时,还能让人愉快地接受。





关于本书


简单地说,Java 8中的新增功能以及Java 9引入的变化(虽然并不显著)是自Java 1.0发布21年以来,Java发生的最大变化。这一演进没有去掉任何东西,因此你原有的Java代码都能工作,但新功能提供了更强大的新习语和新设计模式,能帮助你编写更清晰、更简洁的代码。就像遇到所有新功能时那样,你一开始可能会想:“为什么又要去改我的语言呢?”但稍加练习之后,你就会发觉自己只用预期的一半时间,就用新功能写出了更短、更清晰的代码,这时你会意识到自己永远无法返回到“旧Java”了。

本书会帮助你跨过“原理听起来不错,但还是有点儿新,不太适应”的门槛,从而熟练地编程。

“也许吧,”你可能会想,“可是Lambda、函数式编程,这些不是那些留着胡子、穿着凉鞋的学究们在象牙塔里面琢磨的东西吗?”或许是的,但Java 8中加入的新想法的分量刚刚好,它们带来的好处也可以被普通的Java程序员所理解。本书会从普通程序员的角度来叙述,偶尔谈谈“这是怎么来的”。

“Lambda,听起来跟天书一样!”是的,也许是这样,但它是一个很好的想法,让你可以编写简明的Java程序。许多人都熟悉事件处理器和回调函数,即注册一个对象,它包含会在事件发生时使用的一个方法。Lambda使人更容易在Java中广泛应用这种思想。简单来说,Lambda和它的朋友“方法引用”让你在做其他事情的过程中,可以简明地将代码或方法作为参数传递进去执行。在本书中,你会看到这种思想出现得比预想的还要频繁:从加入做比较的代码来简单地参数化一个排序方法,到利用新的Stream API在一组数据上表达复杂的查询指令。

“流(stream)是什么?”这是Java 8的一个新功能。它的特点和集合(collection)差不多,但有几个明显的优点,让我们可以使用新的编程风格。首先,如果你使用过SQL等数据库查询语言,就会发现用几行代码写出的查询语句要是换成Java要写好长。Java 8的流支持这种简明的数据库查询式编程——但用的是Java语法,而无须了解数据库!其次,流被设计成无须同时将所有的数据调入内存(甚至根本无须计算),这样就可以处理无法装入计算机内存的流数据了。但Java 8可以对流做一些集合所不能的优化操作,例如,它可以将对同一个流的若干操作组合起来,从而只遍历一次数据,而不是花很大成本去多次遍历它。更妙的是,Java可以自动将流操作并行化(集合可不行)。

“还有函数式编程,这又是什么?”就像面向对象编程一样,它是另一种编程风格,其核心是把函数作为值,前面在讨论Lambda的时候提到过。

Java 8的好处在于,它把函数式编程中一些最好的想法融入到了大家熟悉的Java语法中。有了这个优秀的设计选择,你可以把函数式编程看作Java 8中一个额外的设计模式和习语,让你可以用更少的时间,编写更清晰、更简洁的代码。想想你的编程兵器库中的利器又多了一样。

当然,除了这些在概念上对Java有很大扩充的功能,我们也会解释很多其他有用的Java 8功能和更新,如默认方法、新的Optional类、CompletableFuture,以及新的日期和时间API。

Java 9的更新包括一个支持通过Flow API进行反应式编程的模块系统,以及其他各种增强功能。

别急,这只是一个概览,现在该让你自己去看看本书了。





本书结构


本书分为六个部分,分别是:“基础知识”“使用流进行函数式数据处理”“使用流和Lambda进行高效编程”“无所不在的Java”“提升Java的并发性”和“函数式编程以及Java未来的演进”。我们强烈建议你按顺序阅读前两部分的内容,因为很多概念都需要前面的章节作为基础,后面四个部分的内容你可以按照任意顺序阅读。大多数章节都附有几个测验,可以帮助你学习和掌握这些内容。

第一部分旨在帮助你初步使用Java 8。学完这一部分,你将会对Lambda表达式有充分的了解,并可以编写简洁而灵活的代码,能够轻松适应不断变化的需求。

第1章总结Java的主要变化(Lambda表达式、方法引用、流和默认方法),为学习后面的内容做准备。

第2章介绍行为参数化,这是Java 8非常依赖的一种软件开发模式,也是引入Lambda表达式的主要原因。

第3章对Lambda表达式和方法引用进行全面介绍,每一步都提供了代码示例和测验。



第二部分详细讨论新的Stream API。通过Stream API,你将能够写出功能强大的代码,以声明性方式处理数据。学完这一部分,你将充分理解流是什么,以及如何在Java应用程序中使用它们来简洁而高效地处理数据集。

第4章介绍流的概念,并解释它们与集合有何异同。

第5章详细讨论为了表达复杂的数据处理查询可以使用的流操作。其间会谈到很多模式,如筛选、切片、查找、匹配、映射和归约。

第6章介绍收集器——Stream API的一个功能,可以让你表达更为复杂的数据处理查询。

第7章探讨流如何得以自动并行执行,并利用多核架构的优势。此外,你还会学到为正确而高效地使用并行流,要避免的若干陷阱。



第三部分探索Java 8和Java 9的多个主题,这些主题中的技巧能让你的Java代码更高效,并能帮助你利用现代的编程习语改进代码库。这一部分的出发点是介绍高级编程思想,本书后续内容并不依赖于此。

第8章是这一版新增的,探讨Java 8和Java 9对Collection API的增强。内容涵盖如何使用集合工厂,如何使用新的惯用模式处理List和Set,以及使用Map的惯用模式。

第9章探讨如何利用Java 8的新功能和一些秘诀来改善你现有的代码。此外,该章还探讨了一些重要的软件开发技术,如设计模式、重构、测试和调试。

第10章也是这一版新增的,介绍依据领域特定语言(domain-specific language,DSL)实现API的思想。这不仅是一种强大的API设计方法,而且正变得越来越流行。Java中已经有API采用这种模式实现,譬如Comparator、Stream以及Collector接口。



第四部分介绍Java 8和Java 9中新增的多个特性,这些特性能帮助程序员事半功倍地编写代码,让程序更加稳定可靠。我们首先从Java 8新增的两个API入手。

第11章介绍java.util.Optional类,它能让你设计出更好的API,并减少空指针异常。

第12章探讨新的日期和时间API,这相对于以前涉及日期和时间时容易出错的API是一大改进。

第13章讨论默认方法是什么,如何利用它们来以兼容的方式演变API,一些实际的应用模式,以及有效使用默认方法的规则。

第14章是这一版新增的,探讨Java的模块系统——它是Java 9的主要改进,使大型系统能够以文档化和可执行的方式进行模块化,而不是简单地将一堆包杂乱无章地堆在一起。



第五部分探讨如何使用Java的高级特性构建并发程序——注意,我们要讨论的不是第6章和第7章中介绍的流的并发处理。

第15章是这一版新增的,从宏观的角度介绍异步API的思想,包括Future、反应式编程背后的“发布–订阅”协议(封装在Java 9的Flow API中)。

第16章探讨CompletableFuture,它可以让你用声明性方式表达复杂的异步计算,从而让Stream API的设计并行化。

第17章也是这一版新增的,详细介绍Java 9的Flow API,并提供反应式编程的实战代码解析。



第六部分是本书最后一部分,我们会谈谈怎么用Java编写高效的函数式程序,还会将Java的功能和Scala做比较。

第18章是一个完整的函数式编程教程,会介绍一些术语,并解释如何在Java 8中编写函数式风格的程序。

第19章涵盖更高级的函数式编程技巧,包括高阶函数、柯里化、持久化数据结构、延迟列表和模式匹配。这一章既提供了可以用在代码库中的实际技术,也提供了能让你成为更渊博的程序员的学术知识。

第20章将对比Java与Scala的功能。Scala和Java一样,是一种在JVM上实现的语言,近年来发展迅速,在编程语言生态系统中已经威胁到了Java的一些方面。

第21章会回顾这段学习Java 8并慢慢走向函数式编程的历程。此外,我们还会猜测,在Java 8、9以及10中添加的小功能之后,未来可能会有哪些增强和新功能出现。



最后,本书有四个附录,涵盖了与Java 8相关的其他一些话题。附录A总结了本书未讨论的一些Java 8的小特性。附录B概述了Java库的其他主要扩展,可能对你有用。附录C是第二部分的延续,介绍了流的高级用法。附录D探讨了Java编译器在幕后是如何实现Lambda表达式的。





关于代码


所有代码清单和正文中的源代码都采用等宽字体(如fixed-widthfontlikethis),以与普通文字区分开来。许多代码清单中都有注释,突出了重要的概念。

书中示例的源代码请至图灵社区本书主页http://ituring.cn/book/2659“随书下载”处下载。





本书论坛


购买了英文版的读者可免费访问Manning出版社运营的一个私有在线论坛,你可以在那里发表对图书的评论、询问技术问题,并获得作者和其他用户的帮助,网址为:https://forums.manning.com/forums/modern-java-in-action。如欲了解Manning论坛以及论坛上的行为守则,请访问https://forums.manning.com/forums/about。

Manning对读者的承诺是提供一个平台,供读者之间以及读者和作者之间进行有意义的对话。但这并不意味着作者会有任何特定程度的参与。他们对论坛的贡献是完全自愿的(且无报酬)。我们建议你试着询问作者一些有挑战性的问题,以免他们失去兴趣。只要书仍在发行,你就可以在出版商网站上访问作者在线论坛和先前所讨论内容的归档文件。

读者也可登录图灵社区本书主页http://ituring.cn/book/2659提交反馈意见和勘误。





电子书


扫描如下二维码,即可购买本书电子版。





关于封面图片


本书封面上的图像标题为“1700年中国清朝满族战士的服饰”。图片中的人物衣饰华丽,身佩利剑,背背弓和箭筒。如果你仔细看他的腰带,会发现一个λ形的带扣(这是我们的设计师加上去的,暗示本书的一个主题)。该图选自Thomas Jefferys的《各国古代和现代服饰集》 (A Collection of the Dresses of Different Nations, Ancient and Modern,伦敦,1757年至1772年间出版),该书标题页中说这些图是手工上色的铜版雕刻品,并且是用阿拉伯树胶填充的。Thomas Jefferys(1719–1771)被称为“乔治三世的地理学家”。他是一名英国制图员,是当时主要的地图供应商。他为政府和其他官方机构雕刻和印制地图,制作了很多商业地图和地理地图集,尤以北美地区为多。地图制作商的工作让他对勘察和绘图过的地方的服饰产生了兴趣,这些都在这个四卷本中得到了出色的展现。

向往遥远的土地、渴望旅行,在18世纪还是相对新鲜的现象,而类似于这本集子的书则十分流行,这些集子向旅游者和坐着扶手椅梦想去旅游的人介绍了其他国家的人。Jefferys书中异彩纷呈的图画生动地描绘了几百年前世界各国的独特与个性。如今,着装规则已经改变,各个国家和地区一度非常丰富的多样性也已消失,来自不同大陆的人仅靠衣着已经很难区分开了。不过,要是乐观点儿看,我们这是用文化和视觉上的多样性,换得了更多姿多彩的个人生活——或是更为多样化、更为有趣的知识和技术生活。

如今计算机图书的封面设计风格类似,Manning出版社独树一帜,用Jefferys画中复活的三个世纪前风格各异的国家服饰,来象征计算机行业中的发明与创造的异彩纷呈。





第一部分 基础知识


第一部分旨在帮助你初步使用Java 8。学完这一部分,你将对Lambda表达式有充分的了解,并可以编写简洁而灵活的代码,能够轻松适应不断变化的需求。

第1章总结Java的主要变化(Lambda表达式、方法引用、流和默认方法),为学习后面的内容做准备。

第2章介绍行为参数化,这是Java 8非常依赖的一种软件开发模式,也是引入Lambda表达式的主要原因。

第3章对Lambda表达式和方法引用进行全面的介绍,每一步都提供了代码示例和测验。





第 1 章 Java 8、9、10以及11的变化


本章内容

Java怎么又变了

日新月异的计算应用背景

Java改进的压力

Java 8和Java 9的核心新特性





自1996年JDK 1.0(Java 1.0)发布以来,Java已经受到了学生、项目经理和程序员等一大批活跃用户的欢迎。这一语言极具活力,不断被用在大大小小的项目里。从Java 1.1(1997年)到Java 7(2011年),Java通过不断地增加新功能,得到了良好的升级。Java 8于2014年3月发布,Java 9于2017年9月发布,Java 10于2018年3月发布,Java 11于2018年9月发布1。那么,问题来了:为什么要关心这些变化?

1如想了解Oracle公司对JDK的最新支持情况,请访问https://www.oracle.com/technetwork/java/java-se-support-roadmap.html。——译者注





1.1 为什么要关心Java的变化


我们的理由是,从很多方面来说,Java 8所做的改变,其影响比Java历史上任何一次改变都深远(Java 9新增了效率提升方面的重要改进,但并不伤筋动骨,这些内容本章后面会介绍。 Java 10对类型推断做了微调)。好消息是,这些改变会让编程更容易,我们再也不用编写下面这种啰唆的程序了(按照重量给inventory中的苹果排序):

Collections.sort(inventory, new Comparator<Apple>() { public int compare(Apple a1, Apple a2){ return a1.getWeight().compareTo(a2.getWeight()); } });

使用Java 8,你能书写更简洁的代码,让代码读起来更接近问题描述本身:

inventory.sort(comparing(Apple::getWeight)); ←---- 本书第一段Java 8代码

这段代码的意思是“按照重量给库存苹果排序”。目前你不用担心不理解这段代码,本书后续的章节将会介绍它做了什么,以及如何写出这样的代码。

Java 8的改变也受到了硬件的影响:平常我们用的CPU都是多核的——你的笔记本电脑或台式机的处理器可能有四个甚至更多的CPU核。然而,绝大多数现存的Java程序都只使用了其中一个核,其他三个核都闲着,或者仅消耗了它的一小部分处理能力来运行操作系统或杀毒程序。

Java 8之前,专家们可能会跟你说,只有通过多线程才能利用多个处理器核。问题是,多线程用起来不仅难,还容易出错。从Java的演进路径来看,它一直致力于让并发编程更容易、出错更少。早在1.0版本Java就引入了线程和锁,甚至还有一个内存模型——这是当时的最佳做法,然而事实证明,除非你的项目团队是由专家组成的,否则很难可靠地利用这些基本模型。Java 5添加了工业级的构建模块,如线程池和并发集合。Java 7添加了分支/合并(fork/join)框架,让并行变得更实用,然而这依旧很困难。Java 8提供了一种全新的思想,可以帮助你更容易地实现并行。然而,你仍然需要遵循一些规则,这些内容本书都会逐一介绍。

本书还会介绍Java 9新增的反应式编程支持,它是一种实现并发的结构化方法。虽然实现反应式编程有多种专有的方式,但是RxJava和Akka反应式流工具集正日益流行,已成为构建高并发系统的标准方式。

基于前文介绍的两个迫切需求(即编写更简洁的代码,以及更方便地利用处理器的多核)催生出了一座拔地而起相互勾连一致的Java 8大厦。先快速了解一下这些想法(希望能引起你的兴趣,也希望这些总结足够简洁):

Stream API;

向方法传递代码的技巧;

接口的默认方法。



Java 8提供了一个新的API(称为“流”,Stream),它支持多个数据处理的并行操作,其思路和数据库查询语言类似——从高层的角度描述需求,而由“实现”(这里是Stream库)来选择底层最佳执行机制。这样就可以避免用synchronized编写代码,这种代码不仅容易出错,而且在多核CPU2上执行所需的成本也比你想象的要高。

2多核CPU的每个处理器核都有独立的高速缓存。加锁需要这些高速缓存同步运行,然而这又需要在内核间进行较慢的缓存一致性协议通信。

从修正的角度来看,在Java 8中加入Stream可以视为添加另外两项的直接原因:向方法传递代码的简洁技巧(方法引用、Lambda)和接口中的默认方法。

如果仅仅把“向方法传递代码”看成引入Stream的结果,就低估了它在Java 8中的应用范围。它提供了一种新的方式,能够简洁地表达行为参数化。比方说,你想要写两个只有几行代码不同的方法,现在只需把不同的那部分代码作为参数传递进去就可以了。采用这种编程技巧,代码更短、更清晰,也比常用的复制粘贴更少出错。高手看到这里就会想,Java 8之前可以用匿名类实现行为参数化呀——但是想想本章开头那个更加简洁的Java 8代码示例,代码本身就说明了它有多清晰!

Java 8里将代码传递给方法的功能(同时也能够返回代码并将其包含在数据结构中)还让我们能够使用一整套新技巧,通常称为函数式编程。一言以蔽之,这种被函数式编程界称为函数的代码,可以被来回传递并加以组合,以产生强大的编程语汇。这样的例子在本书中随处可见。

本章首先从宏观角度探讨语言为什么会演变,然后介绍Java 8的核心特性,接着介绍函数式编程思想——新的特性简化了使用,而且更适应新的计算机体系结构。简而言之,1.2节讨论Java的演变过程和原因,即Java以前缺乏以简易方式利用多核并行的能力。1.3节介绍为什么把代码传递给方法在Java 8里是如此强大的一个新的编程语汇。1.4节对Stream做同样的介绍:Stream是Java 8表示有序数据以及这些数据是否可以并行处理的新方式。1.5节解释如何利用Java 8中的默认方法功能让接口和库的演变更顺畅、编译更少,还会介绍Java 9中新增的模块,有了这一特性,Java系统组件就不会再被称为“只是包的JAR文件”了。最后,1.6节展望在Java和其他共用JVM的语言中进行函数式编程的思想。总的来说,本章会介绍整体脉络,而细节会在本书的其余部分中逐一展开。请尽情享受吧!





1.2 Java怎么还在变


20世纪60年代,人们开始追求完美的编程语言。当时著名的计算机科学家Peter Landin在1966年的一篇标志性论文3中提到那时已经有700种编程语言了,并推测了接下来的700种会是什么样子,文中也对类似于Java 8中的函数式编程进行了讨论。

3P. J. Landin,“The Next 700 Programming Languages,”CACM 9(3):157–65, March 1966。

之后,又出现了数以千计的编程语言。于是学者们得出结论:编程语言就像生态系统一样,新的语言会出现,旧语言则被取代,除非它们不断演变。我们都希望出现一种完美的通用语言,可在现实中,某些语言只是更适合某些方面。比如,C和C++仍然是构建操作系统和各种嵌入式系统的流行工具,因为它们编写出的程序尽管安全性不佳,但运行时占用资源少。缺乏安全性可能会导致程序意外崩溃,并把安全漏洞暴露给病毒等。确实,Java和C#等安全型语言在诸多运行资源不太紧张的应用中已经取代了C和C++。

先抢占市场通常能够吓退竞争对手。为了一个功能而改用新的语言和工具链往往太痛苦了,但新来者最终会取代现有的语言,除非后者演变得够快,能跟上节奏。年纪大一点的读者大多可以列举出一堆这样的语言——他们以前用过,但是现在这些语言已经不流行了。随便列举几个吧:Ada、Algol、COBOL、Pascal、Delphi、SNOBOL等。

你是一位Java程序员。在过去近20年的时间里,Java已经成功地霸占了编程生态系统中的一大块,同时替代了竞争对手语言。下面来看看其中的原因。





1.2.1 Java在编程语言生态系统中的位置


Java天资不错。从一开始,它就是一门精心设计的面向对象的语言,提供了大量有用的库。由于有集成的线程和锁的支持,它从第一天起就支持小规模并发(并且它很有先见之明地承认,在硬件无关的内存模型中,并发线程在多核处理器上发生意外的概率比单核处理器上大得多)。此外,将Java编译成JVM字节码(一种很快就被每一种浏览器支持的虚拟机代码)意味着它成为了互联网applet(小应用)的首选。(你还记得applet吗?)确实,Java虚拟机(JVM)及其字节码可能会变得比Java语言本身更重要,而且对于某些应用来说,Java可能会被同样运行在JVM上的竞争对手语言(如Scala或Groovy)取代。JVM各种最新的更新(例如JDK7中的新invokedynamic字节码)旨在帮助这些竞争对手语言在JVM上顺利运行,并与Java交互操作。Java也已经成功地占领了嵌入式计算的若干领域,从智能卡、烤面包机、机顶盒到汽车制动系统。

Java是如何进入通用编程市场的?

面向对象在20世纪90年代开始流行,原因有两个:封装原则使得其软件工程问题比C少;作为一个思维模型,它轻松地反映了Windows 95及之后的WIMP编程模式。可以这样总结:一切都是对象,单击鼠标就能给处理程序发送一个事件消息(在Mouse对象中触发clicked方法)。Java的“一次编写,随处运行”模式,以及早期浏览器安全地执行Java小应用的能力让它占领了大学市场,毕业生随后又把它带进了业界。开始时由于运行成本比C/C++要高,Java还遇到了一些阻力,但后来机器变得越来越快,程序员的时间也变得越来越重要了。微软的C#进一步验证了Java的面向对象模型。



但是,编程语言生态系统的气候正在变化。程序员越来越多地要处理所谓的大数据(数百万兆甚至更多字节的数据集),并希望利用多核计算机或计算集群来有效地处理。这意味着需要使用并行处理——Java以前对此并不支持。你可能接触过其他编程领域的思想,比如Google的map-reduce,或使用过相对容易的数据库查询语言(如SQL)执行数据操作,它们能帮助你处理大量数据和多核CPU。图1-1总结了语言生态系统:把这幅图看作编程问题空间,每个地方生长的主要植物就是程序最喜欢的语言。气候变化的意思是,新的硬件或新的编程因素(例如,“我为什么不能用SQL的风格来写程序?”)意味着新项目优选的语言各有不同,就像地区气温上升就意味着葡萄在较高的纬度也能长得好。当然这会有滞后——很多老农会一直种植着传统作物。总之,新的语言不断出现,并因为迅速适应了气候变化,越来越受欢迎。



图 1-1 编程语言生态系统和气候变化

对程序员来说,Java 8的主要好处在于它提供了更多的编程工具和概念,能以更快、更简洁、更易于维护的方式解决新的或现有的编程问题,其中简洁和易维护更重要。虽然这些概念对于Java来说是新的,但是研究型的语言已经证明了它们的强大。我们会重点探讨三个编程概念背后的思想,它们促使Java 8开发出了利用并行和编写更简洁代码的功能。这里介绍它们的顺序和本书其余部分略有不同,一方面是为了类比Unix,另一方面是为了揭示Java 8新的多核并行中存在的“因为这个所以需要那个”的依赖关系。

另一个影响Java气候变化的因素

影响Java气候变化的另一个因素是大型系统的设计方式。现在,越来越多的大型系统会集成来自第三方的大型子系统,而这些子系统可能又构建于别的供应商提供的组件之上。更糟糕的是,这些组件以及它们的接口也会不断演进。为了解决这些设计风格上的问题,Java 8和Java 9提供了默认方法和模块系统。



接下来的三个小节会逐一介绍驱动Java 8设计的三个编程概念。





1.2.2 流处理


第一个编程概念是流处理。流是一系列数据项,一次只生成一项。程序可以从输入流中一个一个读取数据项,然后以同样的方式将数据项写入输出流。一个程序的输出流很可能是另一个程序的输入流。

一个实际的例子是在Unix或Linux中,很多程序都从标准输入(Unix和C中的stdin,Java中的System.in)读取数据,然后把结果写入标准输出(Unix和C中的stdout,Java中的System.out)。首先来看一点点背景:Unix的cat命令会把两个文件连接起来创建一个流,tr会转换流中的字符,sort会对流中的行进行排序,tail -3则给出流的最后三行。Unix命令行允许这些程序通过管道(|)连接在一起,比如下面这段代码会假设file1和file2中每行都只有一个单词,先把字母转换成小写字母,然后打印出按照词典顺序排在最后的三个单词:

cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3

我们说sort把一个行流4作为输入,产生了另一个行流(进行排序)作为输出,如图1-2所示。请注意在Unix中,这些命令(cat、tr、sort和tail)是同时执行的,这样sort就可以在cat或tr完成前先处理头几行。就像汽车组装流水线一样,汽车排队进入加工站,每个加工站会接收、修改汽车,然后将之传递给下一站做进一步的处理。尽管流水线实际上是一个序列,但不同加工站的运行一般是并行的。

4有语言洁癖的人会说“字符流”,不过认为sort会对行重新排序比较简单。



图 1-2 操作流的Unix命令

基于这一思想,Java 8在java.util.stream中添加了一个Stream API。Stream<T>就是一系列T类型的项目。你现在可以把它看成一种比较花哨的迭代器。Stream API的很多方法可以链接起来形成一个复杂的流水线,就像先前例子里面链接起来的Unix命令一样。

推动这种做法的关键在于,现在你可以在一个更高的抽象层次上写Java 8程序了:思路变成了把这样的流变成那样的流(就像写数据库查询语句时的那种思路),而不是一次只处理一个项目。另一个好处是,Java 8可以透明地把输入的不相关部分拿到几个CPU核上去分别执行你的Stream操作流水线——这是几乎免费的并行,用不着去费劲搞Thread了。本书第4~7章会仔细讨论Java 8的Stream API。





1.2.3 用行为参数化把代码传递给方法


Java 8中增加的另一个编程概念是通过API来传递代码的能力。这听起来实在太抽象了。在Unix的例子里,你可能想告诉sort命令使用自定义排序。虽然sort命令支持通过命令行参数来执行各种预定义类型的排序,比如倒序,但这毕竟是有限的。

比方说,你有一堆发票代码,格式类似于2013UK0001、2014US0002……其中前四位数代表年份,接下来两个字母代表国家,最后四位是客户的代码。你可能想按照年份、客户代码,甚至国家来对发票进行排序。你真正想要的是,能够给sort命令一个参数让用户定义顺序:给sort命令传递一段独立的代码。

那么,直接套在Java上,你是要让sort方法利用自定义的顺序进行比较。你可以写一个compareUsingCustomerId来比较两张发票的代码,但是在Java 8之前,你无法把这个方法传给另一个方法。你可以像本章开头介绍的那样,创建一个Comparator对象,将之传递给sort方法,不过这不但啰唆,而且让“重用现有行为”的思想变得不那么清楚了。Java 8增加了把方法(你的代码)作为参数传递给另一个方法的能力。图1-3是基于图1-2画出的,它描绘了这种思路。我们把这一概念称为行为参数化。它的重要之处在哪儿呢?Stream API就是构建在通过传递代码使操作行为实现参数化的思想上的,当把compareUsingCustomerId传进去,你就把sort的行为参数化了。



图 1-3 将compareUsingCustomerId方法作为参数传给sort

我们将在1.3节中概述这种方式,第2章和第3章再进行详细讨论。第18章和第19章将讨论这一功能的高级用法,还有函数式编程自身的一些技巧。





1.2.4 并行与共享的可变数据


第三个编程概念更隐晦一点,它源自前面讨论流处理能力时说的“几乎免费的并行”。你需要放弃什么吗?你可能需要稍微改变一下编写传给流方法的行为的方法。这些改变一开始可能会让你有点儿不舒服,但一旦习惯了你就会爱上它们。你提供的行为必须能够同时在不同的输入上安全地执行。一般情况下这就意味着,所写的代码不能访问共享的可变数据来完成它的工作。这些函数有时被称为“纯函数”“无副作用函数”或“无状态函数”,第18章和第19章会详细讨论。前面说的并行只有在你的代码的多个副本可以独立工作时才能进行。但如果要写入的是一个共享变量或对象,就行不通了:如果两个进程需要同时修改这个共享变量怎么办?(1.4节通过配图给出了更详细的解释。)在后续章节中,你会进一步了解这种风格。

Java 8的流实现并行比Java现有的Thread API更容易,因此,尽管可以使用synchronized来打破“不能有共享的可变数据”这一规则,但这相当于是在和整个体系作对,因为它使所有围绕这一规则做出的优化都失去意义了。在多个处理器核之间使用synchronized,其代价往往比你预期的要大得多,因为同步迫使代码按照顺序执行,而这与并行处理的宗旨相悖。

没有共享的可变数据,以及将方法和函数(即代码)传递给其他方法的能力,这两个要点是函数式编程范式的基石,第18章和第19章会详细讨论。与此相反,在命令式编程范式中,你写的程序则是一系列改变状态的指令。“不能有共享的可变数据”意味着,一个方法可以通过它将参数值转换为结果的方式来完整描述,换句话说,它的行为就像一个数学函数,没有可见的副作用。





1.2.5 Java需要演变


前面已经介绍了Java的演变。例如,引入泛型,以及使用List<String>而不只是List,一开始可能都挺烦人的,但现在你已经熟悉了这种风格和它所带来的好处,即在编译时能发现更多错误,且代码更易读,因为你现在知道列表里面是什么了。

其他改变使得表达普通的东西变得更容易,例如,使用for-each循环,而不用暴露Iterator里面的模板写法。Java 8中的主要变化反映了它开始远离常侧重改变现有值的经典面向对象思想,而向函数式编程领域转变。在函数式编程中,在大体上考虑想做什么(例如,创建一个值来代表所有从A到B的低于给定价格的路线)被视为头等大事,并和具体实现方式(例如,扫描一个数据结构并修改某些元素)区分开来。请注意,如果极端点儿来说,传统的面向对象编程和函数式编程可能看起来是冲突的。但是我们的理念是获取两种编程范式中的精华,以便为任务找到理想的工具。1.3节和1.4节会详细讨论。

简而言之,语言需要不断改进,以适应硬件的更新或满足程序员的期待(如果你还不够信服,想想COBOL可一度是最重要的商用语言之一呢)。要坚持下去,Java必须通过增加新功能来改进,而且只有新功能被人使用,变化才有意义。所以,使用Java 8,你就是在保护你作为Java程序员的职业生涯。除此之外,我们有一种感觉——你一定会喜欢Java 8的新功能。随便问问哪个用过Java 8的人,看看他们愿不愿意退回去使用旧版本。还有,用生态系统打比方的话,Java 8的新功能使得Java能够征服如今被其他语言占领的编程任务领地,所以对Java 8程序员的需求更多了。

下面将逐一介绍Java 8中的新概念,并顺便指出哪一章还会详细讨论这些概念。





1.3 Java中的函数


编程语言中的函数一词通常是指方法,尤其是静态方法,这是在数学函数,也就是没有副作用的函数之外的一个新含义。幸运的是,你将会看到,当Java 8提到函数时,这两种用法几乎是一致的。

Java 8中新增了函数,作为值的一种新形式。它有助于使用1.4节中谈到的流,有了它,Java 8可以在多核处理器上进行并行编程。首先来展示一下作为值的函数本身的有用之处。

想想Java程序可能操作的值吧。首先有原始值,比如42(int类型)和3.14(double类型)。其次,值可以是对象(更严格地说是对象的引用)。获得对象的唯一途径是利用new,这也许是通过工厂方法或库函数实现的;对象引用指向一个类的实例。例子包括"abc"(String类型)、new Integer(1111)(Integer类型),以及new HashMap<Integer,String>(100)的结果——它显式调用了HashMap的构造函数。甚至数组也是对象。那么有什么问题呢?

为了帮助回答这个问题,我们要注意到,编程语言的整个目的就在于操作值,按照历史上编程语言的传统,这些值应被称为一等值(或一等公民)。编程语言中的其他结构也许有助于表示值的结构,但在程序执行期间不能传递,因而是二等值。前面所说的值是Java中的一等值,但其他很多Java概念(比如方法和类等)则是二等值。用方法来定义类很不错,类还可以实例化来产生值,但方法和类本身都不是值。这又有什么关系呢?还真有,人们发现,在运行时传递方法能将方法变成一等值。这在编程中非常有用,因此Java 8的设计者把这个功能加入到了Java中。顺便说一下,你可能会想,让类等其他二等值也变成一等值可能也是个好主意。有很多语言,比如Smalltalk和JavaScript,都探索过这条路。





1.3.1 方法和Lambda作为一等值


Scala和Groovy等语言的实践已经证明,让方法等概念作为一等值可以扩充程序员的工具库,从而让编程变得更容易。一旦程序员熟悉了这个强大的功能,就再也不愿意使用没有这一功能的语言了。因此,Java 8的设计者决定允许将方法作为值,让编程更轻松。此外,让方法作为值也构成了其他几个Java 8功能(比如Stream)的基础。

我们介绍的Java 8的第一个新功能是方法引用。比方说,你想要筛选一个目录中的所有隐藏文件。你需要编写一个方法,然后给它一个File,它就会告诉你文件是不是隐藏的。幸好,File类里面有一个叫作isHidden的方法。可以把它看作一个函数,接受一个File,返回一个布尔值。但要用它做筛选,需要把它包在一个FileFilter对象里,然后传递给File.listFiles方法,如下所示:

File[] hiddenFiles = new File(".").listFiles(new FileFilter() { public boolean accept(File file) { return file.isHidden(); ←---- 筛选隐藏文件 } });

呃,真可怕!虽然只有三行,但这三行可真够绕的。我们第一次碰到的时候肯定都说过:“非得这样不可吗?”已经有一个方法isHidden可用,为什么非得把它包在一个啰唆的FileFilter类里面再实例化呢?因为在Java 8之前你必须这么做!

如今在Java 8里,你可以把代码重写成这样:

File[] hiddenFiles = new File(".").listFiles(File::isHidden);

哇!酷不酷?你已经有了函数isHidden,因此只需用Java 8的方法引用::语法(即“把这个方法作为值”)将其传给listFiles方法。请注意,我们也开始用函数代表方法了。稍后会解释这个机制是如何工作的。一个好处是,你的代码现在读起来更接近问题的陈述了。

方法不再是二等值了。与用对象引用传递对象类似(对象引用是用new创建的),在Java 8里写下File::isHidden的时候,你就创建了一个方法引用,你同样可以传递它。第3章会详细讨论这一概念。只要方法中有代码(方法中的可执行部分),那么用方法引用就可以传递代码,如图1-3所示。图1-4说明了这一概念。你在下一节中还将看到一个具体的例子——从库存中选择苹果。



图 1-4 将方法引用File::isHidden传递给listFiles方法





Lambda——匿名函数


除了允许(命名)函数成为一等值外,Java 8还体现了更广义的将函数作为值的思想,包括Lambda5 (或匿名函数)。比如,你现在可以写(int x) -> x + 1,表示“调用时给定参数,就返回值的函数”。你可能会想这有什么必要呢?因为你可以在MyMathsUtils类里面定义一个add1方法,然后写MyMathsUtils::add1嘛!确实是可以,但要是你没有方便的方法和类可用,新的Lambda语法更简洁。第3章会详细讨论Lambda。我们说使用这些概念的程序具有函数式编程风格,这句话的意思是“编写把函数作为一等值来传递的程序”。

5最初是根据希腊字母λ命名的。虽然Java中不使用这个符号,但是名称还是被保留了下来。





1.3.2 传递代码:一个例子


来看一个例子,看看它是如何帮助你写程序的,我们在第2章还会进行更详细的讨论。所有的示例代码均可见于图灵社区本书主页http://ituring.com.cn/book/2659“随书下载”处。假设你有一个Apple类,它有一个getColor方法,还有一个变量inventory保存着一个Apples列表。你可能想要选出所有的绿苹果(此处使用包含值GREEN和RED的Color枚举类型 ),并返回一个列表。通常用筛选(filter)一词来表达这个概念。在Java 8之前,你可能会写这样一个方法filterGreenApples:

public static List<Apple> filterGreenApples(List<Apple> inventory){ List<Apple> result = new ArrayList<>(); ←---- result是用来累积结果的List,开始为空,然后一个个加入绿苹果 for (Apple apple: inventory){ if (GREEN.equals(apple.getColor())) { ←---- 加粗显示的代码会仅仅选出绿苹果 result.add(apple); } } return result; }

但是接下来,有人可能想要选出重的苹果,比如超过150克的苹果,于是你心情沉重地写了下面这个方法,甚至用了复制粘贴:

public static List<Apple> filterHeavyApples(List<Apple> inventory){ List<Apple> result = new ArrayList<>(); for (Apple apple: inventory){ if (apple.getWeight() > 150) { ←---- 这里加粗显示的代码会仅仅选出重的苹果 result.add(apple); } } return result; }

我们都知道软件工程中复制粘贴的危险——给一个做了更新和修正,却忘了另一个。嘿,这两个方法只有一行不同:if里面加粗的那行条件。如果这两个加粗的方法之间的差异仅仅是接受的重量范围不同,那么你只要把接受的重量上下限作为参数传递给filter就行了,比如指定(150, 1000)来选出重的苹果(超过150克),或者指定(0, 80)来选出轻的苹果(低于80克)。

但是,前面提过了,Java 8会把条件代码作为参数传递进去,这样可以避免filter方法中出现重复的代码。现在你可以写:

public static boolean isGreenApple(Apple apple) { return GREEN.equals(apple.getColor()); } public static boolean isHeavyApple(Apple apple) { return apple.getWeight() > 150; } public interface Predicate<T>{ ←---- 写出来是为了清晰(平常只要从java.util.function导入就可以了) boolean test(T t); } static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p) { ←---- 方法作为Predicate参数p传递进去(见附注栏“什么是谓词?”) List<Apple> result = new ArrayList<>(); for (Apple apple: inventory){ if (p.test(apple)) { ←---- 苹果符合p所代表的条件吗 result.add(apple); } } return result; }

要用它的话,你可以写:

filterApples(inventory, Apple::isGreenApple);

或者

filterApples(inventory, Apple::isHeavyApple);

接下来的两章会详细讨论它是怎么工作的。现在重要的是你可以在Java 8里面传递方法了!

什么是谓词?

前面的代码传递了方法Apple::isGreenApple(它接受参数Apple并返回一个boolean)给filterApples,后者则希望接受一个Predicate<Apple>参数。谓词(predicate)在数学上常常用来代表类似于函数的东西,它接受一个参数值,并返回true或false。后面你会看到,Java 8也允许你写Function<Apple,Boolean>——在学校学过函数却没学过谓词的读者对此可能更熟悉,但用Predicate<Apple>是更标准的方式,效率也会更高一点儿,这避免了把boolean封装在Boolean里面。





1.3.3 从传递方法到Lambda


把方法作为值来传递显然很有用,但要是为类似于isHeavyApple和isGreenApple这种可能只用一两次的短方法写一堆定义就有点儿烦人了。不过Java 8也解决了这个问题,它引入了一套新记法(匿名函数或Lambda),让你可以写

filterApples(inventory, (Apple a) -> GREEN.equals(a.getColor()) );

或者

filterApples(inventory, (Apple a) -> a.getWeight() > 150 );

甚至

filterApples(inventory, (Apple a) -> a.getWeight() < 80 || RED.equals(a.getColor()) );

所以,你甚至不需要为只用一次的方法写定义。代码更干净、更清晰,因为你用不着去找自己到底传递了什么代码。但要是Lambda的长度多于几行(它的行为也不是一目了然)的话,那你还是应该用方法引用来指向一个有描述性名称的方法,而不是使用匿名的Lambda。你应该以代码的清晰度为准绳。

Java 8的设计师几乎可以就此打住了,要不是有了多核CPU,可能他们真的就到此为止了。函数式编程竟然如此强大,后面你会有更深的体会。本来,Java加上filter和几个相关的东西作为通用库方法就足以让人满意了,比如

static <T> Collection<T> filter(Collection<T> c, Predicate<T> p);

这样你甚至不需要写filterApples了,因为比如先前的调用

filterApples(inventory, (Apple a) -> a.getWeight() > 150 );

就可以直接调用库方法filter:

filter(inventory, (Apple a) -> a.getWeight() > 150 );

但是,为了更好地利用并行,Java的设计师没有这么做。Java 8中有一整套新的类Collection API——Stream,它有一套类似于函数式程序员熟悉的filter的操作,比如map、reduce,还有接下来要讨论的在Collection和Stream之间做转换的方法。





1.4 流


几乎每个Java应用都会制造和处理集合。但集合用起来并不总是那么理想。比方说,你需要从一个列表中筛选金额较高的交易,然后按货币分组。你需要写一大堆模板代码来实现这个数据处理命令,如下所示:

Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>(); ←---- 建立累积交易分组的Map for (Transaction transaction : transactions) { ←---- 遍历交易的List if(transaction.getPrice() > 1000){ ←---- 筛选金额较高的交易 Currency currency = transaction.getCurrency(); ←---- 提取交易货币 List<Transaction> transactionsForCurrency = transactionsByCurrencies.get(currency); if (transactionsForCurrency == null) { ←---- 如果这个货币的分组Map是空的,那就建立一个 transactionsForCurrency = new ArrayList<>(); transactionsByCurrencies.put(currency, transactionsForCurrency); } transactionsForCurrency.add(transaction); ←---- 将当前遍历的交易添加到具有同一货币的交易List中 } }

此外,很难一眼看出这些代码是做什么的,因为有好几个嵌套的控制流指令。

有了Stream API,你现在可以这样解决这个问题了:

import static java.util.stream.Collectors.groupingBy; Map<Currency, List<Transaction>> transactionsByCurrencies = transactions.stream() .filter((Transaction t) -> t.getPrice() > 1000) ←---- 筛选金额较高的交易 .collect(groupingBy(Transaction::getCurrency)); ←---- 按货币分组

这看起来有点儿神奇,不过现在先不用担心。第4~7章会专门讲述怎么理解Stream API。现在值得注意的是,Stream API处理数据的方式与Collection API不同。用集合的话,你得自己管理迭代过程。你得用for-each循环一个个地迭代元素,然后再处理元素。我们把这种数据迭代方法称为外部迭代。相反,有了Stream API,你根本用不着操心循环的事情。数据处理完全是在库内部进行的。我们把这种思想叫作内部迭代。第4章还会谈到这些思想。

使用集合的另一个头疼之处是,想想看,要是交易量非常庞大,你要怎么处理这个巨大的列表呢?单个CPU根本搞不定这么大量的数据,但你很可能已经有了一台多核计算机。理想情况下,你可能想让这些CPU核共同分担处理工作,以缩短处理时间。理论上来说,要是你有八个核,那并行起来,处理数据的速度应该是单核的八倍。

多核计算机

所有新的台式机和笔记本电脑都是多核的。它们不是仅有一个CPU,而是有四个、八个,甚至更多CPU,通常称为核6。问题是,经典的Java程序只能利用其中一个核,其他核的处理能力都浪费了。类似地,很多公司利用计算集群(用高速网络连接起来的多台计算机)来高效处理海量数据。Java 8提供了新的编程风格,可更好地利用这样的计算机。

Google的搜索引擎就是一个无法在单台计算机上运行的代码示例。它要读取互联网上的每个页面并建立索引,将每个网页上出现的每个词都映射到包含该词的网址上。然后,如果你用多个词进行搜索,软件就可以快速利用索引,给你一个包含这些词的网页集合。想想看,你会如何在Java中实现这个算法,哪怕是比Google小的引擎也需要你利用计算机上所有的核。



6从某种意义上说,这个名字不太好。一块多核芯片上的每个核都是一个五脏俱全的CPU。但“多核CPU”的说法很流行,所以我们就用核来指代各个CPU。





多线程并非易事


问题在于,通过多线程代码来利用并行(使用先前Java版本中的Thread API)并非易事。你得换一种思路:线程可能会同时访问并更新共享变量。因此,如果没有协调好7 ,那么数据可能会被意外改变。相比一步步执行的顺序模型,这个模型不太好理解8。比如,图1-5就展示了如果没有同步好,两个线程同时向共享变量sum加上一个数时,可能会出现的问题。

7传统上是利用synchronized关键字,但是要是用错了地方,就可能出现很多难以察觉的错误。Java 8基于Stream的并行提倡很少使用synchronized的函数式编程风格,它关注数据分块而不是协调访问。

8啊哈,促使语言发展的一个动力源!



图 1-5 两个线程对共享的sum变量做加法的一种可能方式。结果是105,而不是预想的108

Java 8也用Stream API(java.util.stream)解决了这两个问题:集合处理时的模板化和晦涩,以及难以利用多核。这样设计的第一个原因是,有许多反复出现的数据处理模式,类似于前一节所说的filterApples或SQL等数据库查询语言里熟悉的操作,如果库中有这些就会很方便:根据标准筛选数据(比如较重的苹果),提取数据(例如抽取列表中每个苹果的重量字段),或给数据分组(例如,将一个数字列表分为奇数列表和偶数列表)等。第二个原因是,这类操作常常可以并行。例如,如图1-6所示,在两个CPU上筛选列表,可以让一个CPU处理列表的前一半,另一个CPU处理后一半,这称为分支步骤➊。CPU随后对各自的半个列表做筛选➋。最后➌,一个CPU会将两个结果合并(Google搜索这么快就与此紧密相关,当然用的CPU远远不止两个)。



图 1-6 将filter分支到两个CPU上并合并结果

到这里,我们只是说新的Stream API和Java现有的Collection API的行为差不多,它们都能够访问数据项目的序列。不过,现在最好记住,Collection主要是为了存储和访问数据,Stream则主要用于描述对数据的计算。这里的关键点在于,Stream API允许并提倡并行处理一个Stream中的元素。虽然乍看上去可能有点儿怪,但筛选一个Collection(将上一节的filterApples应用在一个List上)的最快方法常常是将其转换为Stream,进行并行处理,然后再转换回List,下面列举的串行和并行的例子都是如此。我们这里还只是说“几乎免费的并行”,让你稍微体验一下,如何利用Stream和Lambda表达式顺序或并行地从一个列表里筛选比较重的苹果。

顺序处理:

import static java.util.stream.Collectors.toList; List<Apple> heavyApples = inventory.stream().filter((Apple a) -> a.getWeight() > 150) .collect(toList());

并行处理:

import static java.util.stream.Collectors.toList; List<Apple> heavyApples = inventory.parallelStream().filter((Apple a) -> a.getWeight() > 150) .collect(toList());

Java中的并行与无共享可变状态

大家都说在Java中并行很难,而且和synchronized相关的“玩意儿”都容易出问题。那Java 8里面有什么“灵丹妙药”呢?事实上有两个。首先,库会负责分块,即把大的流分成几个小的流,以便并行处理。其次,流提供的这个几乎免费的并行,只有在传递给filter之类的库方法的方法不会互动(比方说有可变的共享对象)时才能工作。但是其实这个限制对于程序员来说挺自然的,比如Apple::isGreenApple就是这样。确实,虽然函数式编程中的函数的主要意思是“把函数作为一等值”,但它也常常隐含着第二层意思,即“执行时在元素之间无互动”。



第7章会详细探讨Java 8中的并行数据处理及其特点。在加入所有这些新“玩意儿”改进Java的时候,Java 8设计者发现的一个现实问题就是现有的接口也在改进。比如,Collections.sort方法真的应该属于List接口,但从来没有放在后者里。理想情况下,你会希望做list.sort(comparator),而不是Collections.sort(list, comparator)。这看起来无关紧要,但是在Java 8之前,你可能会更新一个接口,然后发现你把所有实现它的类也给更新了——简直是逻辑灾难!这个问题在Java 8里由默认方法解决了。





1.5 默认方法及Java模块


正如前文所介绍的,现代系统倾向于基于组件进行构建,而这些组件可能源自第三方。历史上,Java对此的支持非常薄弱,它只支持由几个Java包组成的JAR文件,并且这些Java包也没什么结构。此外,要演进这些包中的接口也比较困难——改动一个Java接口时,实现该接口的所有类都会受影响。Java 8和Java 9已经开始着手改进这一问题。

首先,Java 9提供了模块系统,允许你通过语法定义由一系列包组成的模块——通过它你能更好地控制命名空间和包的可见性。模块对简单的类JAR组件进行了增强,使其具备了结构,既能作为用户文档,也能由机器进行检查。第14章会详细探讨这部分内容。其次,Java 8引入了默认方法来支持接口的演进。第13章会详细介绍默认方法。它们非常重要,因为使用接口时你经常会碰到,然而大多数的程序员可能并不需要编写默认方法,因为默认方法只是推进程序演进的一种技术,并不会直接帮助你实现某个特性。本节会基于一个例子简要地介绍默认方法。

1.4节中给出了下面这段Java 8示例代码:

List<Apple> heavyApples1 = inventory.stream().filter((Apple a) -> a.getWeight() > 150) .collect(toList()); List<Apple> heavyApples2 = inventory.parallelStream().filter((Apple a) -> a.getWeight() > 150) .collect(toList());

但这里有个问题:在Java 8之前,List<T>并没有stream或parallelStream方法,它实现的Collection<T>接口也没有,因为当初还没有想到这些方法嘛!可没有这些方法,这些代码就不能编译。换作你自己的接口的话,最简单的解决方案就是让Java 8的设计者把stream方法加入Collection接口,并加入ArrayList类的实现。

可要是这样做,对用户来说就是噩梦了。有很多集合框架都用Collection API实现了接口。但给接口加入一个新方法,意味着所有的实体类都必须为其提供一个实现。语言设计者没法控制Collection所有现有的实现,这下你就进退两难了:你如何改变已发布的接口而不破坏已有的实现呢?

Java 8的解决方法就是打破最后一环——接口如今可以包含实现类没有提供实现的方法签名了!那谁来实现它呢?缺失的方法主体随接口提供了(因此就有了默认实现),而不是由实现类提供。

这就给接口设计者提供了一种扩充接口的方式,而不会破坏现有的代码。Java 8在接口声明中使用新的default关键字来表示这一点。

例如,在Java 8里,你可以直接对List调用sort方法。它是用Java 8 List接口中如下所示的默认方法实现的,它会调用Collections.sort静态方法:

default void sort(Comparator<? super E> c) { Collections.sort(this, c); }

这意味着List的任何实体类都不需要显式实现sort,而在以前的Java版本中,除非提供了sort的实现,否则这些实体类在重新编译时都会失败。

不过请稍等,一个类可以实现多个接口,不是吗?那么,如果在好几个接口里有多个默认实现,是否意味着Java中有了某种形式的多重继承?是的,在某种程度上是这样。第13章中会谈到,Java 8用一些限制来避免出现类似于C++中臭名昭著的菱形继承问题。





1.6 来自函数式编程的其他好思想


前几节介绍了Java从函数式编程引入的两个核心思想:将方法和Lambda作为一等值,以及在没有可变共享状态时,函数或方法可以有效、安全地并行执行。这两种思想新的Stream API都用到了。

常见的函数式语言,如SML、OCaml、Haskell,还提供了进一步的结构来帮助程序员,其中之一就是通过显式使用更多的描述性数据类型来避免null。确实,计算机科学巨擘之一托尼·霍尔(Tony Hoare)在2009年伦敦QCon上的演讲中说道:

我把它叫作我“价值亿万美元的错误”,就是在1965年发明了空引用……我无法抵抗放进一个空引用的诱惑,仅仅是因为它实现起来非常容易。



Java 8提供了一个Optional<T>类,如果你能一致地使用它,就能帮助你避免出现NullPointerException。这是一个容器对象,它既可以包含值,也可以不包含值。Optional<T>提供了方法来明确地处理值不存在的情况,这样就可以避免NullPointerException了。换句话说,它通过类型系统,允许你表明一个变量可能缺失值。第11章会详细讨论Optional<T>。

第二个思想是(结构化的)模式匹配9。这个术语最早用在数学里,例如:

9这个术语有两个意思,这里指的是数学和函数式编程中的意思,即函数是分情况定义的,而不是使用if-then-else。它的另一个意思类似于“在给定目录中找到所有类似于IMG*.JPG形式的文件”,和所谓的正则表达式有关。

f(0) = 1 f(n) = n*f(n-1) otherwise

Java中,你可以使用if-then-else或switch语句表达同样的语义。其他语言已经证实,对于更复杂的数据类型,在表达编程思想时,使用模式匹配比if-then-else更简明。你也可以采用多态和方法重写替代if-then-else来处理这种类型的数据,但是,到底哪种方式更适合,在语言设计上仍然有很多争论。10 我们认为两者都是有用的工具,你都应该掌握。不幸的是,Java 8并不完全支持模式匹配,我们会在第19章介绍如何用Java表达模式匹配。此外,还会介绍一个Java改进提议,讨论如何在未来的Java版本中支持模式匹配。与此同时,我们会用Scala语言(这是另一种基于JVM的类Java语言,它启发了Java的一些新特性,更多内容参见第20章)的一个例子进行介绍。譬如,你要设计一个程序,要对描述算术表达式的树做基本的简化。假设数据类型Expr代表了这个表达式,你可以用Scala编写如下代码,将Expr拆分为各个部分,然后返回一个新的Expr:

10维基百科中的文章“Expression Problem”(由Phil Wadler发明的术语)对这一讨论有所介绍。

def simplifyExpression(expr: Expr): Expr = expr match { case BinOp("+", e, Number(0)) => e ←---- 加上0 case BinOp("-", e, Number(0)) => e ←---- 减去0 case BinOp("*", e, Number(1)) => e ←---- 乘以1 case BinOp("/", e, Number(1)) => e ←---- 除以1 case _ => expr ←---- 不能简化expr }

这里,Scala的语法expr match就对应于Java中的switch (expr)。你暂时不用担心不理解这段代码,第19章会介绍更多关于模式匹配的内容。现在,你可以把模式匹配看作switch的扩展形式,它能够同时将一个数据类型分解成元素。

为什么Java中的switch语句要局限于原始类型值和Strings呢?函数式语言倾向于让switch支持更多的数据类型,甚至允许模式匹配(就像Scala语言中match的操作)。面向对象设计中,常用的访客模式可以用来遍历一组类(比如汽车的不同组件:车轮、发动机、底盘等),并对每个访问的对象执行操作。模式匹配的优势之一是编译器能够检测常见的错误,例如:“Brakes类是用来表示Car类的组件的一族类。你忘记了要显式处理它。”

第18章和第19章会全面介绍函数式编程,以及如何在Java 8中编写函数式风格的程序,包括库中提供的函数工具。第20章会讨论Java 8的功能并与Scala进行比较。Scala和Java一样基于JVM实现,且近年来发展迅速,已经在编程语言生态系统的一些方面威胁到了Java。这部分内容放在了本书的后面几章,你会进一步了解Java 8和Java 9为什么加上了这些新功能。

Java 8、9、10以及11的新特性:从哪里入手?

Java 8和Java 9都为Java语言提供了重大更新。不过,作为Java程序员,你更关心的可能是Java 8带来的变化,因为这将直接影响你的日常工作——传递方法或者Lambda表达式正变成日益重要的Java知识。与此相反,Java 9的改进提升的是我们定义和使用大型组件的能力,譬如使用模块化构建一个系统,或者导入一个反应式编程的工具集。最后,Java 10引入的变化比前面几个版本小得多,主要是新增了对局部变量类型推断的支持,第21章会详细探讨。此外,Java 11中Lambda表达式支持的参数语法会更丰富,第21章也会介绍。

截至本书创作时,Java 11的发布计划是2018年9月。Java 11还引入了一个全新的异步HTTP客户端库,它基于Java 8和Java 9提供的CompletableFuture和反应式编程(详细内容参见第15章、第16章和第17章)。





1.7 小结


以下是本章中的关键概念。

请记住语言生态系统的思想,以及语言面临的“要么改变,要么衰亡”的压力。虽然Java可能现在非常有活力,但你可以回忆一下其他曾经也有活力但未能及时改进的语言的命运,如COBOL。

Java 8中新增的核心内容提供了令人激动的新概念和功能,方便我们编写既有效又简洁的程序。

Java 8之前的编程实践并不能很好地利用多核处理器。

函数是一等值。记住方法如何作为函数式值来传递,还有Lambda是怎样写的。

Java 8中流的概念使得集合的许多方面得以推广,但流让代码更易读,并允许并行处理流元素。

Java对基于大型组件的程序设计以及系统需要不断演化的接口的支持一直都不太好。现在,你可以使用Java 9的模块构建你的系统,使用默认方法支持接口的持续演化,而不影响实现该接口的所有类。

其他来自函数式编程的有趣思想,包括处理null和使用模式匹配。





第 2 章 通过行为参数化传递代码


本章内容

应对不断变化的需求

行为参数化

匿名类

Lambda表达式预览

真实示例:Comparator、Runnable和GUI





软件工程中一个众所周知的问题就是,不管你做什么,用户的需求肯定会变。比方说,有个应用程序是帮助农民了解自己的库存。这位农民可能想要一个查找库存中所有绿色苹果的功能。但到了第二天,他可能会告诉你:“其实我还想找出所有重量超过150克的苹果。”过了两天,农民又跑回来补充道:“要是我可以找出所有既是绿色,重量也超过150克的苹果,那就太棒了。”你要如何应对这样不断变化的需求?理想的状态下,应该把你的工作量降到最少。此外,类似的新功能实现起来还应该很简单,而且易于长期维护。

行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式。一言以蔽之,它意味着拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被你程序的其他部分调用,这意味着你可以推迟这块代码的执行。例如,你可以将代码块作为参数传递给另一个方法,稍后再去执行它。这样,这个方法的行为就基于那块代码被参数化了。例如,如果你要处理一个集合,可能会写一个方法:

可以对列表中的每个元素做“某件事”;

可以在列表处理完后做“另一件事”;

遇到错误时可以做“另外一件事”。



行为参数化说的就是这个。打个比方吧:你的室友知道怎么开车去超市,再开回家。于是你可以告诉他去买一些东西,比如面包、奶酪、葡萄酒什么的。这相当于调用一个goAndBuy方法,把购物单作为参数。然而,有一天你在上班,你需要他去做一件他从来没有做过的事情:从邮局取一个包裹。现在你就需要传递给他一系列指示了:去邮局,使用单号,和工作人员说明情况,取走包裹。你可以把这些指示用电子邮件发给他,当他收到之后就可以按照指示行事了。你现在做的事情就更高级一些了,相当于一个方法:goAndBuy。它可以接受不同的新行为作为参数,然后去执行。

这一章首先会给你讲解一个例子,说明如何对你的代码加以改进,从而更灵活地适应不断变化的需求。在此基础之上,我们将展示如何把行为参数化用在几个真实的例子上。比如,你可能已经用过了行为参数化模式——使用Java API中现有的类和接口,对List进行排序,筛选文件名,或告诉一个Thread去执行代码块,甚或是处理GUI事件。你很快会发现,在Java中使用这种模式十分啰唆。Java 8中的Lambda解决了代码啰唆的问题。第3章会向你展示如何构建Lambda表达式、其使用场合,以及如何利用它让代码更简洁。





2.1 应对不断变化的需求


编写能够应对变化的需求的代码并不容易。下面来看一个例子,我们会逐步改进这个例子,以展示一些让代码更灵活的最佳做法。就农场库存程序而言,你必须实现一个从列表中筛选绿苹果的功能。听起来很简单吧?





2.1.1 初试牛刀:筛选绿苹果


我们在第1章中假设你使用一个枚举变量Color来表示苹果的各种颜色:

enum Color { RED, GREEN }

第一个解决方案可能是下面这样的:

public static List<Apple> filterGreenApples(List<Apple> inventory) { List<Apple> result = new ArrayList<>(); ←---- 累积苹果的列表 for(Apple apple: inventory){ if( GREEN.equals(apple.getColor() ) { ←---- 仅仅选出绿苹果 result.add(apple); } } return result; }

突出显示的行就是筛选绿苹果所需的条件。你可以假设枚举变量Color是一个由颜色组成的集合,譬如GREEN。但是现在农民突然改主意了,他还想要筛选出红色的苹果。你该怎么做呢?简单的解决办法就是复制这个方法,把名字改成filterRedApples,然后更改if条件来匹配红苹果。然而,要是农民想要筛选多种颜色,这种方法就应付不了了。一个好的原则是编写类似的代码之后,尽量对其进行抽象化。





2.1.2 再展身手:把颜色作为参数


为了创建filterRedApples,我们重复了filterGreenApples中的大部分代码,怎样才能避免这种问题发生呢?一种做法是给方法添加一个参数,把颜色变成参数,这样就能灵活地适应变化了:

public static List<Apple> filterApplesByColor(List<Apple> inventory, Color color) { List<Apple> result = new ArrayList<>(); for (Apple apple: inventory) { if ( apple.getColor().equals(color) ) { result.add(apple); } } return result; }

现在,只要像下面这样调用方法,农民朋友就会满意了:

List<Apple> greenApples = filterApplesByColor(inventory, GREEN); List<Apple> redApples = filterApplesByColor(inventory, RED); ...

太简单了,对吧?让我们把例子再弄得复杂一点儿。这位农民又跑回来和你说:“要是能区分轻的苹果和重的苹果就太好了。重的苹果一般是重量大于150克。”

作为软件工程师,你早就想到农民可能会要改变重量,于是你写了下面的方法,用另一个参数来应对不同的重量:

public static List<Apple> filterApplesByWeight(List<Apple> inventory, int weight) { List<Apple> result = new ArrayList<>(); For (Apple apple: inventory){ if ( apple.getWeight() > weight ) { result.add(apple); } } return result; }

解决方案不错,但是请注意,你复制了大部分的代码来实现遍历库存,并对每个苹果应用筛选条件。这有点儿令人失望,因为它打破了DRY(Don't Repeat Yourself,不要重复自己)的软件工程原则。如果你想要改变筛选遍历方式以提升性能,该怎么办?那就得修改所有方法的实现,而不是只改一个。从工程工作量的角度来看,这代价太大了。

你可以将颜色和重量结合为一个方法,称为filter。不过就算这样,你还是需要一种方式来区分想要筛选哪个属性。你可以加上一个标志来区分对颜色和重量的查询(但绝不要这样做!我们很快会解释为什么)。





2.1.3 第三次尝试:对你能想到的每个属性做筛选


一种把所有属性结合起来的笨拙尝试如下所示:

public static List<Apple> filterApples(List<Apple> inventory, Color color, int weight, boolean flag) { List<Apple> result = new ArrayList<>(); for (Apple apple: inventory) { if ( (flag && apple.getColor().equals(color)) || (!flag && apple.getWeight() > weight) ){ ←---- 十分笨拙的选择颜色或重量的方式 result.add(apple); } } return result; }

你可以这么用(但真的很笨拙):

List<Apple> greenApples = filterApples(inventory, GREEN, 0, true); List<Apple> heavyApples = filterApples(inventory, null, 150, false); ...

这个解决方案再差不过了。首先,客户端代码看上去糟透了。true和false是什么意思?此外,这个解决方案还是不能很好地应对变化的需求。如果这位农民要求你对苹果的不同属性做筛选,比如大小、形状、产地等,该怎么办?而且,如果农民要求你组合属性,做更复杂的查询,比如绿色的重苹果,又该怎么办?你会有好多个重复的filter方法,或一个巨大的非常复杂的方法。到目前为止,你已经给filterApples方法加上了值(比如String、Integer或boolean)的参数。这对于某些确定性问题可能还不错。但如今这种情况下,你需要一种更好的方式,来把苹果的选择标准告诉你的filterApples方法。下一节会介绍如何利用行为参数化实现这种灵活性。





2.2 行为参数化


你在上一节中已经看到了,你需要一种比添加很多参数更好的方法来应对变化的需求。让我们后退一步来看看更高层次的抽象。一种可能的解决方案是对你的选择标准建模:你考虑的是苹果,需要根据Apple的某些属性(比如它是绿色的吗?重量超过150克吗?)来返回一个boolean值。我们把它称为谓词(即一个返回boolean值的函数)。让我们定义一个接口来对选择标准建模:

public interface ApplePredicate{ boolean test (Apple apple); }

现在你就可以用ApplePredicate的多个实现代表不同的选择标准了,比如(如图2-1所示):

public class AppleHeavyWeightPredicate implements ApplePredicate{ ←---- 仅仅选出重的苹果 public boolean test(Apple apple){ return apple.getWeight() > 150; } } public class AppleGreenColorPredicate implements ApplePredicate{ ←---- 仅仅选出绿苹果 public boolean test(Apple apple){ return GREEN.equals(apple.getColor()); } }



图 2-1 选择苹果的不同策略

你可以把这些标准看作filter方法的不同行为。你刚做的这些和“策略设计模式”相关,它让你定义一族算法,把它们封装起来(称为“策略”),然后在运行时选择一个算法。在这里,算法族就是ApplePredicate,不同的策略就是AppleHeavyWeightPredicate和AppleGreenColorPredicate。

但是,该怎么利用ApplePredicate的不同实现呢?你需要filterApples方法接受ApplePredicate对象,对Apple做条件测试。这就是行为参数化:让方法接受多种行为(策略)作为参数,并在内部使用,来完成不同的行为。

要在我们的例子中实现这一点,你要给filterApples方法添加一个参数,让它接受ApplePredicate对象。这在软件工程上有很大好处:现在你把filterApples方法迭代集合的逻辑与你要应用到集合中每个元素的行为(这里是一个谓词)区分开了。





第四次尝试:根据抽象条件筛选


利用ApplePredicate改过之后,filter方法看起来是这样的:

public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) { List<Apple> result = new ArrayList<>(); for(Apple apple: inventory) { if(p.test(apple)){ ←---- 谓词p 封装了测试苹果的条件 result.add(apple); } } return result; }

传递代码/行为

这里值得停下来小小地庆祝一下。这段代码比我们第一次尝试的时候灵活多了,读起来、用起来也更容易!现在你可以创建不同的ApplePredicate对象,并将它们传递给filterApples方法。免费的灵活性!比如,如果农民让你找出所有重量超过150克的红苹果,你只需要创建一个类来实现ApplePredicate就行了。你的代码现在足够灵活,可以应对任何涉及苹果属性的需求变更了:

public class AppleRedAndHeavyPredicate implements ApplePredicate { public boolean test(Apple apple){ return RED.equals(apple.getColor()) && apple.getWeight() > 150; } } List<Apple> redAndHeavyApples = filterApples(inventory, new AppleRedAndHeavyPredicate());

你已经做成了一件很酷的事:filterApples方法的行为取决于你通过ApplePredicate对象传递的代码。换句话说,你把filterApples方法的行为参数化了!

请注意,在上一个例子中,唯一重要的代码是test方法的实现,如图2-2所示,正是它定义了filterApples方法的新行为。但令人遗憾的是,由于该filterApples方法只能接受对象,所以你必须把代码包裹在ApplePredicate对象里。你的做法就类似于在内联“传递代码”,因为你是通过一个实现了test方法的对象来传递布尔表达式的。你将在2.3节(第3章中有更详细的内容)中看到,通过使用Lambda,可以直接把表达式RED.equals(apple.getColor()) &&apple.getWeight() > 150传递给filterApples方法,而无须定义多个ApplePredicate类,从而去掉不必要的代码。



图 2-2 参数化filterApples的行为并传递不同的筛选策略



多种行为,一个参数

正如先前解释的那样,行为参数化的好处在于你可以把迭代要筛选的集合的逻辑与对集合中每个元素应用的行为区分开来。这样你可以重复使用同一个方法,给它不同的行为来达到不同的目的,如图2-3所示。这就是行为参数化是一个有用的概念的原因。你应该把它放进你的工具箱里,用来编写灵活的API。



图 2-3 参数化filterApples的行为并传递不同的筛选策略

为了保证你对行为参数化运用自如,看看测验2.1吧!

测验2.1:编写灵活的prettyPrintApple方法

编写一个prettyPrintApple方法,它接受一个Apple的List,并可以对它参数化,以多种方式根据苹果生成一个String输出(有点儿像多个可定制的toString方法)。例如,你可以告诉prettyPrintApple方法,只打印每个苹果的重量。此外,你可以让prettyPrintApple方法分别打印每个苹果,然后说明它是重的还是轻的。解决方案和前面讨论的筛选的例子类似。为了帮你上手,我们提供了prettyPrintApple方法的一个粗略的框架:

public static void prettyPrintApple(List<Apple> inventory, ???){ for(Apple apple: inventory) { String output = ???.???(apple); System.out.println(output); } }

答案:首先,你需要一种表示接受Apple并返回一个格式String值的方法。前面我们在编写ApplePredicate接口的时候,写过类似的东西:

public interface AppleFormatter{ String accept(Apple a); }

现在你就可以通过实现AppleFormatter方法来表示多种格式行为了:

public class AppleFancyFormatter implements AppleFormatter{ public String accept(Apple apple){ String characteristic = apple.getWeight() > 150 ? "heavy" : "light"; return "A " + characteristic + " " + apple.getColor() +" apple"; } } public class AppleSimpleFormatter implements AppleFormatter{ public String accept(Apple apple){ return "An apple of " + apple.getWeight() + "g"; } }

最后,你需要告诉prettyPrintApple方法接受AppleFormatter对象,并在内部使用它们。你可以给prettyPrintApple加上一个参数:

public static void prettyPrintApple(List<Apple> inventory, AppleFormatter formatter){ for(Apple apple: inventory){ String output = formatter.accept(apple); System.out.println(output); } }

搞定啦!现在你就可以给prettyPrintApple方法传递多种行为了。为此,你首先要实例化AppleFormatter的实现,然后把它们作为参数传给prettyPrintApple:

prettyPrintApple(inventory, new AppleFancyFormatter());

这将产生一个类似于下面的输出:

A light green apple A heavy red apple ...

或者试试这个:

prettyPrintApple(inventory, new AppleSimpleFormatter());

这将产生一个类似于下面的输出:

An apple of 80g An apple of 155g ...



你已经看到,可以把行为抽象出来,让你的代码适应需求的变化,但这个过程很啰唆,因为你需要声明很多只要实例化一次的类。来看看可以怎样改进。





2.3 对付啰唆


我们都知道,人们不愿意用那些很麻烦的功能或概念。目前,当要把新的行为传递给filterApples方法的时候,你不得不声明好几个实现ApplePredicate接口的类,然后实例化好几个只会提到一次的ApplePredicate对象。下面的程序总结了你目前看到的一切。这真是很啰唆,很费时间!

代码清单 2-1 行为参数化:用谓词筛选苹果



public class AppleHeavyWeightPredicate implements ApplePredicate{ ←---- 选择较重苹果的谓词 public boolean test(Apple apple){ return apple.getWeight() > 150; } } public class AppleGreenColorPredicate implements ApplePredicate{ ←---- 选择绿苹果的谓词 public boolean test(Apple apple){ return GREEN.equals(apple.getColor()); } } public class FilteringApples{ public static void main(String...args) { List<Apple> inventory = Arrays.asList(new Apple(80, GREEN), new Apple(155, GREEN), new Apple(120, RED)); List<Apple> heavyApples = filterApples(inventory, new AppleHeavyWeightPredicate()); ←---- 结果是一个包含一个155克Apple的List List<Apple> greenApples = filterApples(inventory, new AppleGreenColorPredicate()); ←---- 结果是一个包含两个绿Apple的List } public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) { List<Apple> result = new ArrayList<>(); for (Apple apple : inventory){ if (p.test(apple)){ result.add(apple); } } return result; } }

费这么大劲儿真没必要,能不能做得更好呢?Java有一个机制称为匿名类,它可以让你同时声明和实例化一个类。它可以帮助你进一步改善代码,让它变得更简洁。但这也不完全令人满意。2.3.3节简短地介绍了Lambda表达式如何让你的代码更易读,下一章将会对此进行更加详细的讨论。





2.3.1 匿名类


匿名类和你熟悉的Java局部类(块中定义的类)差不多,但匿名类没有名字。它允许你同时声明并实例化一个类。换句话说,它允许你随用随建。





2.3.2 第五次尝试:使用匿名类


下面的代码展示了如何通过创建一个用匿名类实现ApplePredicate的对象,重写筛选的例子:

List<Apple> redApples = filterApples(inventory, new ApplePredicate() { ←---- 使用匿名类参数化filterApples方法的行为 public boolean test(Apple apple){ return RED.equals(apple.getColor()); } });

GUI应用程序中经常使用匿名类来创建事件处理器对象(下面的例子使用的是Java FX API,一种现代的Java UI平台):

button.setOnAction(new EventHandler<ActionEvent>() { public void handle(ActionEvent event) { System.out.println("Whoooo a click!!"); } });

但匿名类还是不够好。第一,它往往很笨重,因为它占用了很多空间。还拿前面的例子来看,如下面的粗体代码所示:

List<Apple> redApples = filterApples(inventory, new ApplePredicate() { (以下6行)很多模板代码 public boolean test(Apple a){ return RED.equals(a.getColor()); } }); button.setOnAction(new EventHandler<ActionEvent>() { public void handle(ActionEvent event) { System.out.println("Whoooo a click!!"); } });

第二,很多程序员觉得它用起来很让人费解。比如,测验2.2展示了一个经典的Java谜题,它让大多数程序员都措手不及。你来试试看吧。

测验2.2:匿名类谜题

下面的代码执行时会有什么样的输出,4、5、6还是42?

public class MeaningOfThis { public final int value = 4; public void doIt() { int value = 6; Runnable r = new Runnable(){ public final int value = 5; public void run(){ int value = 10; System.out.println(this.value); } }; r.run(); } public static void main(String...args) { MeaningOfThis m = new MeaningOfThis(); m.doIt(); ←---- 这一行的输出是什么? } }

答案:会输出5,因为this指的是包含它的Runnable,而不是外面的类MeaningOfThis。



整体来说,啰唆就不好。它让人不愿意使用语言的某种功能,因为编写和维护啰唆的代码需要很长时间,而且代码也不易读。好的代码应该是一目了然的。即使匿名类处理在某种程度上改善了为一个接口声明好几个实体类的啰唆问题,但它仍不能令人满意。在只需要传递一段简单的代码时(例如表示选择标准的boolean表达式),你还是要创建一个对象,明确地实现一个方法来定义一个新的行为(例如Predicate中的test方法或是EventHandler中的handle方法)。

在理想的情况下,我们想鼓励程序员使用行为参数化模式,因为正如你在前面看到的,它让代码更能适应需求的变化。在第3章中,你会看到Java 8的语言设计者通过引入Lambda表达式——一种更简洁的传递代码的方式——解决了这个问题。好了,悬念够多了,下面简单介绍一下Lambda表达式是怎么让代码更干净的。





2.3.3 第六次尝试:使用Lambda表达式


上面的代码在Java 8里可以用Lambda表达式重写为下面的样子:

List<Apple> result = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));

不得不承认这段代码看上去比先前干净很多。这很好,因为它看起来更像问题陈述本身了。现在已经解决了啰唆的问题。图2-4对我们到目前为止的工作做了一个小结。



图 2-4 行为参数化与值参数化





2.3.4 第七次尝试:将List类型抽象化


在通往抽象的路上,还可以更进一步。目前,filterApples方法还只适用于Apple。你还可以将List类型抽象化,从而超越你眼前要处理的问题:

public interface Predicate<T>{ boolean test(T t); } public static <T> List<T> filter(List<T> list, Predicate<T> p){ ←---- 引入类型参数T List<T> result = new ArrayList<>(); for(T e: list){ if(p.test(e)){ result.add(e); } } return result; }

现在你可以把filter方法用在香蕉、橘子、Integer或是String的列表上了。这里有一个使用Lambda表达式的例子:

List<Apple> redApples = filter(inventory, (Apple apple) -> RED.equals(apple.getColor())); List<Integer> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);

酷不酷?你现在在灵活性和简洁性之间找到了最佳平衡点,这在Java 8之前是不可能做到的!





2.4 真实的例子


你现在已经看到,行为参数化是一个很有用的模式,它能够轻松地适应不断变化的需求。这种模式可以把一个行为(一段代码)封装起来,并通过传递和使用创建的行为(例如对Apple的不同谓词)将方法的行为参数化。前面提到过,这种做法类似于策略设计模式。你可能已经在实践中用过这个模式了。Java API中的很多方法都可以用不同的行为来参数化。这些方法往往与匿名类一起使用。我们会展示四个例子,这应该能帮助你巩固传递代码的思想了:用一个Comparator排序,用Runnable执行一个代码块,用Callable从任务返回结果,以及GUI事件处理。





2.4.1 用Comparator来排序


对集合进行排序是一个常见的编程任务。比如,你的那位农民朋友想要根据苹果的重量对库存进行排序,或者他可能改了主意,希望你根据颜色对苹果进行排序。听起来有点儿耳熟?是的,你需要一种方法来表示和使用不同的排序行为,以轻松地适应变化的需求。

在Java 8中,List自带了一个sort方法(你也可以使用Collections.sort)。sort的行为可以用java.util.Comparator对象来参数化,它的接口如下:

// java.util.Comparator public interface Comparator<T> { int compare(T o1, T o2); }

因此,你可以随时创建Comparator的实现,用sort方法表现出不同的行为。例如,你可以使用匿名类,按照重量升序对库存排序:

inventory.sort(new Comparator<Apple>() { public int compare(Apple a1, Apple a2) { return a1.getWeight().compareTo(a2.getWeight()); } });

如果农民改了主意,你可以随时创建一个Comparator来满足他的新要求,并把它传递给sort方法。而如何进行排序这一内部细节都被抽象掉了。用Lambda表达式的话,看起来就是这样:

inventory.sort( (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

现在暂时不用担心这个新语法,下一章会详细讲解如何编写和使用Lambda表达式。





2.4.2 用Runnable执行代码块


使用Java的线程,一块代码可以与程序的其他部分并发执行。但是,怎么才能通知线程执行哪块代码呢?此外,几个线程可能还需要执行不同的代码。我们需要一种方式来表示哪一段代码会在之后执行。Java 8之前,能传递给线程结构的只有对象,因此之前典型的使用模式是传递一个带有run方法,返回值为void(即不返回任何对象)的匿名类,非常臃肿。这种匿名类通常会实现一个Runnable接口。

在Java里,你可以使用Runnable接口表示一个要执行的代码块。请注意,该代码不会返回任何结果(即void):

// java.lang.Runnable public interface Runnable{ void run(); }

你可以像下面这样,使用这个接口创建执行不同行为的线程:

Thread t = new Thread(new Runnable() { public void run(){ System.out.println("Hello world"); } });

用Lambda表达式的话,看起来是这样:

Thread t = new Thread(() -> System.out.println("Hello world"));





2.4.3 通过Callable返回结果


你可能已经非常熟悉Java 5引入的ExecutorService。ExecutorService接口解耦了任务的提交和执行。与使用线程和Runnable的方式比较起来,通过ExecutorService你可以把一项任务提交给一个线程池,并且可以使用Future获取其执行的结果,这种方式用处非常大。不必担心你对此一无所知,我们会在之后讨论并发的章节中详细介绍这部分内容。目前你只需要知道使用Callable接口可以对返回结果的任务建模。你可以把它看成升级版的Runnable:

// java.util.concurrent.Callable public interface Callable<V> { V call(); }

你可以像下面这样使用它,即提交一个任务给ExecutorService。下面这段代码会返回执行任务的线程名:

ExecutorService executorService = Executors.newCachedThreadPool(); Future<String> threadName = executorService.submit(new Callable<String>() { @Override public String call() throws Exception { return Thread.currentThread().getName(); } });

如果使用Lambda表达式,上述代码可以更加简化,如下所示:

Future<String> threadName = executorService.submit( () -> Thread.currentThread().getName());





2.4.4 GUI事件处理


GUI编程的一个典型模式就是执行一个操作来响应特定事件,如鼠标单击或在文本上悬停。例如,如果用户单击“发送”按钮,你可能想显示一个弹出式窗口,或把行为记录在一个文件中。你还是需要一种方法来应对变化。你应该能够作出任意形式的响应。在JavaFX中,你可以使用EventHandler,把它传给setOnAction来表示对事件的响应:

Button button = new Button("Send"); button.setOnAction(new EventHandler<ActionEvent>() { public void handle(ActionEvent event) { label.setText("Sent!!"); } });

这里,setOnAction方法的行为就用EventHandler参数化了。用Lambda表达式的话,看起来就是这样:

button.setOnAction((ActionEvent event) -> label.setText("Sent!!"));





2.5 小结


以下是本章中的关键概念。

行为参数化就是一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。

行为参数化可让代码更好地适应不断变化的要求,减轻未来的工作量。

传递代码就是将新行为作为参数传递给方法。但在Java 8之前这实现起来很啰唆。为接口声明许多只用一次的实体类而造成的啰唆代码,在Java 8之前可以用匿名类来减少。

Java API包含很多可以用不同行为进行参数化的方法,包括排序、线程和GUI处理。





第 3 章 Lambda表达式


本章内容

Lambda管中窥豹

在哪里以及如何使用Lambda

环绕执行模式

函数式接口,类型推断

方法引用

Lambda复合





在上一章中,你了解了利用行为参数化来传递代码有助于应对不断变化的需求。它允许你定义一段代码块来表示一个行为,然后传递它。你可以决定在某一事件发生时(例如单击一个按钮)或在算法中的某个特定时刻(例如筛选算法中类似于“重量超过150克的苹果”的谓词,或排序中自定义的比较操作)运行该代码块。一般来说,利用这个概念,你就可以编写更为灵活且可重复使用的代码了。

但你也看到了,采用匿名类来表示多种行为并不令人满意:代码十分啰唆,这会影响程序员在实践中使用行为参数化的积极性。本章会教给你Java 8解决这个问题的新工具——Lambda表达式。它能帮助你很简洁地表示一个行为或者传递代码。现在你可以把Lambda表达式看成匿名函数,它基本上就是没有声明名称的方法,但和匿名类一样,它也能作为参数传递给一个方法。

我们会展示如何构建Lambda,它的使用场合,以及如何利用它让代码更简洁。还会介绍一些新的东西,如类型推断以及Java 8 API中新增的重要接口。最后会介绍方法引用,这是个常常与Lambda表达式联合使用的新功能,非常有价值。

本章的行文思想就是教你如何一步一步地写出更简洁、更灵活的代码。本章结束时,我们会把所有教过的概念融合在一个具体的例子里:用Lambda表达式和方法引用逐步改进第2章中的排序例子,使之更加简明易读。这一章很重要,我们会在本章中大量使用贯穿全书的Lambda。





3.1 Lambda管中窥豹


可以把Lambda表达式理解为一种简洁的可传递匿名函数:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。这个定义够大的,让我们慢慢道来。

匿名——说它是匿名的,因为它不像普通的方法那样有一个明确的名称:写得少而想得多!

函数——说它是一种函数,是因为Lambda函数不像方法那样属于某个特定的类。但和方法一样,Lambda有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。

传递——Lambda表达式可以作为参数传递给方法或存储在变量中。

简洁——你无须像匿名类那样写很多模板代码。



你是不是很好奇Lambda这个词是从哪儿来的?其实它起源于学术界开发出的一套用来描述计算的λ演算法。

你为什么应该关心Lambda表达式呢?你在上一章中看到了,在Java中传递代码十分烦琐和冗长。那么,现在有了好消息!Lambda解决了这个问题:它可以让你十分简明地传递代码。理论上来说,你在Java 8之前做不了的事情,Lambda也做不了。但是,现在你用不着再用匿名类写一堆笨重的代码,来体验行为参数化的好处了!Lambda表达式鼓励你采用上一章中提到的行为参数化风格。最终结果就是你的代码变得更清晰、更灵活。比如,利用Lambda表达式,你可以更为简洁地自定义一个Comparator对象。

先前:

Comparator<Apple> byWeight = new Comparator<Apple>() { public int compare(Apple a1, Apple a2){ return a1.getWeight().compareTo(a2.getWeight()); } };

之后(用了Lambda表达式):

Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

不得不承认,代码看起来更清晰了!要是现在你觉得Lambda表达式看起来一头雾水的话也没关系,我们很快会一点点解释清楚的。现在,请注意你基本上只传递了比较两个苹果重量所真正需要的代码。看起来就像是只传递了compare方法的主体。你很快就会学到,你甚至还可以进一步简化代码。下一节会解释在哪里以及如何使用Lambda表达式。

我们刚刚展示给你的Lambda表达式有三个部分,如图3-1所示。



图 3-1 Lambda表达式由参数、箭头和主体组成

参数列表——这里它采用了Comparator中compare方法的参数,两个Apple。

箭头——箭头->把参数列表与Lambda主体分隔开。

Lambda主体——比较两个Apple的重量。表达式就是Lambda的返回值。



为了进一步说明,下面给出了Java 8中五个有效的Lambda表达式的例子。

代码清单 3-1 Java 8中有效的Lambda表达式



(String s) -> s.length() ←---- 第一个Lambda表达式具有一个String类型的参数并返回一个int。Lambda没有return语句,因为已经隐含了return (Apple a) -> a.getWeight() > 150 ←---- 第二个Lambda表达式有一个Apple类型的参数并返回一个boolean(苹果的重量是否超过150克) (int x, int y) -> { System.out.println("Result:"); System.out.println(x + y); } ←---- 第三个Lambda表达式具有两个int类型的参数而没有返回值(void返回)。注意Lambda表达式可以包含多行语句,这里是两行 () -> 42 ←---- 第四个Lambda 表达式没有参数,返回一个int (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) ←---- 第五个Lambda表达式具有两个Apple类型的参数,返回一个int:比较两个Apple的重量

Java语言设计者选择这样的语法,是因为C#和Scala等语言中的类似功能广受欢迎。JavaScript也有类似的语法。Lambda的基本语法是(被称为表达式–风格的Lambda)

(parameters) -> expression

或(请注意语句的花括号,这种Lambda经常被叫作块–风格的Lambda)

(parameters) -> { statements; }

你可以看到,Lambda表达式的语法很简单。做一下测验3.1,看看自己是不是理解了这个模式。

测验3.1:Lambda语法

根据上述语法规则,以下哪个不是有效的Lambda表达式?

(1) () -> {}

(2) () -> "Raoul"

(3) () -> {return "Mario";}

(4) (Integer i) -> return "Alan" + i;

(5) (String s) -> {"Iron Man";}

答案:只有(4)和(5)是无效的Lambda,其余都是有效的。详细解释如下。

(1) 这个Lambda没有参数,并返回void。它类似于主体为空的方法:public void run() {}。一个有趣的事实:这种Lambda也经常被叫作“汉堡型Lambda”。如果只从一边看,它的形状就像是两块圆面包组成的汉堡。

(2) 这个Lambda没有参数,并返回String作为表达式。

(3) 这个Lambda没有参数,并返回String(利用显式返回语句)。

(4) return是一个控制流语句。要使此Lambda有效,需要使用花括号,如下所示:

(Integer i) -> {return "Alan" + i;}

(5)“Iron Man”是一个表达式,不是一个语句。要使此Lambda有效,可以去除花括号和分号,如下所示:

(String s) -> "Iron Man"

或者如果你喜欢,可以使用显式返回语句,如下所示:

(String s) -> {return "Iron Man";}



表3-1提供了一些Lambda的例子和使用案例。

表 3-1 Lambda示例

使用案例

Lambda示例



布尔表达式



(List<pString> list) -> list.isEmpty()



创建对象



() -> new Apple(10)



消费一个对象



(Apple a) -> {

System.out.println(a.getWeight());

}



从一个对象中选择/抽取



(String s) -> s.length()



组合两个值



(int a, int b) -> a * b



比较两个对象



(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())





3.2 在哪里以及如何使用Lambda


现在你可能在想,在哪里可以使用Lambda表达式。在上一个例子中,你把Lambda赋给了一个Comparator<Apple>类型的变量。你也可以在上一章中实现的filter方法中使用Lambda:

List<Apple> greenApples = filter(inventory, (Apple a) -> GREEN.equals(a.getColor()));

那到底在哪里可以使用Lambda呢?你可以在函数式接口上使用Lambda表达式。在上面的代码中,可以把Lambda表达式作为第二个参数传给filter方法,因为它这里需要Predicate<T>,而这是一个函数式接口。如果这听起来太抽象,不要担心,现在我们就来详细解释这是什么意思,以及函数式接口是什么。





3.2.1 函数式接口


还记得你在第2章里,为了参数化filter方法的行为而创建的Predicate<T>接口吗?它就是一个函数式接口!为什么呢?因为Predicate仅仅定义了一个抽象方法:

public interface Predicate<T>{ boolean test (T t); }

一言以蔽之,函数式接口就是只定义一个抽象方法的接口。你已经知道了Java API中的一些其他函数式接口,如第2章中谈到的Comparator和Runnable。

public interface Comparator<T> { ←---- java.util.Comparator int compare(T o1, T o2); } public interface Runnable { ←---- java.lang.Runnable void run(); } public interface ActionListener extends EventListener { ←---- java.awt.event.ActionListener void actionPerformed(ActionEvent e); } public interface Callable<V> { ←---- java.util.concurrent.Callable V call() throws Exception; } public interface PrivilegedAction<T> { ←---- java.security.PrivilegedAction T run(); }

注意 你将会在第13章中看到,接口现在还可以拥有默认方法(即在类没有对方法进行实现时,其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。



为了检查你的理解程度,测验3.2将帮助你测试自己是否掌握了函数式接口的概念。

测验3.2:函数式接口

下面哪些接口是函数式接口?

public interface Adder { int add(int a, int b); } public interface SmartAdder extends Adder { int add(double a, double b); } public interface Nothing { }

答案:只有Adder是函数式接口。

SmartAdder不是函数式接口,因为它定义了两个叫作add的抽象方法(其中一个是从Adder那里继承来的)。

Nothing也不是函数式接口,因为它没有声明抽象方法。



用函数式接口可以干什么呢?Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)。你用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后再直接内联将它实例化。下面的代码是有效的,因为Runnable是只定义了一个抽象方法run的函数式接口:

Runnable r1 = () -> System.out.println("Hello World 1"); ←---- 使用Lambda Runnable r2 = new Runnable(){ ←---- 使用匿名类 public void run(){ System.out.println("Hello World 2"); } }; public static void process(Runnable r){ r.run(); } process(r1); ←---- 打印“Hello World 1” process(r2); ←---- 打印“Hello World 2” process(() -> System.out.println("Hello World 3")); ←---- 利用直接传递的Lambda打印“Hello World 3”





3.2.2 函数描述符


函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作函数描述符。例如,Runnable接口可以看作一个什么也不接受什么也不返回(void)的函数的签名,因为它只有一个叫作run的抽象方法,这个方法什么也不接受,什么也不返回(void)。1

1Scala等语言的类型系统提供显式类型标注,可以描述函数的类型(称为“函数类型”)。Java重用了函数式接口提供的标准类型,并将其映射成一种形式的函数类型。

本章中使用了一个特殊表示法来描述Lambda和函数式接口的签名。() -> void代表了参数列表为空且返回void的函数。这正是Runnable接口所代表的。再举一个例子,(Apple, Apple) -> int代表接受两个Apple作为参数且返回int的函数。3.4节和本章后面的表3-2中提供了关于函数描述符的更多信息。

你可能已经在想,Lambda表达式是怎么做类型检查的。3.5节会详细介绍编译器是如何检查Lambda在给定上下文中是否有效的。现在,只要知道Lambda表达式可以被赋给一个变量,或传递给一个接受函数式接口作为参数的方法就好了,当然这个Lambda表达式的签名要和函数式接口的抽象方法一样。比如,在之前的例子里,你可以像下面这样直接把一个Lambda传给process方法:

public void process(Runnable r){ r.run(); } process(() -> System.out.println("This is awesome!!"));

此段代码执行时将打印“This is awesome!!”。Lambda表达式()-> System.out.println ("This is awesome!!")不接受参数且返回void。这恰恰是Runnable接口中run方法的签名。

Lambda及空方法调用

虽然下面这种Lambda表达式调用看起来很奇怪,但是合法的:

process(() -> System.out.println("This is awesome"));

System.out.println返回void,所以很明显这不是一个表达式!为什么不像下面这样用花括号环绕方法体呢?

process(() -> { System.out.println("This is awesome"); });

结果表明,方法调用的返回值为空时,Java语言规范有一条特殊的规定。这种情况下,你不需要使用括号环绕返回值为空的单行方法调用。



你可能会想:“为什么在只需要函数式接口的时候才可以传递Lambda呢?”语言的设计者也考虑过其他办法,例如给Java添加函数类型(有点儿像我们介绍描述Lambda表达式签名时的特殊表示法,第20章和第21章会继续讨论这个问题)。但是他们选择了现在这种方式,因为这种方式很自然,并且能避免让语言变得更复杂。此外,大多数Java程序员都已经熟悉了带有一个抽象方法的接口(譬如进行事件处理时)。然而,最重要的原因在于Java 8之前函数式接口就已经得到了广泛应用。这意味着,采用这种方式,遗留代码迁移到Lambda表达式的迁移路径会比较顺畅。实际上,你已经使用了函数式接口,像Comparator、Runnable,甚至你自己的接口,如果只定义了一个抽象方法,都算是函数式接口。你可以使用Lambda表达式替换他们,而无须修改你的API。试试看测验3.3,测试一下你对哪里可以使用Lambda这个知识点的掌握情况。

测验3.3:在哪里可以使用Lambda

以下哪些是使用Lambda表达式的有效方式?

(1)

execute(() -> {}); public void execute(Runnable r){ r.run(); }

(2)

public Callable<String> fetch() { return () -> "Tricky example ;-)"; }

(3)

Predicate<Apple> p = (Apple a) -> a.getWeight();

答案:只有(1)和(2)是有效的。

第(1)个例子有效,是因为Lambda() -> {}具有签名() -> void,这和Runnable中的抽象方法run的签名相匹配。请注意,此代码运行后什么都不会做,因为Lambda是空的!

第(2)个例子也是有效的。事实上,fetch方法的返回类型是Callable<String>。Callable<String>基本上就定义了一个方法,签名是() -> String,其中T被String代替了。因为Lambda() -> "Trickyexample;-)"的签名是() -> String,所以在这个上下文中可以使用Lambda。

第(3)个例子无效,因为Lambda表达式(Apple a) -> a.getWeight()的签名是(Apple) -> Integer,这和Predicate<Apple>: (Apple) -> boolean中定义的test方法的签名不同。





@FunctionalInterface又是怎么回事?

如果你去看看新的Java API,会发现函数式接口带有@FunctionalInterface的标注(3.4节中会深入研究函数式接口,并会给出一个长长的列表)。这个标注用于表示该接口会设计成一个函数式接口,因此对文档来说非常有用。此外,如果你用@FunctionalInterface定义了一个接口,而它不是函数式接口的话,编译器将返回一个提示原因的错误。例如,错误消息可能是“Multiple non-overriding abstract methods found in interface Foo”,表明存在多个抽象方法。请注意,@FunctionalInterface不是必需的,但对于为此设计的接口而言,使用它是比较好的做法。它就像是@Override标注表示方法被重写了。





3.3 把Lambda付诸实践:环绕执行模式


让我们通过一个例子,看看在实践中如何利用Lambda和行为参数化来让代码更为灵活,更为简洁。资源处理(例如处理文件或数据库)时一个常见的模式就是打开一个资源,做一些处理,然后关闭资源。这个设置和清理阶段总是很类似,并且会围绕着执行处理的那些重要代码。这就是所谓的环绕执行(execute around)模式,如图3-2所示。例如,在以下代码中,加粗显示的就是从一个文件中读取一行所需的模板代码(注意你使用了Java 7中的带资源的try语句,它已经简化了代码,因为你不需要显式地关闭资源了):

public String processFile() throws IOException { try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) { return br.readLine(); ←----这就是做有用工作的那行代码 } }



图 3-2 任务A和任务B周围都环绕着进行准备/清理的同一段冗余代码





3.3.1 第1步:记得行为参数化


现在这段代码是有局限的。你只能读文件的第一行。如果你想要返回头两行,甚至是返回使用最频繁的词,该怎么办呢?在理想的情况下,你要重用执行设置和清理的代码,并告诉processFile方法对文件执行不同的操作。这听起来是不是很耳熟?是的,你需要把processFile的行为参数化。你需要一种方法把行为传递给processFile,以便它可以利用BufferedReader执行不同的行为。

传递行为正是Lambda的拿手好戏。那要是想一次读两行,这个新的processFile方法看起来又该是什么样的呢?基本上,你需要一个接受BufferedReader并返回String的Lambda。例如,下面就是从BufferedReader中打印两行的写法:

String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());





3.3.2 第2步:使用函数式接口来传递行为


前面解释过了,Lambda仅可用于上下文是函数式接口的情况。你需要创建一个能匹配BufferedReader -> String,还可以抛出IOException异常的接口。让我们把这一接口叫作BufferedReaderProcessor吧。

@FunctionalInterface public interface BufferedReaderProcessor { String process(BufferedReader b) throws IOException; }

现在你就可以把这个接口作为新的processFile方法的参数了:

public String processFile(BufferedReaderProcessor p) throws IOException { ... }





3.3.3 第3步:执行一个行为


任何BufferedReader -> String形式的Lambda都可以作为参数来传递,因为它们符合BufferedReaderProcessor接口中定义的process方法的签名。现在你只需要一种方法在processFile主体内执行Lambda所代表的代码。请记住,Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。因此,你可以在processFile主体内,对得到的BufferedReaderProcessor对象调用process方法执行处理:

public String processFile(BufferedReaderProcessor p) throws IOException { try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) { return p.process(br); ←---- 处理BufferedReader对象 } }





3.3.4 第4步:传递Lambda


现在你就可以通过传递不同的Lambda来重用processFile方法,并以不同的方式处理文件了。

处理一行:

String oneLine = processFile((BufferedReader br) -> br.readLine());

处理两行:

String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());

图3-3总结了所采取的使pocessFile方法更灵活的四个步骤。



图 3-3 应用环绕执行模式所采取的四个步骤

我们已经展示了如何利用函数式接口来传递Lambda,但你还是得定义自己的接口。下一节会探讨Java 8中加入的新接口,你可以重用它来传递多个不同的Lambda。





3.4 使用函数式接口


就像你在3.2.1节中学到的,函数式接口定义且只定义了一个抽象方法。函数式接口很有用,因为抽象方法的签名可以描述Lambda表达式的签名。函数式接口的抽象方法的签名称为函数描述符。所以为了应用不同的Lambda表达式,你需要一套能够描述常见函数描述符的函数式接口。Java API中已经有了几个函数式接口,比如你在3.2节中见到的Comparator、Runnable和Callable。

Java 8的库设计师帮你在java.util.function包中引入了几个新的函数式接口。我们接下来会介绍Predicate、Consumer和Function,更完整的列表可见本节结尾处的表3-2。





3.4.1 Predicate


java.util.function.Predicate<T>接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。这恰恰和你先前创建的一样,现在就可以直接使用了。在你需要表示一个涉及类型T的布尔表达式时,就可以使用这个接口。比如,你可以定义一个接受String对象的Lambda表达式,如下所示。

代码清单 3-2 使用Predicate



@FunctionalInterface public interface Predicate<T> { boolean test(T t); } public <T> List<T> filter(List<T> list, Predicate<T> p) { List<T> results = new ArrayList<>(); for(T t: list) { if(p.test(t)) { results.add(t); } } return results; } Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty(); List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);

如果你去查Predicate接口的Javadoc说明,可能会注意到诸如and和or等其他方法。现在你不用太计较这些,3.8节会讨论。





3.4.2 Consumer


java.util.function.Consumer<T>接口定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回(void)。你如果需要访问类型T的对象,并对其执行某些操作,就可以使用这个接口。比如,你可以用它来创建一个forEach方法,接受一个Integers的列表,并对其中每个元素执行操作。在下面的代码中,你就可以使用这个forEach方法,并配合Lambda来打印列表中的所有元素。

代码清单 3-3 使用Consumer



@FunctionalInterface public interface Consumer<T>{ void accept(T t); } public <T> void forEach(List<T> list, Consumer<T> c){ for(T i: list){ c.accept(i); } } forEach( Arrays.asList(1,2,3,4,5), (Integer i) -> System.out.println(i) ←----Lambda是Consumer中accept方法的实现 );





3.4.3 Function


java.util.function.Function<T, R>接口定义了一个叫作apply的抽象方法,它接受泛型T的对象,并返回一个泛型R的对象。如果你需要定义一个Lambda,将输入对象的信息映射到输出,就可以使用这个接口(比如提取苹果的重量,或把字符串映射为它的长度)。在下面的代码中,我们向你展示如何利用它来创建一个map方法,以将一个String列表映射到包含每个String长度的Integer列表。

代码清单 3-4 使用Function



@FunctionalInterface public interface Function<T, R> { R apply(T t); } public <T, R> List<R> map(List<T> list, Function<T, R> f) { List<R> result = new ArrayList<>(); for(T t: list) { result.add(f.apply(t)); } return result; } // [7, 2, 6] List<Integer> l = map( Arrays.asList("lambdas", "in", "action"), (String s) -> s.length() ←----Lambda是Function接口的apply方法的实现 );





基本类型特化


我们介绍了三个泛型函数式接口:Predicate<T>、Consumer<T>和Function<T,R>。还有些函数式接口专为某些类型而设计。

回顾一下:Java类型要么是引用类型(比如Byte、Integer、Object、List),要么是基本类型(比如int、double、byte、char)。但是泛型(比如Consumer<T>中的T)只能绑定到引用类型。这是由泛型内部的实现方式造成的。2 因此,在Java里有一个将基本类型转换为对应的引用类型的机制。这个机制叫作装箱(boxing)。相反的操作,也就是将引用类型转换为对应的基本类型,叫作拆箱(unboxing)。Java还有一个自动装箱机制来帮助程序员执行这一任务:装箱和拆箱操作是自动完成的。比如,这就是为什么下面的代码是有效的(一个int被装箱成为Integer):

2C#等其他语言没有这一限制。Scala等语言只有引用类型。第20章会再次探讨这个问题。

List<Integer> list = new ArrayList<>(); for (int i = 300; i < 400; i++){ list.add(i); }

但这在性能方面是要付出代价的。装箱后的值本质上就是把基本类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的基本值。

Java 8为前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是基本类型时避免自动装箱的操作。比如,在下面的代码中,使用IntPredicate就避免了对值1000进行装箱操作,但要是用Predicate<Integer>就会把参数1000装箱到一个Integer对象中:

public interface IntPredicate { boolean test(int t); } IntPredicate evenNumbers = (int i) -> i % 2 == 0; evenNumbers.test(1000); ←---- true(无装箱) Predicate<Integer> oddNumbers = (Integer i) -> i % 2 != 0; oddNumbers.test(1000); ←---- false(装箱)

一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的基本类型前缀,比如DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction等。Function接口还有针对输出参数类型的变种:ToIntFunction<T>、IntToDoubleFunction等。

表3-2总结了Java API中最常用的函数式接口,它们的函数描述符及其基本类型特化。请记住这个集合只是一个启始集。如果有需要,你完全可以设计一个自己的基本类型特化(测验3.7中的TriFunction就是出于这个目的而设计的)。此外,创建你自己的接口,让接口的名字反映其在领域中的功能,还能帮助程序员理解代码逻辑,同时也便于程序的维护。请记住,标记符(T, U) -> R展示的是该怎样理解一个函数描述符。箭头左侧代表了参数的类型,右侧代表了返回结果的类型。这儿它代表的是一个函数,具有两个参数,分别为泛型T和U,返回类型为R。

表 3-2 Java 8中的常用函数式接口

函数式接口

Predicate<T>

Consumer<T>



Predicate<T>

T -> boolean



IntPredicate,

LongPredicate,

DoublePredicate



Consumer<T>

T -> void



IntConsumer,

LongConsumer,

DoubleConsumer



Function<T, R>

T -> R



IntFunction<R>,

IntToDoubleFunction,

IntToLongFunction,

LongFunction<R>,

LongToDoubleFunction,

LongToIntFunction,

DoubleFunction<R>,

DoubleToIntFunction,

DoubleToLongFunction,

ToIntFunction<T>,

ToDoubleFunction<T>,

ToLongFunction<T>



Supplier<T>

() -> T



BooleanSupplier, IntSupplier,

LongSupplier, DoubleSupplier



UnaryOperator<T>

T -> T



IntUnaryOperator,

LongUnaryOperator,

DoubleUnaryOperator



BinaryOperator<T>

(T, T) -> T



IntBinaryOperator,

LongBinaryOperator,

DoubleBinaryOperator



BiPredicate<T, U>

(T, U) -> boolean





BiConsumer<T, U>

(T, U) -> void



ObjIntConsumer<T>,

ObjLongConsumer<T>,

ObjDoubleConsumer<T>



BiFunction<T, U, R>

(T, U) -> R



ToIntBiFunction<T, U>,

ToLongBiFunction<T, U>,

ToDoubleBiFunction<T, U>



你现在已经看到了很多函数式接口,可以用于描述各种Lambda表达式的签名。为了检验你的理解程度,试试测验3.4。

测验3.4:函数式接口

对于下列函数描述符(即Lambda表达式的签名),你会使用哪些函数式接口?在表3-2中可以找到大部分答案。作为进一步练习,请构造一个可以利用这些函数式接口的有效Lambda表达式:

(1) T -> R

(2) (int, int) -> int

(3) T -> void

(4) () -> T

(5) (T, U) -> R

答案:(1) Function<T, R>不错。它一般用于将类型T的对象转换为类型R的对象(比如Function<Apple, Integer>用来提取苹果的重量)。

(2) IntBinaryOperator具有唯一一个抽象方法——applyAsInt,代表的函数描述符是(int, int) -> int。

(3) Consumer<T>具有唯一一个抽象方法——accept,代表的函数描述符是T -> void。

(4) Supplier<T>具有唯一一个抽象方法——get,代表的函数描述符是()-> T。

(5) BiFunction<T, U, R>具有唯一一个抽象方法——apply,代表的函数描述符是(T, U) -> R。



为了总结关于函数式接口和Lambda的讨论,表3-3总结了一些使用案例、Lambda的例子,以及可以使用的函数式接口。

表 3-3 Lambda及函数式接口的例子

使用案例

Lambda的例子

对应的函数式接口



布尔表达式

(List<String> list) -> list.isEmpty()

Predicate<List<String>>



创建对象

() -> new Apple(10)

Supplier<Apple>



消费一个对象

(Apple a) ->

System.out.println(a.getWeight())

Consumer<Apple>



从一个对象中选择/提取

(String s) -> s.length()

Function<String, Integer> or ToIntFunction<String>



合并两个值

(int a, int b) -> a * b

IntBinaryOperator



比较两个对象

(Apple a1, Apple a2) ->

a1.getWeight().compareTo(a2.getWeight())

Comparator<Apple> or

BiFunction<Apple, Apple, Integer> or ToIntBiFunction<Apple, Apple>



异常、Lambda,还有函数式接口又是怎么回事?

请注意,这些函数式接口中的任何一个都不允许抛出受检异常(checked exception)。如果你需要Lambda表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda包在一个try/catch块中。

比如,3.3节介绍过一个新的函数式接口BufferedReaderProcessor,它显式声明了一个IOException:

@FunctionalInterface public interface BufferedReaderProcessor { String process(BufferedReader b) throws IOException; } BufferedReaderProcessor p = (BufferedReader br) -> br.readLine();

但是你可能是在使用一个接受函数式接口的API,比如Function<T, R>,没有办法自己创建一个(你会在下一章看到,Stream API中大量使用了表3-2中的函数式接口)。这种情况下,你可以显式捕捉受检异常:

Function<BufferedReader, String> f = (BufferedReader b) -> { try {· return b.readLine(); } catch(IOException e) { throw new RuntimeException(e); } };



现在你知道如何创建Lambda,在哪里以及如何使用它们了。接下来我们会介绍一些更高级的细节:编译器如何对Lambda做类型检查,以及你应当了解的规则,诸如Lambda在自身内部引用局部变量,还有和void兼容的Lambda等。你无须立即就充分理解下一节的内容,可以留待日后再看,接着往下学习3.6节讲的方法引用就可以了。





3.5 类型检查、类型推断以及限制


当我们第一次提到Lambda表达式时,说它可以为函数式接口生成一个实例。然而,Lambda表达式本身并不包含它在实现哪个函数式接口的信息。为了全面了解Lambda表达式,你应该知道Lambda的实际类型是什么。





3.5.1 类型检查


Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。让我们通过一个例子,看看当你使用Lambda表达式时背后发生了什么。图3-4概述了下列代码的类型检查过程。

List<Apple> heavierThan150g = filter(inventory, (Apple apple) -> apple.getWeight() > 150);

类型检查过程分解如下。

第一,你要找出filter方法的声明。

第二,要求它是Predicate<Apple>(目标类型)对象的第二个正式参数。

第三,Predicate<Apple>是一个函数式接口,定义了一个叫作test的抽象方法。

第四,test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean。

第五,filter的任何实际参数都必须匹配这个要求。





图 3-4 解读Lambda表达式的类型检查过程

这段代码是有效的,因为我们所传递的Lambda表达式也同样接受Apple为参数,并返回一个boolean。请注意,如果Lambda表达式抛出一个异常,那么抽象方法所声明的throws语句也必须与之匹配。





3.5.2 同样的Lambda,不同的函数式接口


有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来,只要它们的抽象方法签名能够兼容。比如,前面提到的Callable和PrivilegedAction,这两个接口都代表着什么也不接受且返回一个泛型T的函数。因此,下面两个赋值是有效的:

Callable<Integer> c = () -> 42; PrivilegedAction<Integer> p = () -> 42;

这里,第一个赋值的目标类型是Callable<Integer>,第二个赋值的目标类型是PrivilegedAction<Integer>。

在表3-3中展示了一个类似的例子,同一个Lambda可用于多个不同的函数式接口:

Comparator<Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); ToIntBiFunction<Apple, Apple> c2 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); BiFunction<Apple, Apple, Integer> c3 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

菱形运算符

那些熟悉Java演变的人会记得,Java 7中已经引入了菱形运算符(<>),利用泛型推断从上下文推断类型的思想(这一思想甚至可以追溯到更早的泛型方法)。一个类实例表达式可以出现在两个或更多不同的上下文中,并会像下面这样推断出适当的类型参数:

List<String> listOfStrings = new ArrayList<>(); List<Integer> listOfIntegers = new ArrayList<>();





特殊的void兼容规则

如果一个Lambda的主体是一个语句表达式,它就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)。例如,以下两行都是合法的,尽管List的add方法返回了一个boolean,而不是Consumer上下文(T -> void)所要求的void:

// Predicate返回了一个boolean Predicate<String> p = (String s) -> list.add(s); // Consumer返回了一个void Consumer<String> b = (String s) -> list.add(s);



到现在为止,你应该能够很好地理解在什么时候以及在哪里可以使用Lambda表达式了。它们可以从赋值的上下文、方法调用的上下文(参数和返回值),以及类型转换的上下文中获得目标类型。为了检验你的掌握情况,请试试测验3.5。

测验3.5:类型检查——为什么下面的代码不能编译呢?

你该如何解决这个问题呢?

Object o = () -> { System.out.println("Tricky example"); };

答案:Lambda表达式的上下文是Object(目标类型)。但Object不是一个函数式接口。为了解决这个问题,你可以把目标类型改成Runnable,它的函数描述符是() -> void:

Runnable r = () -> { System.out.println("Tricky example"); };

你还可以通过强制类型转换将Lambda表达式转换成Runnable,显式地生成一个目标类型,以这种方式来修复这个问题:

Object o = (Runnable) () -> { System.out.println("Tricky example"); };

处理方法重载时,如果两个不同的函数式接口却有着同样的函数描述符,使用这个技巧有立竿见影的效果。到底该选择使用哪一个方法签名呢?为了消除这种显式的二义性,你可以对Lamda进行强制类型转换。

譬如,下面这段代码中,方法调用execute( () -> {} )使用了execute方法,不过它存在着二义性,因为Runnable和Action接口中都提供了同样的函数描述符:

public void execute(Runnable runnable) { runnable.run(); } public void execute(Action<T> action) { action.act(); } @FunctionalInterface interface Action { void act(); }

然而,通过强制类型转换表达式,这种显式的二义性被消除了:

execute((Action) () -> { });



你已经了解如何利用目标类型来判断某个Lambda是否适用于某个特定的上下文。其实,它还可以用来做一些别的事:推断Lambda参数的类型。





3.5.3 类型推断


你还可以进一步简化你的代码。Java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。这样做的好处在于,编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda语法中省去标注参数类型。换句话说,Java编译器会像下面这样推断Lambda的参数类型:3

3请注意,当Lambda仅有一个类型需要推断的参数时,参数名称两边的括号也可以省略。

List<Apple> greenApples = filter(inventory, apple -> GREEN.equals(apple.getColor())); ←---- 参apple没有显式类型

Lambda表达式有多个参数,代码可读性的好处就更为明显。例如,你可以这样来创建一个Comparator对象:

Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); ←---- 没有类型推断 Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight()); ←---- 有类型推断

请注意,有时候显式写出类型更易读,有时候去掉它们更易读。没有什么法则说哪种更好,对于如何让代码更易读,程序员必须做出自己的选择。





3.5.4 使用局部变量


我们迄今为止所介绍的所有Lambda表达式都只用到了其主体里面的参数。但Lambda表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。它们被称作捕获Lambda。例如,下面的Lambda捕获了portNumber变量:

int portNumber = 1337; Runnable r = () -> System.out.println(portNumber);

尽管如此,还有一点点小麻烦:关于能对这些变量做什么有一些限制。Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为final,或事实上是final。换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以被看作捕获最终局部变量this。) 例如,下面的代码无法编译,因为portNumber变量被赋值两次:

int portNumber = 1337; Runnable r = () -> System.out.println(portNumber); ←---- 错误:Lambda表达式引用的局部变量必须是最终的(final)或事实上最终的portNumber = 31337;





对局部变量的限制


你可能会问自己,为什么局部变量有这些限制。第一,实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问基本变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了这个限制。

第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式(我们会在以后的各章中解释,这种模式会阻碍很容易做到的并行处理)。

闭包

你可能已经听说过闭包(closure,不要和Clojure编程语言混淆)这个词,可能会想Lambda是否满足闭包的定义。用科学的说法来说,闭包就是一个函数的实例,且它可以无限制地访问那个函数的非本地变量。例如,闭包可以作为参数传递给另一个函数。它也可以访问和修改其作用域之外的变量。现在,Java 8的Lambda和匿名类可以做类似于闭包的事情:它们可以作为参数传递给方法,并且可以访问其作用域之外的变量。但有一个限制:它们不能修改定义Lambda的方法的局部变量的内容。这些变量必须是隐式最终的。可以认为Lambda是对值封闭,而不是对变量封闭。如前所述,这种限制存在的原因在于局部变量保存在栈上,并且隐式表示它们仅限于其所在线程。如果允许捕获可改变的局部变量,就会引发造成线程不安全的新的可能性,而这是我们不想看到的(实例变量可以,因为它们保存在堆中,而堆是在线程之间共享的)。



现在,我们来介绍你会在Java 8代码中看到的另一个功能:方法引用。可以把它们视为某些Lambda的快捷写法。





3.6 方法引用


方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。在一些情况下,比起使用Lambda表达式,它们似乎更易读,感觉也更自然。下面就是我们借助更新的Java 8 API(3.7节会详细讨论),用方法引用写的一个排序的例子:

先前:

inventory.sort((Apple a1, Apple a2) a1.getWeight().compareTo(a2.getWeight()));

之后(使用方法引用和java.util.Comparator.comparing):

inventory.sort(comparing(Apple::getWeight)); ←---- 你的第一个方法引用

不用担心新的语法及其工作原理,接下来的几节将会对此进行介绍。





3.6.1 管中窥豹


你为什么应该关注方法引用?方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法。它的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。事实上,方法引用就是让你根据已有的方法实现来创建Lambda表达式。但是,显式地指明方法的名称,你的代码的可读性会更好。它是如何工作的呢?当你需要使用方法引用时,目标引用放在分隔符::前,方法的名称放在后面。例如,Apple::getWeight就是引用了Apple类中定义的方法getWeight。请记住,getWeight后面不需要括号,因为你没有实际调用这个方法,只是引用了它的名称。方法引用就是Lambda表达式(Apple apple) -> apple.getWeight()的快捷写法。表3-4给出了Java 8中方法引用的其他一些例子。

表 3-4 Lambda及其等效方法引用的例子

Lambda

等效的方法引用



(Apple apple) -> apple.getWeight()

Apple::getWeight



() -> Thread.currentThread().dumpStack()

Thread.currentThread()::dumpStack



(str, i) -> str.substring(i)

String::substring



(String s) -> System.out.println(s) (String s) -> this.isValidName(s)

System.out::println this::isValidName



你可以把方法引用看作针对仅仅涉及单一方法的Lambda的语法糖,因为你表达同样的事情时要写的代码更少了。





如何构建方法引用


方法引用主要有三类。

(1) 指向静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt)。

(2) 指向任意类型实例方法的方法引用(例如String的length方法,写作String::length)。

(3) 指向现存对象或表达式实例方法的方法引用(假设你有一个局部变量expensive Transaction保存了Transaction类型的对象,它提供了实例方法getValue,那你就可以这么写expensiveTransaction::getValue)。

第二种和第三种方法引用可能乍看起来有点儿晕。第二种方法引用的思想是你在引用一个对象的方法,譬如String::length,而这个对象是Lambda表达式的一个参数。举个例子,Lambda表达式(String s) -> s.toUppeCase()可以重写成String::toUpperCase。而第三种方法引用主要用在你需要在Lambda中调用一个现存外部对象的方法时。例如,Lambda表达式()->expensiveTransaction.getValue()可以重写为expensiveTransaction::getValue。第三种方法引用在你需要传递一个私有辅助方法时特别有用。譬如,你定义了一个辅助方法isValidName:

private boolean isValidName(String string) { return Character.isUpperCase(string.charAt(0)); }

你可以借助方法引用,在Predicate<String>的上下文中传递该方法:

filter(words, this::isValidName)

为了帮助你消化这些新知识,我们准备了一份将Lambda表达式重构为等价方法引用的简易速查表,如图3-5所示。



图 3-5 为三种不同类型的Lambda表达式构建方法引用的办法

请注意,构造函数、数组构造函数以及父类调用(super-call)的方法引用形式比较特殊。举一个方法引用的具体例子。假设你想要忽略大小写对一个由字符串组成的List排序。List的sort方法需要一个Comparator作为参数。前文介绍过,Comparator使用(T, T) -> int这样的签名作为函数描述符。你可以利用String类中的compareToIgnoreCase方法来定义一个Lambda表达式(注意compareToIgnoreCase是String类中预先定义的)。

List<String> str = Arrays.asList("a","b","A","B"); str.sort((s1, s2) -> s1.compareToIgnoreCase(s2));

Lambda表达式的签名与Comparator的函数描述符兼容。利用前面所述的方法,这个例子可以用方法引用改写成下面的样子,这样代码更加简洁了:

List<String> str = Arrays.asList("a","b","A","B"); str.sort(String::compareToIgnoreCase);

请注意,编译器会进行一种与Lambda表达式类似的类型检查过程,来确定对于给定的函数式接口,这个方法引用是否有效:方法引用的签名必须和上下文类型匹配。

为了检验你对方法引用的理解程度,试试测验3.6吧!

测验3.6:方法引用

下列Lambda表达式的等效方法引用是什么?

(1) ToIntFunction<String> stringToInt = (String s) -> Integer.parseInt(s);

(2) BiPredicate<List<String>, String> contains = (list, element) -> list.contains(element);

(3) Predicate<String> startsWithNumber = (String string) -> this .startsWithNumber(string);

答案:(1) 这个Lambda表达式将其参数传给了Integer的静态方法parseInt。这种方法接受一个需要解析的String,并返回一个Integer。因此,可以使用图3-5中的办法➊(Lambda表达式调用静态方法)来重写Lambda表达式,如下所示:

ToIntFunction<String> stringToInt = Integer::parseInt;

(2) 这个Lambda使用其第一个参数,调用其contains方法。由于第一个参数是List类型的,因此你可以使用图3-5中的办法➋,如下所示:

BiPredicate<List<String>, String> contains = List::contains;

这是因为,目标类型描述的函数描述符是(List<String>,String) -> boolean,而List::contains可以被解包成这个函数描述符。

(3) 这种“表达式–风格”的Lambda会调用一个私有方法。你可以使用图3-5中的办法❸,如下所示:

Predicate<String> startsWithNumber = this::startsWithNumber



到目前为止,我们只展示了如何利用现有的方法实现和如何创建方法引用。但是你也可以对类的构造函数做类似的事情。





3.6.2 构造函数引用


对于一个现有构造函数,你可以利用它的名称和关键字new来创建它的一个引用:ClassName::new。它的功能与指向静态方法的引用类似。例如,假设有一个构造函数没有参数。它适合Supplier的签名() -> Apple。你可以这样做:

Supplier<Apple> c1 = Apple::new; ←---- 构造函数引用指向默认的Apple()构造函数 Apple a1 = c1.get(); ←---- 调用Supplier的get方法将产生一个新的Apple

这就等价于:

Supplier<Apple> c1 = () -> new Apple(); ←---- 利用默认构造函数创建Apple的Lambda 表达式 Apple a1 = c1.get(); ←---- 调用Supplier的get方法将产生一个新的Apple

如果你的构造函数的签名是Apple(Integer weight),那么它就适合Function接口的签名,于是你可以这样写:

Function<Integer, Apple> c2 = Apple::new; ←---- 指向Apple(Integer weight)的构造函数引用 Apple a2 = c2.apply(110); ←---- 调用该Function函数的apply方法,并给出要求的重量,将产生一个Apple

这就等价于:

Function<Integer, Apple> c2 = (weight) -> new Apple(weight); ←---- 用要求的重量创建一个Apple的Lambda表达式 Apple a2 = c2.apply(110); ←---- 调用该Function函数的apply方法,并给出要求的重量,将产生一个新的Apple对象

在下面的代码中,一个由Integer构成的List中的每个元素都通过前面定义的类似的map方法传递给了Apple的构造函数,得到了一个具有不同重量苹果的List:

List<Integer> weights = Arrays.asList(7, 3, 4, 10); List<Apple> apples = map(weights, Apple::new); ←---- 将构造函数引用传递给map方法 public List<Apple> map(List<Integer> list, Function<Integer, Apple> f) { List<Apple> result = new ArrayList<>(); for(Integer i: list) { result.add(f.apply(i)); } return result; }

如果你有一个具有两个参数的构造函数Apple(String color, Integer weight),那么它就适合BiFunction接口的签名,于是你可以这样写:

BiFunction<Color, Integer, Apple> c3 = Apple::new; ←---- 指向Apple(String color, Integer weight)的构造函数引用 Apple a3 = c3.apply(GREEN, 110); ←---- 调用该BiFunction函数的apply方法,并给出要求的颜色和重量,将产生一个新的Apple对象

这就等价于:

BiFunction<String, Integer, Apple> c3 = ←---- 用要求的颜色和重量创建一个Apple的Lambda表达式 (color, weight) -> new Apple(color, weight); Apple a3 = c3.apply(GREEN, 110); ←---- 调用该BiFunction函数的apply方法,并给出要求的颜色和重量,将产生一个新的Apple对象

不将构造函数实例化却能够引用它,这个功能有一些有趣的应用。例如,你可以使用Map来将构造函数映射到字符串值。你可以创建一个giveMeFruit方法,给它一个String和一个Integer,它就可以创建出不同重量的各种水果:

static Map<String, Function<Integer, Fruit>> map = new HashMap<>(); static { map.put("apple", Apple::new); map.put("orange", Orange::new); // etc... } public static Fruit giveMeFruit(String fruit, Integer weight){ return map.get(fruit.toLowerCase()) ←---- 你用map得到了一个Function<Integer, Fruit> .apply(weight); ←---- 用Integer类型的weight参数调用Function的apply()方法将提供所要求的Fruit }

为了检验你对方法和构造函数引用的理解程度,试试测验3.7吧!

测验3.7:构造函数引用

你已经看到了如何将有零个、一个、两个参数的构造函数转变为构造函数引用。那要怎么样才能对具有三个参数的构造函数,比如RGB(int, int, int),使用构造函数引用呢?

答案:你看,构造函数引用的语法是ClassName::new,那么在这个例子里面就是RGB::new。但是你需要与构造函数引用的签名匹配的函数式接口。由于语言本身并没有提供这样的函数式接口,因此你可以自己创建一个:

public interface TriFunction<T, U, V, R> { R apply(T t, U u, V v); }

现在你可以像下面这样使用构造函数引用了:

TriFunction<Integer, Integer, Integer, RGB> colorFactory = RGB::new;



我们讲了好多新内容:Lambda、函数式接口和方法引用。下一节会把这一切付诸实践!





3.7 Lambda和方法引用实战


为了给这一章还有我们讨论的所有关于Lambda的内容收个尾,我们需要继续研究开始的那个问题——用不同的排序策略给一个Apple列表排序,并需要展示如何把一个原始粗暴的解决方案转变得更为简明。这会用到书中迄今讲到的所有概念和功能:行为参数化、匿名类、Lambda表达式和方法引用。我们想要实现的最终解决方案是这样的:

inventory.sort(comparing(Apple::getWeight));





3.7.1 第1步:传递代码


你很幸运,Java 8 API已经为你提供了一个List可用的sort方法,你不用自己去实现它。那么最困难的部分已经搞定了!但是,如何把排序策略传递给sort方法呢?你看,sort方法的签名是这样的:

void sort(Comparator<? super E> c)

它需要一个Comparator对象来比较两个Apple!这就是在Java中传递策略的方式:它们必须包裹在一个对象里。我们说sort的行为被参数化了:传递给它的排序策略不同,其行为也会不同。

你的第一个解决方案看上去是这样的:

public class AppleComparator implements Comparator<Apple> { public int compare(Apple a1, Apple a2){ return a1.getWeight().compareTo(a2.getWeight()); } } inventory.sort(new AppleComparator());





3.7.2 第2步:使用匿名类


你在前面看到了,你可以使用匿名类来改进解决方案,而不是实现一个Comparator却只实例化一次:

inventory.sort(new Comparator<Apple>() { public int compare(Apple a1, Apple a2){ return a1.getWeight().compareTo(a2.getWeight()); } });





3.7.3 第3步:使用Lambda表达式


但你的解决方案仍然挺啰唆的。Java 8引入了Lambda表达式,它提供了一种轻量级语法来实现相同的目标:传递代码。你看到了,在需要函数式接口的地方可以使用Lambda表达式。回顾一下:函数式接口就是仅仅定义一个抽象方法的接口。抽象方法的签名(称为函数描述符)描述了Lambda表达式的签名。在这个例子里,Comparator代表了函数描述符(T, T) -> int。因为你用的是苹果,所以它具体代表的就是(Apple, Apple) -> int。改进后的新解决方案看上去就是这样的了:

inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) );

前面解释过了,Java编译器可以根据Lambda出现的上下文来推断Lambda表达式参数的类型。那么你的解决方案就可以重写成这样:

inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));

你的代码还能变得更易读一点吗?Comparator具有一个叫作comparing的静态辅助方法,它可以接受一个Function来提取Comparable键值,并生成一个Comparator对象(第13章会解释为什么接口可以有静态方法)。它可以像下面这样用(注意你现在传递的Lambda只有一个参数,Lambda说明了如何从Apple中提取需要比较的键值):

Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());

现在你可以把代码再改得紧凑一点了:

import static java.util.Comparator.comparing; inventory.sort(comparing(apple -> apple.getWeight()));





3.7.4 第4步:使用方法引用


前面解释过,方法引用就是替代那些转发参数的Lambda表达式的语法糖。你可以用方法引用让你的代码更简洁(假设你静态导入了java.util.Comparator.comparing):

inventory.sort(comparing(Apple::getWeight));

恭喜你,这就是你的最终解决方案!这比Java 8之前的代码好在哪儿呢?它比较短;它的意思也很明显,并且代码读起来和问题描述差不多:“对库存进行排序,比较苹果的重量。”





3.8 复合Lambda表达式的有用方法


Java 8的好几个函数式接口都有为方便而设计的方法。具体而言,许多函数式接口,比如用于传递Lambda表达式的Comparator、Function和Predicate都提供了允许你进行复合的方法。这是什么意思呢?在实践中,这意味着你可以把多个简单的Lambda复合成复杂的表达式。比如,你可以让两个谓词之间做一个or操作,组合成一个更大的谓词。而且,你还可以让一个函数的结果成为另一个函数的输入。你可能会想,函数式接口中怎么可能有更多的方法呢?(毕竟,这违背了函数式接口的定义啊!)窍门在于,我们即将介绍的方法都是默认方法,也就是说它们不是抽象方法。第13章会详谈。现在只需相信我们,等想要进一步了解默认方法以及你可以用它做什么时,再去看看第13章。





3.8.1 比较器复合


我们前面看到,你可以使用静态方法Comparator.comparing,根据提取用于比较的键值的Function来返回一个Comparator,如下所示:

Comparator<Apple> c = Comparator.comparing(Apple::getWeight);

逆序

如果你想要对苹果按重量递减排序怎么办?用不着去建立另一个Comparator的实例。接口有一个默认方法reversed可以使给定的比较器逆序。因此仍然用开始的那个Comparator,只要修改一下前一个例子就可以对苹果按重量递减排序:

inventory.sort(comparing(Apple::getWeight).reversed()); ←---- 按重量递减排序



比较器链

上面说得都很好,但如果发现有两个苹果一样重怎么办?哪个苹果应该排在前面呢?你可能需要再提供一个Comparator来进一步定义这个比较。比如,在按重量比较两个苹果之后,你可能想要按原产国排序。thenComparing方法就是做这个用的。它接受一个函数作为参数(就像comparing方法一样),如果两个对象用第一个Comparator比较之后是一样的,就提供第二个Comparator。你又可以优雅地解决这个问题了:

inventory.sort(comparing(Apple::getWeight) .reversed() ←---- 按重量递减排序 .thenComparing(Apple::getCountry)); ←---- 两个苹果一样重时,进一步按国家排序





3.8.2 谓词复合


谓词接口包括三个方法:negate、and和or,让你可以重用已有的Predicate来创建更复杂的谓词。比如,你可以使用negate方法来返回一个Predicate的非,比如苹果不是红的:

Predicate<Apple> notRedApple = redApple.negate(); ←---- 产生现有Predicate对象redApple的非

你可能想要把两个Lambda用and方法组合起来,比如一个苹果既是红色又比较重:

Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150); ←---- 链接两个谓词来生成另一个Predicate对象

你可以进一步组合谓词,表达要么是重(150克以上)的红苹果,要么是绿苹果:

Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(apple -> apple.getWeight() > 150) .or(apple -> GREEN.equals(a.getColor())); ←---- 链接三个谓词来

这一点为什么很好呢?从简单Lambda表达式出发,你可以构建更复杂的表达式,但读起来仍然和问题的陈述差不多!请注意,and和or方法是按照在表达式链中的位置,从左向右确定优先级的。因此,a.or(b).and(c)可以看作(a || b) && c。同样,a.and(b).or(c) 可以看作(a && b) || c。





3.8.3 函数复合


最后,你还可以把Function接口所代表的Lambda表达式复合起来。Function接口为此配了andThen和compose两个默认方法,它们都会返回Function的一个实例。

andThen方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。比如,假设有一个函数f给数字加1 (x -> x + 1),另一个函数g给数字乘2,那么你可以将它们组合成一个函数h,先给数字加1,再给结果乘2:

Function<Integer, Integer> f = x -> x + 1; Function<Integer, Integer> g = x -> x * 2; Function<Integer, Integer> h = f.andThen(g); ←---- 数学上会写作g(f(x))或(g o f)(x) int result = h.apply(1); ←---- 这将返回4

你也可以类似地使用compose方法,先把给定的函数用作compose的参数里面给的那个函数,然后再把函数本身用于结果。比如在上一个例子里用compose的话,它将意味着f(g(x)),andThen则意味着g(f(x)):

Function<Integer, Integer> f = x -> x + 1; Function<Integer, Integer> g = x -> x * 2; Function<Integer, Integer> h = f.compose(g); ←---- 数学上会写作f(g(x))或(f o g)(x) int result = h.apply(1); ←---- 这将返回3

图3-6说明了andThen和compose之间的区别。



图 3-6 使用andThen与compose

这一切听起来有点太抽象了。那么在实际中这有什么用呢?比方说你有一系列工具方法,对用String表示的一封信做文本转换:

public class Letter{ public static String addHeader(String text){ return "From Raoul, Mario and Alan: " + text; } public static String addFooter(String text){ return text + " Kind regards"; } public static String checkSpelling(String text){ return text.replaceAll("labda", "lambda"); } }

现在你可以通过复合这些工具方法来创建各种转型流水线了,比如创建一个流水线:先加上抬头,然后进行拼写检查,最后加上一个落款,如图3-7所示。

Function<String, String> addHeader = Letter::addHeader; Function<String, String> transformationPipeline = addHeader.andThen(Letter::checkSpelling) .andThen(Letter::addFooter);



图 3-7 使用andThen的转换流水线

第二个流水线可能只加抬头、落款,而不做拼写检查:

Function<String, String> addHeader = Letter::addHeader; Function<String, String> transformationPipeline = addHeader.andThen(Letter::addFooter);





3.9 数学中的类似思想


如果你上学的时候对数学很擅长,那这一节就从另一个角度来谈谈Lambda表达式和函数传递的思想。你可以跳过它,书中没有任何其他内容依赖这一节,不过从另一个角度看看也挺好的。





3.9.1 积分


假设你有一个(数学,不是Java)函数,比如说定义是



那么,(工科学校里)经常问的一个问题就是,求画在纸上之后函数下方的面积(把轴作为基准)。比如对于图3-8所示的面积你会写

或



图 3-8 函数(从3到7)下方的面积

在这个例子里,函数是一条直线,因此你很容易通过梯形方法(画几个三角形和矩形)来算出面积:

1/2 × ((3 + 10) + (7 + 10)) × (7 – 3) = 60

那么这在Java里面如何表达呢?你的第一个问题是把积分号或之类的换成熟悉的编程语言符号。

确实,根据第一条原则你需要一个方法,比如说叫integrate,它接受三个参数:一个是f,还有上下限(这里是3.0和7.0)。于是写在Java里就是下面这个样子,函数f是作为参数被传递进去的:

integrate(f, 3, 7)

请注意,你不能简单地写:

integrate(x + 10, 3, 7)

原因有两个。第一,x的作用域不清楚;第二,这将把x + 10的值而不是函数f传给积分。

事实上,数学上的秘密作用就是说“以为自变量、结果是的那个函数。”





3.9.2 与Java 8的Lambda联系起来


前面说过,Java 8的表示法(double x) -> x + 10(一个Lambda表达式)恰恰就是为此设计的,因此你可以写:

integrate((double x) -> x + 10, 3, 7)

或者

integrate((double x) -> f(x), 3, 7)

或者,用前面说的方法引用,只要写:

integrate(C::f, 3, 7)

这里C是包含静态方法f的一个类。理念就是把f背后的代码传给integrate方法。

现在你可能在想如何写integrate本身了。我们还假设f是一个线性函数(直线)。你可能会写成类似数学的形式:

public double integrate((double -> double) f, double a, double b) { ←---- 错误的Java代码!(函数的写法不能像数学里那样。) return (f(a) + f(b)) * (b - a) / 2.0 }

不过,由于Lambda表达式只能用于接受函数式接口的地方(这里就是DoubleFunction4),所以你必须得写成这个样子:

4使用DoubleFunction比Function更高效,因为它避免了结果的装箱操作。

public double integrate(DoubleFunction<Double> f, double a, double b) { return (f.apply(a) + f.apply(b)) * (b - a) / 2.0; }

或者用DoubleUnaryOperator,这样也可以避免对结果进行装箱:

public double integrate(DoubleUnaryOperator f, double a, double b) { return (f.applyAsDouble(a) + f.applyAsDouble(b)) * (b - a) / 2.0; }

顺便提一句,有点可惜的是你必须写f.apply(a),而不是像数学里面写f(a),但Java无法摆脱“一切都是对象”的思想——它不能让函数完全独立!





3.10 小结


以下是本章中的关键概念。

Lambda表达式可以理解为一种匿名函数:它没有名称,但有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常的列表。

Lambda表达式让你可以简洁地传递代码。

函数式接口就是仅仅声明了一个抽象方法的接口。

只有在接受函数式接口的地方才可以使用Lambda表达式。

Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。

Java 8自带一些常用的函数式接口,放在java.util.function包里,包括Predicate <T>、Function<T, R>、Supplier<T>、Consumer<T>和BinaryOperator<T>,如表3-2所述。

为了避免装箱操作,对Predicate<T>和Function<T, R>等通用函数式接口的基本类型特化:IntPredicate、IntToLongFunction等。

环绕执行模式(即在方法所必需的代码中间,你需要执行点儿什么操作,比如资源分配和清理)可以配合Lambda提高灵活性和可重用性。

Lambda表达式所需要代表的类型称为目标类型。

方法引用让你重复使用现有的方法实现并直接传递它们。

Comparator、Predicate和Function等函数式接口都有几个可以用来结合Lambda表达式的默认方法。





第二部分 使用流进行函数式数据处理


第二部分仔细讨论新的Stream API。通过Stream API,你将能够写出功能强大的代码,以声明性方式处理数据。学完这一部分,你将充分理解流是什么,以及如何在Java应用程序中使用它们来简洁而高效地处理数据集。

第4章介绍流的概念,并解释它们与集合有何异同。

第5章详细讨论为了表达复杂的数据处理查询可以使用的流操作。其间会谈到很多模式,如筛选、切片、查找、匹配、映射和归约。

第6章介绍收集器——Stream API的一个功能,可以让你表达更为复杂的数据处理查询。

第7章探讨流如何得以自动并行执行,并利用多核架构的优势。此外,你还会学到为正确而高效地使用并行流,要避免的若干陷阱。





第 4 章 引入流


本章内容

什么是流

集合与流

内部迭代与外部迭代

中间操作与终端操作





集合是Java中使用最多的API。要是没有集合,还能做什么呢?几乎每个Java应用程序都会制造和处理集合。集合对于很多编程任务来说都是非常基本的:它们可以让你把数据分组并加以处理。为了解释集合是怎么工作的,想象一下你准备列出一系列菜,组成一张菜单,然后再遍历一遍,把每盘菜的热量加起来。或者,你可能想选出那些热量比较低的菜,组成一张健康的特殊菜单。尽管集合对于几乎任何一个Java应用都是不可或缺的,但集合操作远远算不上完美。

很多业务逻辑都涉及类似于数据库的操作,比如对几道菜按照类别进行分组(比如全素菜肴),或查找出最贵的菜。你自己用迭代器重新实现过这些操作多少遍?大部分数据库都允许你声明式地指定这些操作。比如,以下SQL查询语句就可以选出热量较低的菜肴名称:SELECT name FROM dishes WHERE calorie < 400。你看,你不需要实现如何根据菜肴的属性进行筛选(比如利用迭代器和累加器),只需要表达想要什么就可以了。这个基本的思路意味着,你用不着担心如何显式地实现这些查询语句——都替你办好了!怎么到了集合这里就不能这样了呢?

如果要处理大量元素又该怎么办呢?为了提高性能,你需要并行处理,并利用多核架构。但写并行代码比用迭代器还要复杂,而且调试起来也十分没意思!



那Java语言的设计者能做些什么,来帮助你节约宝贵的时间,让你这个程序员活得轻松一点儿呢?你可能已经猜到了,答案就是流。





4.1 流是什么


流是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。就现在来说,你可以把它们看成遍历数据集的高级迭代器。此外,流还可以透明地并行处理,你无须写任何多线程代码了!第7章会详细解释流和并行化是怎么工作的。先来简单看看使用流的好处吧。下面两段代码都是用来返回低热量菜肴名称的,并按照卡路里排序,一个是用Java 7写的,另一个是用Java 8的流写的。比较一下。不用太担心Java 8代码怎么写,接下来的几节会详细解释。

之前(Java 7):

List<Dish> lowCaloricDishes = new ArrayList<>(); for(Dish dish: menu) { if(dish.getCalories() < 400) { ←---- 用累加器筛选元素 lowCaloricDishes.add(dish); } } Collections.sort(lowCaloricDishes, new Comparator<Dish>() { ←---- 用匿名类对菜肴排序 public int compare(Dish dish1, Dish dish2) { return Integer.compare(dish1.getCalories(), dish2.getCalories()); } }); List<String> lowCaloricDishesName = new ArrayList<>(); for(Dish dish: lowCaloricDishes) { lowCaloricDishesName.add(dish.getName()); ←---- 处理排序后的菜名列表 }

在这段代码中,你用了一个“垃圾变量”lowCaloricDishes。它唯一的作用就是作为一次性的中间容器。在Java 8中,实现的细节被放在它本该归属的库里了。

之后(Java 8):

import static java.util.Comparator.comparing; import static java.util.stream.Collectors.toList; List<String> lowCaloricDishesName = menu.stream() .filter(d -> d.getCalories() < 400) ←---- 选出400卡路里以下的菜肴 .sorted(comparing(Dish::getCalories)) ←---- 按照卡路里排序 .map(Dish::getName) ←---- 提取菜肴的名称 .collect(toList()); ←---- 将所有名称保存在List中

为了利用多核架构并行执行这段代码,你只需要把stream()换成parallelStream():

List<String> lowCaloricDishesName = menu.parallelStream() .filter(d -> d.getCalories() < 400) .sorted(comparing(Dishes::getCalories)) .map(Dish::getName) .collect(toList());

你可能会想,在调用parallelStream方法的时候到底发生了什么。用了多少个线程?对性能有多大提升?是否应该使用这个方法?第7章会详细讨论这些问题。现在,你可以看出,从软件工程师的角度来看,新的方法有几个显而易见的好处。

代码是以声明性方式写的:说明想要完成什么(筛选热量低的菜肴)而不是说明如何实现一个操作(利用循环和if条件等控制流语句)。你在前面的章节中也看到了,这种方法加上行为参数化让你可以轻松应对变化的需求:你很容易再创建一个代码版本,利用Lambda表达式来筛选高卡路里的菜肴,而用不着去复制粘贴代码。这种方式的另一个好处是,线程模型与查询操作实现了解耦。由于你提供了查询的菜谱,因此具体的执行既可以串行,也可以并行。这部分内容的更多细节请参考第7章。

你可以把几个基础操作链接起来,来表达复杂的数据处理流水线(在filter后面接上sorted、map和collect操作,如图4-1所示),同时保持代码清晰可读。filter的结果被传给了sorted方法,再传给map方法,最后传给collect方法。





图 4-1 将流操作链接起来构成流的流水线

因为filter、sorted、map和collect等操作是与具体线程模型无关的高层次构件,所以它们的内部实现可以是单线程的,也可能透明地充分利用你的多核架构!在实践中,这意味着你用不着为了让某些数据处理任务并行而去操心线程和锁,Stream API都替你做好了!

新的Stream API表达能力非常强。比如在读完本章以及第5章、第6章之后,你就可以写出像下面这样的代码:

Map<Dish.Type, List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType));

第6章会解释这个例子。简单来说就是,按照Map里面的类别对菜肴进行分组。比如,Map可能包含下列结果:

{FISH=[prawns, salmon], OTHER=[french fries, rice, season fruit, pizza], MEAT=[pork, beef, chicken]}

想想要是改用循环这种典型的指令型编程方式该怎么实现吧。别浪费太多时间了。拥抱这一章和接下来几章中强大的流吧!

其他库:Guava、Apache和lambdaj

为了给Java程序员提供更好的库操作集合,前人已经做过了很多尝试。比如,Guava就是谷歌创建的一个很流行的库。它提供了multimaps和multisets等额外的容器类。Apache Commons Collections库也提供了类似的功能。最后,本书作者Mario Fusco编写的lambdaj受到函数式编程的启发,也提供了很多声明性操作集合的工具。

如今Java 8自带了官方库,可以以更加声明性的方式操作集合了。



总结一下,Java 8中的Stream API可以让你写出这样的代码:

声明性——更简洁,更易读;

可复合——更灵活;

可并行——性能更好。



在本章剩下的部分和下一章中,我们会使用这样一个例子:一个menu,它只是一张菜肴列表。

List<Dish> menu = Arrays.asList( new Dish("pork", false, 800, Dish.Type.MEAT), new Dish("beef", false, 700, Dish.Type.MEAT), new Dish("chicken", false, 400, Dish.Type.MEAT), new Dish("french fries", true, 530, Dish.Type.OTHER), new Dish("rice", true, 350, Dish.Type.OTHER), new Dish("season fruit", true, 120, Dish.Type.OTHER), new Dish("pizza", true, 550, Dish.Type.OTHER), new Dish("prawns", false, 300, Dish.Type.FISH), new Dish("salmon", false, 450, Dish.Type.FISH) );

Dish类的定义是:

public class Dish { private final String name; private final boolean vegetarian; private final int calories; private final Type type; public Dish(String name, boolean vegetarian, int calories, Type type) { this.name = name; this.vegetarian = vegetarian; this.calories = calories; this.type = type; } public String getName() { return name; } public boolean isVegetarian() { return vegetarian; } public int getCalories() { return calories; } public Type getType() { return type; } @Override public String toString() { return name; } public enum Type { MEAT, FISH, OTHER } }

现在就来仔细探讨一下怎么使用Stream API。我们会用流与集合做类比,做点儿铺垫。下一章会详细讨论可以用来表达复杂数据处理查询的流操作。我们会谈到很多模式,比如筛选、切片、查找、匹配、映射和归约,还会提供很多测验和练习来加深你的理解。

接下来会讨论如何创建和操纵数字流,比如生成一个偶数流,或是勾股数流。最后,我们会讨论如何从不同的源(比如文件)创建流。还会讨论如何生成一个具有无穷多元素的流——这用集合肯定是搞不定了!





4.2 流简介


讨论流之前,先来聊聊集合,这可能是最容易上手的方式了。Java 8的集合支持一个新的stream方法,它返回一个流(接口定义在java.util.stream.Stream中)。后面你会看到,还有很多别的方法也可以返回流,比如利用数值范围或从I/O资源生成流元素。

那么,流到底是什么?简短的定义就是“从支持数据处理操作的源生成的元素序列”。让我们一步步剖析这个定义。

元素序列——就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储和访问元素(如ArrayList 与LinkedList)。但流的目的在于表达计算,比如你前面见到的filter、sorted和map。集合讲的是数据,流讲的是计算。后面几节会详细解释这个思想。

源——流会使用一个提供数据的源,比如集合、数组或I/O资源。请注意,从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。

数据处理操作——流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,比如filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可以并行执行。



此外,流操作有两个重要的特点。

流水线——很多流操作本身会返回一个流,这样多个操作就可以链接起来,构成一个更大的流水线。这使得下一章中将要讨论的一些优化成为可能,比如处理延迟和短路。流水线的操作可以看作类似对数据源进行数据库查询。

内部迭代——与集合使用迭代器进行显式迭代不同,流的迭代操作是在后台进行的。第1章中简要提到过这一点,下一节还会再谈到它。



下面来看一段能够体现所有这些概念的代码:

import static java.util.stream.Collectors.toList; List<String> threeHighCaloricDishNames = menu.stream() ←---- 从menu(菜肴列表)获得流 .filter(dish -> dish.getCalories() > 300) ←---- 建立操作流水线:首先选出高热量的菜肴 .map(Dish::getName) ←---- 获取菜名 .limit(3) ←---- 只选择头三个 .collect(toList()); ←---- 将结果保存在另一个List中 System.out.println(threeHighCaloricDishNames); ←---- 结果是[pork, beef, chicken]

本例先是对menu调用stream方法,由菜单得到一个流。数据源是菜肴列表(菜单),它给流提供一个元素序列。接下来,对流应用一系列数据处理操作:filter、map、limit和collect。除了collect之外,所有这些操作都会返回另一个流,这样它们就可以接成一条流水线,于是就可以看作对源的一个查询。最后,collect操作开始处理流水线,并返回结果(它和别的操作不一样,因为它返回的不是流,在这里是一个List)。在调用collect之前,没有任何结果产生,实际上根本就没有从menu里选择元素。你可以这么理解:链中的方法调用都在排队等待,直到调用collect。图4-2显示了流操作的顺序:filter、map、limit、collect,每个操作简介如下。

filter——接受一个Lambda,从流中排除某些元素。在本例中,通过传递Lambda d -> d.getCalories() > 300,选择出热量超过300卡路里的菜肴。

map——接受一个Lambda,将元素转换成其他形式或提取信息。在本例中,通过传递方法引用Dish::getName,相当于Lambda d -> d.getName(),提取了每道菜的菜名。

limit——截断流,使其元素不超过给定数量。

collect——将流转换为其他形式。在本例中,流被转换为一个列表。它看起来有点儿像变魔术,第6章会详细解释collect的工作原理。现在,你可以把collect看作能够接受各种方案作为参数,并将流中的元素累积成为一个汇总结果的操作。这里的toList()就是将流转换为列表的方案。





图 4-2 使用流来筛选菜单,找出三个高热量菜肴的名字

注意看,刚刚解释的这段代码,与逐项处理菜单列表的代码有很大不同。首先,我们使用了声明性的方式来处理菜单数据,即你说的对这些数据需要做什么:“查找热量最高的三道菜的菜名。”你并没有去实现筛选(filter)、提取(map)或截断(limit)功能,Streams库已经自带了。因此,Stream API在决定如何优化这条流水线时更为灵活。例如,筛选、提取和截断操作可以一次进行,并在找到这三道菜后立即停止。下一章会介绍一个能体现这一点的例子。

在进一步介绍能对流做什么操作之前,先回过头来看看Collection API和新的Stream API的概念有何不同。





4.3 流与集合


Java现有的集合概念和新的流概念都提供了接口,来配合代表元素型有序值的数据接口。所谓有序,就是说我们一般是按顺序取值,而不是随机取用。那这两者有什么区别呢?

先来打个直观的比方吧。比如说存在DVD里的电影,这就是一个集合(也许是字节,也许是帧,这个无所谓),因为它包含了整个数据结构。现在再来想想在互联网上通过视频流看同样的电影。现在这是一个流(字节流或帧流)。流媒体视频播放器只要提前下载用户观看位置的那几帧就可以了,这样不用等到流中大部分值计算出来,你就可以显示流的开始部分了(想想观看直播足球赛)。特别要注意,视频播放器可能没有将整个流作为集合,保存所需要的内存缓冲区——而且要是非得等到最后一帧出现才能开始看,那等待的时间就太长了。出于实现的考虑,你也可以让视频播放器把流的一部分缓存在集合里,但和概念上的差异不是一回事。

粗略地说,集合与流之间的差异就在于什么时候进行计算。集合是一个内存中的数据结构,它包含数据结构中目前所有的值——集合中的每个元素都得先算出来才能添加到集合中(你可以往集合里加东西或者删东西,但是不管什么时候,集合中的每个元素都是放在内存里的,元素都得先算出来才能成为集合的一部分)。

相比之下,流则是在概念上固定的数据结构(你不能添加或删除元素),其元素是按需计算的。这对编程有很大的好处。第6章会展示构建一个质数流(2, 3, 5, 7, 11, …)有多简单,尽管质数有无穷多个。这个理念就是用户仅仅从流中提取需要的值,而这些值——在用户看不见的地方——只会按需生成。这是一种生产者–消费者的关系。从另一个角度来说,流就像是一个延迟创建的集合:只有在消费者要求的时候才会计算值(用管理学的话说这就是需求驱动,甚至是实时制造)。

与此相反,集合则是急切创建的(供应商驱动:先把仓库装满,再开始卖,就像那些昙花一现的圣诞新玩意儿一样)。以质数为例,要是想创建一个包含所有质数的集合,那这个程序算起来就没完没了了,因为总有新的质数要算,然后把它加到集合里面。当然这个集合是永远也创建不完的,消费者这辈子都见不着了。

图4-3用DVD对比在线流媒体的例子展示了流和集合之间的差异。



图 4-3 流与集合

另一个例子是用浏览器进行互联网搜索。假设你搜索的短语在Google或是网店里面有很多匹配项。你用不着等到所有结果和照片的集合下载完,而是得到一个流,里面有最好的10个或20个匹配项,还有一个按钮来查看下面10个或20个。当你作为消费者点击“下面10个”的时候,供应商就按需计算这些结果,然后再送回你的浏览器上显示。





4.3.1 只能遍历一次


请注意,和迭代器类似,流只能遍历一次。遍历完之后,我们就说这个流已经被消费掉了。你可以从原始数据源那里再获得一个新的流来重新遍历一遍,就像迭代器一样(这里假设它是集合之类的可重复的源,如果是I/O通道就“没戏”了)。例如,以下代码会抛出一个异常,说流已被消费掉了:

List<String> title = Arrays.asList("Modern", "Java", "In", "Action"); Stream<String> s = title.stream(); s.forEach(System.out::println); ←---- 打印标题中的每个单词 s.forEach(System.out::println); ←---- java.lang.IllegalStateException:流已被操作或关闭

所以要记得,流只能消费一次!

哲学中的流和集合

对于喜欢哲学的读者,你可以把流看作在时间中分布的一组值。相反,集合则是空间(这里就是计算机内存)中分布的一组值,在一个时间点上全体存在——你可以使用迭代器来访问for-each循环中的内部成员。



集合和流的另一个关键区别在于它们遍历数据的方式。





4.3.2 外部迭代与内部迭代


使用Collection接口需要用户去做迭代(比如用for-each),这称为外部迭代。相反,Stream库使用内部迭代——它帮你把迭代做了,还把得到的流值存在了某个地方,你只要给出一个函数说要干什么就可以了。下面的代码列表说明了这种区别。

代码清单 4-1 集合:用for-each循环外部迭代



List<String> names = new ArrayList<>(); for(Dish dish: menu){ ←---- 显式顺序迭代菜单列表 names.add(dish.getName()); ←---- 提取名称并将其添加到累加器 }

请注意,for-each还隐藏了迭代中的一些复杂性。for-each结构是一个语法糖,它背后的东西用Iterator对象表达出来会更丑陋。

代码清单 4-2 集合:用背后的迭代器做外部迭代



List<String> names = new ArrayList<>(); Iterator<String> iterator = menu.iterator(); while(iterator.hasNext()) { ←---- 显式迭代 Dish dish = iterator.next(); names.add(dish.getName()); }

代码清单 4-3 流:内部迭代



List<String> names = menu.stream() .map(Dish::getName) ←---- 用getName方法参数化map,提取菜名 .collect(toList()); ←---- 开始执行操作流水线;没有迭代!

让我们用一个比喻来解释内部迭代的差异和好处吧。比方说你正在和你两岁的女儿索菲亚说话,希望她能把玩具收起来。

你:“索菲亚,我们把玩具收起来吧。地上还有玩具吗?”

索菲亚:“有,有球。”

你:“好,把球放进盒子里。还有吗?”

索菲亚:“有,那是我的娃娃。”

你:“好,把娃娃放进盒子里。还有吗?”

索菲亚:“有,有我的书。”

你:“好,把书放进盒子里。还有吗?”

索菲亚:“没了,没有了。”

你:“好,我们收好啦。”



这正是你每天都要对Java集合所做的。你外部迭代一个集合,显式地取出每个项目再加以处理。如果你只需跟索菲亚说“把地上所有的玩具都放进盒子里”就好了。内部迭代比较好的原因有两个:第一,索非亚可以选择一只手拿娃娃,另一只手拿球;第二,她可以决定先拿离盒子最近的那个东西,然后再拿别的。同样的道理,内部迭代时,项目可以透明地并行处理,或者以更优化的顺序进行处理。要是用Java过去的那种外部迭代方法,这些优化都是很困难的。这似乎有点儿鸡蛋里挑骨头,但这差不多就是Java 8引入流的理由了——Streams库的内部迭代可以自动选择一种适合你硬件的数据表示和并行实现。与此相反,一旦选择了for-each这样的外部迭代,那你基本上就要自己管理所有的并行问题了(自己管理实际上意味着“某个良辰吉日我们会把它并行化”或“开始了关于任务和synchronized的漫长而艰苦的斗争”)。Java 8需要一个类似于Collection却没有迭代器的接口,于是就有了Stream!图4-4说明了流(内部迭代)与集合(外部迭代)之间的差异。



图 4-4 内部迭代与外部迭代

我们已经介绍了集合与流在概念上的差异,特别是流利用内部迭代自动地替你执行了迭代。但是,除非你预先定义好了能隐藏迭代的操作列表,例如filter或map,否则这一特性对你不一定有用。大多数这类操作都接受Lambda表达式作为参数,因此你可以利用前几章介绍的方法对它的行为进行参数化。Java语言的设计者为Stream API提供了大量的操作,可以表达非常复杂的数据处理查询逻辑。现在先简要地看一下这些操作,下一章中会配上例子详细讨论。为了检验你对外部迭代和内部迭代的理解,请尝试一下测验4.1。

测验4.1:外部迭代与内部迭代

基于你对代码清单4-1和代码清单4-2中外部迭代的学习,请选择一种流操作来重构下面的代码。

List<String> highCaloricDishes = new ArrayList<>(); Iterator<String> iterator = menu.iterator(); while(iterator.hasNext()) { Dish dish = iterator.next(); if(dish.getCalories() > 300) { highCaloricDishes.add(d.getName()); } }

答案:应该选择使用filter模式。

List<String> highCaloricDish = menu.stream() .filter(dish -> dish.getCalories() > 300) .collect(toList());

即使你现在对如何准确地编写流查询还不太熟悉也不必担心,下一章会深入探讨这部分内容。





4.4 流操作


java.util.stream.Stream中的Stream接口定义了许多操作。它们可以分为两大类。再来看一下前面的例子:

List<String> names = menu.stream() ←---- 从菜单获得流 .filter(dish -> dish.getCalories() > 300) ←---- 中间操作 .map(Dish::getName) ←---- 中间操作 .limit(3) ←---- 中间操作 .collect(toList()); ←---- 将Stream转换为List

你可以看到两类操作:

filter、map和limit可以连成一条流水线;

collect触发流水线执行并关闭它。



可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作。图4-5中展示了这两类操作。这种区分有什么意义呢?



图 4-5 中间操作与终端操作





4.4.1 中间操作


诸如filter或sorted等中间操作会返回另一个流。这让多个操作可以连接起来形成一个查询。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理——它们很懒。这是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。

为了搞清楚流水线中到底发生了什么,我们把代码改一改,让每个Lambda都打印出当前处理的菜肴(就像很多演示和调试技巧一样,这种编程风格要是搁在生产代码里那就吓死人了,但是学习的时候可以直接看清楚求值的顺序):

List<String> names = menu.stream() .filter(dish -> { System.out.println("filtering:" + dish.getName()); return dish.getCalories() > 300; }) ←---- 打印当前筛选的菜肴 .map(dish -> { System.out.println("mapping:" + dish.getName()); return dish.getName(); }) ←---- 提取菜名时打印出来 .limit(3) .collect(toList()); System.out.println(names);

此代码执行时将打印:

filtering:pork mapping:pork filtering:beef mapping:beef filtering:chicken mapping:chicken [pork, beef, chicken]

你会发现,有好几种优化利用了流的延迟性质。第一,尽管很多菜的热量都高于300卡路里,但只选出了前三个!这是因为limit操作和一种称为短路的技巧,下一章会对此做详细解释。第二,尽管filter和map是两个独立的操作,但它们合并到同一次遍历中了(我们把这种技术叫作循环合并)。





4.4.2 终端操作


终端操作会从流的流水线生成结果,其结果是任何不是流的值,比如List、Integer,甚至void。例如,在下面的流水线中,forEach是一个返回void的终端操作,它会对源中的每道菜应用一个Lambda。把System.out.println传递给forEach,并要求它打印出由menu生成的流中的每一个Dish:

menu.stream().forEach(System.out::println);

为了检验你对中间操作和终端操作的理解程度,试试测验4.2吧。

测验4.2:中间操作与终端操作

在下列流水线中,你能找出中间操作和终端操作吗?

long count = menu.stream() .filter(dish -> dish.getCalories() > 300) .distinct() .limit(3) .count();

答案:流水线中最后一个操作count返回一个long,这是一个非Stream的值。因此它是一个终端操作。所有前面的操作,filter、distinct、limit,都是连接起来的,并返回一个Stream,因此它们是中间操作。





4.4.3 使用流


总而言之,流的使用一般包括三件事:

一个数据源(如集合)来执行一个查询;

一个中间操作链,形成一条流的流水线;

一个终端操作,执行流水线,并能生成结果。



流的流水线背后的理念类似于构建器模式。在构建器模式中有一个调用链用来设置一套配置(对流来说这就是一个中间操作链),接着是调用build方法(对流来说就是终