概要信息:
图灵社区的电子书没有采用专有客
户端,您可以在任意设备上,用自
己喜欢的浏览器和PDF阅读器进行
阅读。
但您购买的电子书仅供您个人使
用,未经授权,不得进行传播。
我们愿意相信读者具有这样的良知
和觉悟,与我们共同保护知识产
权。
如果购买者有侵权行为,我们可能
对该用户实施包括但不限于关闭该
帐号等维权措施,并可能追究法律
责任。
图 灵 程 序 设 计 丛 书
人 民 邮 电 出 版 社
北 京
Java 8 Lambdas
Functional Programming For The Masses
[英]Richard Warburton 著
王群锋 译
Java 8函数式编程
Beijing • Cambridge • Farnham • Köln • Sebastopol • Tokyo
O’Reilly Media, Inc.授权人民邮电出版社出版
内 容 提 要
多年以来,函数式编程被认为是少数人的游戏,不适合推广给普罗大众。写作此书的目的就
是为了挑战这种思想。本书将探讨如何编写出简单、干净、易读的代码;如何简单地使用并行计算
提高性能;如何准确地为问题建模,并且开发出更好的领域特定语言;如何写出不易出错,并且更
简单的并发代码;如何测试和调试 Lambda 表达式。
如果你已经掌握 Java SE,想尽快了解 Java 8 新特性,写出简单干净的代码,那么本书不容错过。
定价:39.00元
读者服务热线:(010)51095186转600 印装质量热线:(010)81055316
反盗版热线:(010)81055315
广告经营许可证:京崇工商广字第 0021 号
著 [英] Richard Warburton
译 王群锋
责任编辑 李松峰
执行编辑 李 静 仇祝平
责任印制 杨林杰
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
北京 印刷
开本:800×1000 1/16
印张:9.25
字数:191千字 2015年 4 月第 1 版
印数:1 — 3 500册 2015年 4 月北京第 1次印刷
著作权合同登记号 图字:01-2014-6949号
◆
◆
◆
III
版权声明
© 2014 by O’Reilly Media,Inc.
Simplified Chinese Edition, jointly published by O’Reilly Media,Inc.and Posts & Telecom
Press, 2015. Authorized translation of the English edition, 2014 O’Reilly Media,Inc.,the
owner of all rights to publish and sell the same.
All rights reserved including the rights of reproduction in whole or in part in any form.
英文原版由 O’Reilly Media, Inc. 出版,2014。
简体中文版由人民邮电出版社出版, 2015。英文原版的翻译得到 O’Reilly Media, Inc.
的授权。此简体中文版的出版和销售得到出版权和销售权的所有者 —— O’Reilly
Media, Inc. 的许可。
版权所有,未得书面许可,本书的任何部分和全部不得以任何形式重制。
O’Reilly Media 通过图书、杂志、在线服务、调查研究和会议等方式传播创新知识。
自 1978 年开始,O’Reilly 一直都是前沿发展的见证者和推动者。超级极客们正在开创
着未来,而我们关注真正重要的技术趋势——通过放大那些“细微的信号”来刺激社
会对新科技的应用。作为技术社区中活跃的参与者,O’Reilly 的发展充满了对创新的
倡导、创造和发扬光大。
O’Reilly 为软件开发人员带来革命性的“动物书”;创建第一个商业网站(GNN);组
织了影响深远的开放源代码峰会,以至于开源软件运动以此命名;创立了 Make 杂志,
从而成为 DIY 革命的主要先锋;公司一如既往地通过多种形式缔结信息与人的纽带。
O’Reilly 的会议和峰会集聚了众多超级极客和高瞻远瞩的商业领袖,共同描绘出开创
新产业的革命性思想。作为技术人士获取信息的选择,O’Reilly 现在还将先锋专家的
知识传递给普通的计算机用户。无论是通过书籍出版,在线服务或者面授课程,每一
项 O’Reilly 的产品都反映了公司不可动摇的理念——信息是激发创新的力量。
业界评论
“O’Reilly Radar 博客有口皆碑。”
——Wired
“O’Reilly 凭借一系列(真希望当初我也想到了)非凡想法建立了数百万美元的业务。”
——Business 2.0
“O’Reilly Conference 是聚集关键思想领袖的绝对典范。”
——CRN
“一本 O’Reilly 的书就代表一个有用、有前途、需要学习的主题。”
——Irish Times
“Tim 是位特立独行的商人,他不光放眼于最长远、最广阔的视野并且切实地按照
Yogi Berra 的建议去做了:‘如果你在路上遇到岔路口,走小路(岔路)。’回顾过去
Tim 似乎每一次都选择了小路,而且有几次都是一闪即逝的机会,尽管大路也不错。”
——Linux Journal
O’Reilly Media, Inc.介绍
V
目录
前言 .........................................................................................................................................................IX
第 1 章 简介 .........................................................................................................................................1
1.1 为什么需要再次修改 Java .........................................................................................................1
1.2 什么是函数式编程 .....................................................................................................................2
1.3 示例 .............................................................................................................................................2
第 2 章 Lambda 表达式 ...................................................................................................................5
2.1 第一个 Lambda 表达式 ..............................................................................................................5
2.2 如何辨别 Lambda 表达式 ..........................................................................................................6
2.3 引用值,而不是变量 .................................................................................................................8
2.4 函数接口 .....................................................................................................................................9
2.5 类型推断 ...................................................................................................................................10
2.6 要点回顾 ...................................................................................................................................12
2.7 练习 ...........................................................................................................................................12
第 3 章 流 ...........................................................................................................................................15
3.1 从外部迭代到内部迭代 ...........................................................................................................15
3.2 实现机制 ...................................................................................................................................17
3.3 常用的流操作 ...........................................................................................................................19
3.3.1 collect(toList()) ..................................................................................................19
3.3.2 map ............................................................................................................................19
3.3.3 filter ......................................................................................................................21
3.3.4 flatMap ....................................................................................................................22
VI | 目录
3.3.5 max 和 min .................................................................................................................23
3.3.6 通用模式 ......................................................................................................................24
3.3.7 reduce ......................................................................................................................24
3.3.8 整合操作 ......................................................................................................................26
3.4 重构遗留代码 ...........................................................................................................................27
3.5 多次调用流操作 .......................................................................................................................30
3.6 高阶函数 ...................................................................................................................................31
3.7 正确使用 Lambda 表达式 ........................................................................................................31
3.8 要点回顾 ...................................................................................................................................32
3.9 练习 ...........................................................................................................................................32
3.10 进阶练习 .................................................................................................................................33
第 4 章 类库 .......................................................................................................................................35
4.1 在代码中使用 Lambda 表达式 ................................................................................................35
4.2 基本类型 ...................................................................................................................................36
4.3 重载解析 ...................................................................................................................................38
4.4 @FunctionalInterface ........................................................................................................40
4.5 二进制接口的兼容性 ...............................................................................................................40
4.6 默认方法 ...................................................................................................................................41
4.7 多重继承 ...................................................................................................................................45
4.8 权衡 ...........................................................................................................................................46
4.9 接口的静态方法 .......................................................................................................................46
4.10 Optional .............................................................................................................................47
4.11 要点回顾 .................................................................................................................................48
4.12 练习 .........................................................................................................................................48
4.13 开放练习 .................................................................................................................................49
第 5 章 高级集合类和收集器........................................................................................................51
5.1 方法引用 ...................................................................................................................................51
5.2 元素顺序 ...................................................................................................................................52
5.3 使用收集器 ...............................................................................................................................54
5.3.1 转换成其他集合 ..........................................................................................................54
5.3.2 转换成值 ......................................................................................................................55
5.3.3 数据分块 ......................................................................................................................55
5.3.4 数据分组 ......................................................................................................................56
5.3.5 字符串 ..........................................................................................................................57
5.3.6 组合收集器 ..................................................................................................................58
5.3.7 重构和定制收集器 ......................................................................................................60
目录 | VII
5.3.8 对收集器的归一化处理 ..............................................................................................65
5.4 一些细节 ...................................................................................................................................66
5.5 要点回顾 ...................................................................................................................................67
5.6 练习 ...........................................................................................................................................67
第 6 章 数据并行化 .........................................................................................................................69
6.1 并行和并发 ...............................................................................................................................69
6.2 为什么并行化如此重要 ...........................................................................................................70
6.3 并行化流操作 ...........................................................................................................................71
6.4 模拟系统 ...................................................................................................................................72
6.5 限制 ...........................................................................................................................................75
6.6 性能 ...........................................................................................................................................75
6.7 并行化数组操作 .......................................................................................................................78
6.8 要点回顾 ...................................................................................................................................80
6.9 练习 ...........................................................................................................................................80
第 7 章 测试、调试和重构 ............................................................................................................81
7.1 重构候选项 ...............................................................................................................................81
7.1.1 进进出出、摇摇晃晃 ..................................................................................................82
7.1.2 孤独的覆盖 ..................................................................................................................82
7.1.3 同样的东西写两遍 ......................................................................................................83
7.2 Lambda 表达式的单元测试 .....................................................................................................85
7.3 在测试替身时使用 Lambda 表达式 ........................................................................................87
7.4 惰性求值和调试 .......................................................................................................................89
7.5 日志和打印消息 .......................................................................................................................89
7.6 解决方案:peak ...................................................................................................................90
7.7 在流中间设置断点 ...................................................................................................................90
7.8 要点回顾 ...................................................................................................................................90
第 8 章 设计和架构的原则 ............................................................................................................91
8.1 Lambda 表达式改变了设计模式 .............................................................................................92
8.1.1 命令者模式 ..................................................................................................................92
8.1.2 策略模式 ......................................................................................................................95
8.1.3 观察者模式 ..................................................................................................................97
8.1.4 模板方法模式 ............................................................................................................100
8.2 使用 Lambda 表达式的领域专用语言 ..................................................................................102
8.2.1 使用 Java 编写 DSL ...................................................................................................103
8.2.2 实现 ............................................................................................................................104
VIII | 目录
8.2.3 评估 ............................................................................................................................106
8.3 使用 Lambda 表达式的 SOLID 原则 ....................................................................................106
8.3.1 单一功能原则 ............................................................................................................107
8.3.2 开闭原则 ....................................................................................................................109
8.3.3 依赖反转原则 ............................................................................................................ 111
8.4 进阶阅读 .................................................................................................................................114
8.5 要点回顾 .................................................................................................................................114
第 9 章 使用 Lambda 表达式编写并发程序 ..........................................................................115
9.1 为什么要使用非阻塞式 I/O...................................................................................................115
9.2 回调 .........................................................................................................................................116
9.3 消息传递架构 .........................................................................................................................119
9.4 末日金字塔 .............................................................................................................................120
9.5 Future ......................................................................................................................................122
9.6 CompletableFuture ............................................................................................................123
9.7 响应式编程 .............................................................................................................................126
9.8 何时何地使用新技术 .............................................................................................................128
9.9 要点回顾 .................................................................................................................................129
9.10 练习 .......................................................................................................................................129
第 10 章 下一步该怎么办 ............................................................................................................131
封面介绍 ..............................................................................................................................................133
前言
多年以来,函数式编程被认为是少数人的游戏,这些人总是强调自己在智力上的优越性,
认为函数式编程的智慧不适合推广给普罗大众。写作此书的目的就是为了挑战这种思想,
函数式编程并没有多么了不起,也绝不是少数人的游戏。
在过去的两年中,我请伦敦 Java 社区的开发人员以各种方式测试 Java 8 的新特性。我发现
很多人都喜欢 Java 8 的新用法和类库。他们有可能被一些术语和高大上的概念吓到,但是
稍稍一丁点儿函数式编程技巧都能给编程带来便利,他们对此喜不自胜。人们津津乐道的
话题之一是使用新的 Stream API 操作对象和集合类时(比如从所有的唱片列表中过滤出在
英国本地出品的唱片时),代码是多么易读。
组织这些 Java 社区活动,让我认识到了示例代码的重要性。人们通过不断地阅读和消化这
些简单的示例,最终归纳出某种模式。我还意识到术语是多么令人讨厌,因此,在介绍一
个晦涩的概念时,我都会给出通俗易懂的解释。
对很多人来说,Java 8 提供的函数式编程元素有限:没有单子 1,没有语言层面的惰性求值,
也没有为不可变性提供额外支持。对实用至上的程序员来说,这没什么大不了的,我们只
想在类库级别抽象,写出简单干净的代码来解决业务问题。如果有人为我们写出这样的类
库,那再好不过了,这样我们就可以把主要精力放在日常工作上了。
为什么要阅读本书
本书将探讨如下主题。
如何编写出简单、干净、易读的代码——尤其是对于集合的操作?•
如何简单地使用并行计算提高性能?•
注 1: 别担心,这是本书唯一提及单子的地方。
IX
X | 前言
如何准确地为问题建模,并且开发出更好的领域特定语言?•
如何写出不易出错,并且更简单的并发代码?•
如何测试和调试 Lambda 表达式?•
将 Lambda 表达式加入 Java,并不只是为了提高开发人员的生产效率,业界也对这一特性
有根本性的需求。
本书读者对象
本书面向那些已经掌握 Java SE,并且想尽快了解 Java 8 新特性的开发人员。
如果你对 Lambda 表达式感兴趣,想知道它怎么帮助你提升专业技能,那么这本书就是为
你而写的。我假设读者还不知道 Lambda 表达式,以及 Java 8 中核心类库的变化,我将从
零开始介绍这些概念、类库和技术。
虽然我想让所有开发人员都来买这本书,但这不现实,这不是一本适合所有人的书。如
果你一点儿也不懂 Java,那么这本书就不适合你。同时,尽管本书会详细讲解 Java 中的
Lambda 表达式,但是我不会解释怎样在其他语言中使用 Lambda 表达式。
我也不会讲解 Java SE 中一些基本的概念,比如集合类、匿名内部类或者 Swing 中的事件
处理机制。我假设读者已经掌握了这些知识。
怎样阅读本书
本书采用了示例驱动的写作风格:介绍完一个概念之后,就会紧跟一段代码。代码中的一
些片段,有时你可能无法全部看懂。没关系,通常在代码后面会紧跟一段文字,讲解代码
的细节。
这种方式能让你边学边练,多数章节还在最后提供了练习题,供读者自行练习。我强烈建
议读者读完一章后完成这些练习,熟能生巧。每个务实的程序员都知道,自欺欺人很容
易,你觉得读懂一段代码了,其实还是遗漏了一些细节。
使用 Lambda 表达式,就是将复杂性抽象到类库的过程。在本书中,我会引入很多常用类
库的细节。第 2 章至第 6 章介绍了 JDK 8 中核心语言的变化以及升级后的类库。
最后三章介绍了如何在真实环境下使用函数式编程。第 7 章介绍一些让测试和调试
Lambda 表达式变得容易的技巧;第 8 章解释现有的那些良好的软件设计原则如何应用到
Lambda 表达式上;第 9 章讨论并发,怎样使用 Lambda 表达式写出易读且易于维护的并发
代码。涉及第三方类库时,这些章节也会加以介绍。
读者可以将前四章当作 Java 8 的入门指南——要用好 Java 8, 每个人都必须学会这些知识。
前言 | XI
后面的几章难度略高,但掌握了这几章的内容,你就可以成为知识更加全面的程序员,在
自己的设计中得心应手地使用 Lambda 表达式。你在不断学习的过程中,也会接触大量的
练习,答案可以在 GitHub(https://github.com/RichardWarburton/java-8-Lambdas-exercises)
上找到。如果你能边学边练,就能迅速掌握 Lambda 表达式。
本书排版规范
本书使用以下排版规范。
楷体•
表示新术语。
等宽字体•
表示程序片段,也用于在正文中表示程序中使用的变量、函数名、数据库、数据类型、
环境变量、语句和关键字等元素。
等宽粗体•
表示应该由用户逐字输入的命令或者其他文本。
等宽斜体•
表示将由用户提供的值(或由上下文确定的值)替换的文本。
这个图标表示提示或建议。
这个图标表示重要说明。
这个图标表示警告或提醒。
使用代码示例
可以在这里下载本书随附的资料(代码示例、练习题等):https://github.com/RichardWarburton/
java-8-lambdas-exercises。
XII | 前言
让本书助你一臂之力。也许你需要在自己的程序或文档中用到本书中的代码。除非大段大
段地使用,否则不必与我们联系取得授权。例如,无需请求许可,就可以用本书中的几段
代码写成一个程序。但是销售或者发布 O’Reilly 图书中代码的光盘则必须事先获得授权。
引用书中的代码来回答问题也无需授权。将大段的示例代码整合到你自己的产品文档中则
必须经过许可。
使用我们的代码时,希望你能标明它的出处,但不强求。出处信息一般包括书名、作者、
出版商和书号,例如:Java 8 Lambdas,Richard Warburton 著(O’Reilly,2014)。版权所
有, 978-1-449-37077-0。
如果还有关于使用代码的未尽事宜,可以随时与我们联系:permissions@oreilly.com。
Safari
®
Books Online
Safari Books Online (http://www.safaribooksonline.com)是应需
而变的数字图书馆。它同时以图书和视频的形式出版世界顶级
技术和商务作家的专业作品。
Safari Books Online 是技术专家、软件开发人员、Web 设计师、商务人士和创意人士开展
调研、解决问题、学习和认证培训的第一手资料。
对于组织团体、政府机构和个人,Safari Books Online 提供各种产品组合和灵活的定
价策略。用户可通过一个功能完备的数据库检索系统访问 O’Reilly Media、Prentice
Hall Professional、Addison-Wesley Professional、Microsoft Press、Sams、Que、Peachpit
Press、Focal Press、Cisco Press、John Wiley & Sons、Syngress、Morgan Kaufmann、IBM
Redbooks、Packt、Adobe Press、FT Press、Apress、Manning、New Riders、McGraw-Hill、
Jones & Bartlett、Course Technology 以及其他几十家出版社的上千种图书、培训视频和正
式出版之前的书稿。要了解 Safari Books Online 的更多信息,我们网上见。
联系我们
请把对本书的评价和问题发给出版社。
美国:
O’Reilly Media, Inc.
1005 Gravenstein Highway North
Sebastopol, CA 95472
中国:
北京市西城区西直门南大街 2 号成铭大厦 C 座 807 室(100035)
前言 | XIII
奥莱利技术咨询(北京)有限公司
O’Reilly 的每一本书都有专属网页,你可以在那儿找到本书的相关信息,包括勘误表、示
例代码以及其他信息。本书的网站地址是:
http://oreil.ly/java_8_lambdas。
对于本书的评论和技术性问题,请发送电子邮件到:
bookquestions@oreilly.com
要了解更多 O’Reilly 图书、培训课程、会议和新闻的信息,请访问以下网站:
http://www.oreilly.com
我们在 Facebook 的地址如下:http://facebook.com/oreilly
请关注我们的 Twitter 动态:http://twitter.com/oreillymedia
我们的 YouTube 视频地址如下:http://www.youtube.com/oreillymedia
致谢
虽然本书的封面上署的是我的名字,但本书得以出版要归功于很多人。
首先要感谢我的编辑 Meghan 和 O’Reilly 的出版团队,他们让整个出版过程变得很愉快,
而且他们还适当加快了本书的出版进度。还要感谢 Martijn 和 Ben 将我引荐给 Meghan,没
有这次会面就不会有这本书。
审阅过程极大地提升了本书的质量,衷心感谢那些正式或非正式参与审阅的朋友,他们
是: Martijn Verburg、Jim Gough、John Oliver、Edward Wong、Brian Goetz、Daniel Bryant、
Fred Rosenberger、Jaikiran Pai 和 Mani Sarkar。尤其要感谢 Martijn,他给了我如何写一本
技术书的实战指导。
如果忘记感谢 Oracle 公司的 Project Lambda 项目组,我不会原谅自己。更新一个成熟的语
言是一项巨大的挑战,他们不辱使命,我也因此有了得以编写本书的素材。在 Java 8 发布
早期版本时,伦敦的 Java 社区积极参与测试,通过这些测试,很容易就发现了开发人员犯
了哪类错误,哪些地方可以修复,感谢他们!
在写作本书的过程中,我得到了很多人的支持和帮助,特别是我的父母。在我需要的时
候,他们总是陪伴在身边。我的朋友们也总是给我积极的评价和鼓励,包括 Compsoc 里的
那些老伙计们,特别是 Sadiq Jaffer 和基督少年军,感谢你们!
1
第 1 章
简介
在开始探索 Lambda 表达式之前,首先我们要知道它因何而生。本章将介绍 Lambda 表达
式产生的原因,以及本书的写作动机和组织结构。
1.1 为什么需要再次修改Java
1996 年 1 月,Java 1.0 发布,此后计算机编程领域发生了翻天覆地的变化。商业发展需要
更复杂的应用,大多数程序都跑在功能强大的多核 CPU 的机器上。带有高效运行时编译
器的 Java 虚拟机(JVM)的出现,使程序员将更多精力放在编写干净、易于维护的代码
上,而不是思考如何将每一个 CPU 时钟周期、每字节内存物尽其用。
多核 CPU 的兴起成为了不容回避的事实。涉及锁的编程算法不但容易出错,而且耗费时
间。人们开发了 java.util.concurrent 包和很多第三方类库,试图将并发抽象化,帮助程
序员写出在多核 CPU 上运行良好的程序。很可惜,到目前为止,我们的成果还远远不够。
开发类库的程序员使用 Java 时,发现抽象级别还不够。处理大型数据集合就是个很好的例
子,面对大型数据集合,Java 还欠缺高效的并行操作。开发者能够使用 Java 8 编写复杂的
集合处理算法,只需要简单修改一个方法,就能让代码在多核 CPU 上高效运行。为了编写
这类处理批量数据的并行类库,需要在语言层面上修改现有的 Java:增加 Lambda 表达式。
当然,这样做是有代价的,程序员必须学习如何编写和阅读使用 Lambda 表达式的代码,
但是,这不是一桩赔本的买卖。与手写一大段复杂、线程安全的代码相比,学习一点新语
法和一些新习惯容易很多。开发企业级应用时,好的类库和框架极大地降低了开发时间和
成本,也为开发易用且高效的类库扫清了障碍。
2 | 第 1 章
对于习惯了面向对象编程的开发者来说,抽象的概念并不陌生。面向对象编程是对数据进
行抽象,而函数式编程是对行为进行抽象。现实世界中,数据和行为并存,程序也是如
此,因此这两种编程方式我们都得学。
这种新的抽象方式还有其他好处。不是所有人都在编写性能优先的代码,对于这些人来
说,函数式编程带来的好处尤为明显。程序员能编写出更容易阅读的代码——这种代码更
多地表达了业务逻辑的意图,而不是它的实现机制。易读的代码也易于维护、更可靠、更
不容易出错。
在写回调函数和事件处理程序时,程序员不必再纠缠于匿名内部类的冗繁和可读性,函数
式编程让事件处理系统变得更加简单。能将函数方便地传递也让编写惰性代码变得容易,
惰性代码在真正需要时才初始化变量的值。
Java 8 还让集合类可以拥有一些额外的方法:default 方法。程序员在维护自己的类库时,
可以使用这些方法。
总而言之,Java 已经不是祖辈们当年使用的 Java 了,嗯, 这不是件坏事。
1.2 什么是函数式编程
每个人对函数式编程的理解不尽相同。但其核心是:在思考问题时,使用不可变值和函
数,函数对一个值进行处理,映射成另一个值。
不同的语言社区往往对各自语言中的特性孤芳自赏。现在谈 Java 程序员如何定义函数式编
程还为时尚早,但是,这根本不重要!我们关心的是如何写出好代码,而不是符合函数式
编程风格的代码。
本书将重点放在函数式编程的实用性上,包括可以被大多数程序员理解和使用的技术,帮
助他们写出易读、易维护的代码。
1.3 示例
本书中的示例全部都围绕一个常见的问题领域构造:音乐。具体来说,这些示例代表了在
专辑上常常看到的信息,有关术语定义如下。
Artist•
创作音乐的个人或团队。
name• :艺术家的名字(例如“甲壳虫乐队”)。
members• :乐队成员(例如“约翰 · 列侬”),该字段可为空。
origin• :乐队来自哪里(例如“利物浦”)。
| 3
Track•
专辑中的一支曲目。
name• :曲目名称(例如《黄色潜水艇》)。
Album•
专辑,由若干曲目组成。
name• :专辑名(例如《左轮手枪》)。
tracks• :专辑上所有曲目的列表。
musicians• :参与创作本专辑的艺术家列表。
本书将使用这个问题讲解如何在正常的业务领域或者 Java 应用中使用函数式编程技术。也
许读者认为这些示例并不完美,但它和真实的业务领域应用比起来足够简单,书中的很多
代码都是基于这个简单的模型。
5
第 2 章
Lambda表达式
Java 8 的最大变化是引入了 Lambda 表达式——一种紧凑的、传递行为的方式。它也是本
书后续章节所述内容的基础,因此,接下来就了解一下什么是 Lambda 表达式。
2.1 第一个Lambda表达式
Swing 是一个与平台无关的 Java 类库,用来编写图形用户界面(GUI)。该类库有一个常见
用法:为了响应用户操作,需要注册一个事件监听器。用户一输入,监听器就会执行一些
操作(见例 2-1)。
例 2-1 使用匿名内部类将行为和按钮单击进行关联
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.out.println("button clicked");
}
});
在这个例子中,我们创建了一个新对象,它实现了 ActionListener 接口。这个接口只有一
个方法 actionPerformed,当用户点击屏幕上的按钮时,button 就会调用这个方法。匿名
内部类实现了该方法。在例 2-1 中该方法所执行的只是输出一条信息,表明按钮已被点击。
这实际上是一个代码即数据的例子——我们给按钮传递了一个代表某种行为
的对象。
6 | 第 2 章
设计匿名内部类的目的,就是为了方便 Java 程序员将代码作为数据传递。不过,匿名内部
类还是不够简便。为了调用一行重要的逻辑代码,不得不加上 4 行冗繁的样板代码。若把
样板代码用其他颜色区分开来,就可一目了然:
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.out.println("button clicked");
}
});
尽管如此,样板代码并不是唯一的问题:这些代码还相当难读,因为它没有清楚地表达程
序员的意图。我们不想传入对象,只想传入行为。在 Java 8 中,上述代码可以写成一个
Lambda 表达式,如例 2-2 所示。
例 2-2 使用 Lambda 表达式将行为和按钮单击进行关联
button.addActionListener(event -> System.out.println("button clicked"));
和传入一个实现某接口的对象不同,我们传入了一段代码块——一个没有名字的函数。
event 是参数名,和上面匿名内部类示例中的是同一个参数。-> 将参数和 Lambda 表达式
的主体分开,而主体是用户点击按钮时会运行的一些代码。
和使用匿名内部类的另一处不同在于声明 event 参数的方式。使用匿名内部类时需要显式
地声明参数类型 ActionEvent event,而在 Lambda 表达式中无需指定类型,程序依然可以
编译。这是因为 javac 根据程序的上下文(addActionListener 方法的签名)在后台推断出
了参数 event 的类型。这意味着如果参数类型不言而明,则无需显式指定。稍后会介绍类
型推断的更多细节,现在先来看看编写 Lambda 表达式的各种方式。
尽管与之前相比,Lambda 表达式中的参数需要的样板代码很少,但是 Java 8
仍然是一种静态类型语言。为了增加可读性并迁就我们的习惯,声明参数时
也可以包括类型信息,而且有时编译器不一定能根据上下文推断出参数的
类型!
2.2 如何辨别Lambda表达式
Lambda 表达式除了基本的形式之外,还有几种变体,如例 2-3 所示。
例 2-3 编写 Lambda 表达式的不同形式
Runnable noArguments = () -> System.out.println("Hello World"); n
ActionListener oneArgument = event -> System.out.println("button clicked"); o
Runnable multiStatement = () -> { p
Lambda表达式 | 7
System.out.print("Hello");
System.out.println(" World");
};
BinaryOperator add = (x, y) -> x + y; q
BinaryOperator addExplicit = (Long x, Long y) -> x + y; r
➊中所示的 Lambda 表达式不包含参数,使用空括号 () 表示没有参数。该 Lambda 表达式
实现了 Runnable 接口,该接口也只有一个 run 方法,没有参数,且返回类型为 void。
➋中所示的 Lambda 表达式包含且只包含一个参数,可省略参数的括号,这和例 2-2 中的
形式一样。
Lambda 表达式的主体不仅可以是一个表达式,而且也可以是一段代码块,使用大括号
({})将代码块括起来,如➌所示。该代码块和普通方法遵循的规则别无二致,可以用返
回或抛出异常来退出。只有一行代码的 Lambda 表达式也可使用大括号,用以明确 Lambda
表达式从何处开始、到哪里结束。
Lambda 表达式也可以表示包含多个参数的方法,如➍所示。这时就有必要思考怎样去阅
读该 Lambda 表达式。这行代码并不是将两个数字相加,而是创建了一个函数,用来计算
两个数字相加的结果。变量 add 的类型是 BinaryOperator,它不是两个数字的和,
而是将两个数字相加的那行代码。
到目前为止,所有 Lambda 表达式中的参数类型都是由编译器推断得出的。这当然不错,
但有时最好也可以显式声明参数类型,此时就需要使用小括号将参数括起来,多个参数的
情况也是如此。如➎所示。
目标类型是指 Lambda 表达式所在上下文环境的类型。比如,将 Lambda 表
达式赋值给一个局部变量,或传递给一个方法作为参数,局部变量或方法参
数的类型就是 Lambda 表达式的目标类型。
上述例子还隐含了另外一层意思:Lambda 表达式的类型依赖于上下文环境,是由编译器
推断出来的。目标类型也不是一个全新的概念。如例 2-4 所示,Java 中初始化数组时,数
组的类型就是根据上下文推断出来的。另一个常见的例子是 null,只有将 null 赋值给一
个变量,才能知道它的类型。
例 2-4 等号右边的代码并没有声明类型,系统根据上下文推断出类型信息
final String[] array = { "hello", "world" };
8 | 第 2 章
2.3 引用值,而不是变量
如果你曾使用过匿名内部类,也许遇到过这样的情况:需要引用它所在方法里的变量。这
时,需要将变量声明为 final,如例 2-5 所示。将变量声明为 final,意味着不能为其重复赋
值。同时也意味着在使用 final 变量时,实际上是在使用赋给该变量的一个特定的值。
例 2-5 匿名内部类中使用 final 局部变量
final String name = getUserName();
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.out.println("hi " + name);
}
});
Java 8 虽然放松了这一限制,可以引用非 final 变量,但是该变量在既成事实上必须是
final。虽然无需将变量声明为 final,但在 Lambda 表达式中,也无法用作非终态变量。如
果坚持用作非终态变量,编译器就会报错。
既成事实上的 final 是指只能给该变量赋值一次。换句话说,Lambda 表达式引用的是值,
而不是变量。在例 2-6 中,name 就是一个既成事实上的 final 变量。
例 2-6 Lambda 表达式中引用既成事实上的 final 变量
String name = getUserName();
button.addActionListener(event -> System.out.println("hi " + name));
final 就像代码中的线路噪声,省去之后代码更易读。当然,有些情况下,显式地使用 final
代码更易懂。是否使用这种既成事实上的 final 变量,完全取决于个人喜好。
如果你试图给该变量多次赋值,然后在 Lambda 表达式中引用它,编译器就会报错。比
如,例 2-7 无法通过编译,并显示出错信息:local variables referenced from a Lambda
expression must be final or effectively final1。
例 2-7 未使用既成事实上的 final 变量,导致无法通过编译
String name = getUserName();
name = formatUserName(name);
button.addActionListener(event -> System.out.println("hi " + name));
这种行为也解释了为什么 Lambda 表达式也被称为闭包。未赋值的变量与周边环境隔离起
来,进而被绑定到一个特定的值。在众说纷纭的计算机编程语言圈子里,Java 是否拥有真
正的闭包一直备受争议,因为在 Java 中只能引用既成事实上的 final 变量。名字虽异,功
能相同,就好比把菠萝叫作凤梨,其实都是同一种水果。为了避免无意义的争论,全书将
使用“Lambda 表达式”一词。无论名字如何,如前文所述,Lambda 表达式都是静态类型
注 1: Lambda 表达式中引用的局部变量必须是 final 或既成事实上的 final 变量。——译者注
Lambda表达式 | 9
的。因此,接下来就分析一下 Lambda 表达式本身的类型:函数接口。
2.4 函数接口
函数接口是只有一个抽象方法的接口,用作 Lambda 表达式的类型。
在 Java 里,所有方法参数都有固定的类型。假设将数字 3 作为参数传给一个方法,则参数
的类型是 int。那么,Lambda 表达式的类型又是什么呢?
使用只有一个方法的接口来表示某特定方法并反复使用,是很早就有的习惯。使用 Swing
编写过用户界面的人对这种方式都不陌生,例 2-2 中的用法也是如此。这里无需再标新立
异,Lambda 表达式也使用同样的技巧,并将这种接口称为函数接口。例 2-8 展示了前面例
子中所用的函数接口。
例 2-8 ActionListener 接口:接受 ActionEvent 类型的参数,返回空
public interface ActionListener extends EventListener {
public void actionPerformed(ActionEvent event);
}
ActionListener 只有一个抽象方法:actionPerformed,被用来表示行为:接受一个参数,
返回空。记住,由于 actionPerformed 定义在一个接口里,因此 abstract 关键字不是必需
的。该接口也继承自一个不具有任何方法的父接口:EventListener。
这就是函数接口,接口中单一方法的命名并不重要,只要方法签名和 Lambda 表达式的类
型匹配即可。可在函数接口中为参数起一个有意义的名字,增加代码易读性,便于更透彻
地理解参数的用途。
这里的函数接口接受一个 ActionEvent 类型的参数,返回空(void),但函数接口还可有其
他形式。例如,函数接口可以接受两个参数,并返回一个值, 还可以使用泛型,这完全取
决于你要干什么。
以后我将使用图形来表示不同类型的函数接口。指向函数接口的箭头表示参数,如果箭头
从函数接口射出,则表示方法的返回类型。ActionListener 的函数接口如图 2-1 所示。
图 2-1:ActionListener 接口,接受一个 ActionEvent 对象,返回空
10 | 第 2 章
使用 Java 编程,总会遇到很多函数接口,但 Java 开发工具包(JDK)提供的一组核心函数
接口会频繁出现。表 2-1 罗列了一些最重要的函数接口。
表2-1 Java中重要的函数接口
接口 参数 返回类型 示例
Predicate T boolean 这张唱片已经发行了吗
Consumer T void 输出一个值
Function T R 获得 Artist对象的名字
Supplier None T 工厂方法
UnaryOperator T T 逻辑非 (!)
BinaryOperator (T, T) T 求两个数的乘积 (*)
前面已讲过函数接口接收的类型,也讲过 javac 可以根据上下文自动推断出参数的类型,
且用户也可以手动声明参数类型,但何时需要手动声明呢?下面将对类型推断作详尽说明。
2.5 类型推断
某些情况下,用户需要手动指明类型,建议大家根据自己或项目组的习惯,采用让代码最
便于阅读的方法。有时省略类型信息可以减少干扰,更易弄清状况;而有时却需要类型信
息帮助理解代码。经验证发现,一开始类型信息是有用的,但随后可以只在真正需要时才
加上类型信息。下面将介绍一些简单的规则,来帮助确认是否需要手动声明参数类型。
Lambda 表达式中的类型推断,实际上是 Java 7 就引入的目标类型推断的扩展。读者可能
已经知道 Java 7 中的菱形操作符,它可使 javac 推断出泛型参数的类型。参见例 2-9。
例 2-9 使用菱形操作符,根据变量类型做推断
Map oldWordCounts = new HashMap(); n
Map diamondWordCounts = new HashMap<>(); o
我们为变量 oldWordCounts➊明确指定了泛型的类型,而变量 diamondWordCounts➋则使用了
菱形操作符。不用明确声明泛型类型,编译器就可以自己推断出来,这就是它的神奇之处!
当然,这并不是什么魔法,根据变量 diamondWordCounts ➋的类型可以推断出 HashMap 的泛
型类型,但用户仍需要声明变量的泛型类型。
如果将构造函数直接传递给一个方法,也可根据方法签名来推断类型。在例 2-10 中,我们
传入了 HashMap,根据方法签名已经可以推断出泛型的类型。
例 2-10 使用菱形操作符,根据方法签名做推断
useHashmap(new HashMap<>());
...
Lambda表达式 | 11
private void useHashmap(Map values);
Java 7 中程序员可省略构造函数的泛型类型,Java 8 更进一步,程序员可省略 Lambda 表达
式中的所有参数类型。再强调一次,这并不是魔法,javac 根据 Lambda 表达式上下文信息
就能推断出参数的正确类型。程序依然要经过类型检查来保证运行的安全性,但不用再显
式声明类型罢了。这就是所谓的类型推断。
Java 8 中对类型推断系统的改善值得一提。上面的例子将 new HashMap<>()
传给 useHashmap 方法,即使编译器拥有足够的信息,也无法在 Java 7 中通过
编译。
接下来将通过举例来详细分析类型推断。
例 2-11 和例 2-12 都将变量赋给一个函数接口,这样便于理解。第一个例子(例 2-11)使
用 Lambda 表达式检测一个 Integer 是否大于 5。这实际上是一个 Predicate——用来判断
真假的函数接口。
例 2-11 类型推断
Predicate atLeast5 = x -> x > 5;
Predicate 也是一个 Lambda 表达式,和前文中 ActionListener 不同的是,它还返回一个
值。在例 2-11 中,表达式 x > 5 是 Lambda 表达式的主体。这样的情况下,返回值就是
Lambda 表达式主体的值。
例 2-12 Predicate 接口的源码,接受一个对象,返回一个布尔值
public interface Predicate {
boolean test(T t);
}
从例 2-12 中可以看出,Predicate 只有一个泛型类型的参数,Integer 用于其中。Lambda
表达式实现了 Predicate 接口,因此它的单一参数被推断为 Integer 类型。javac 还可检查
Lambda 表达式的返回值是不是 boolean,这正是 Predicate 方法的返回类型(如图 2-2)。
图 2-2:Predicate 接口图示,接受一个对象,返回一个布尔值
例 2-13 是一个略显复杂的函数接口:BinaryOperator。该接口接受两个参数,返回一个
12 | 第 2 章
值,参数和值的类型均相同。实例中所用的类型是 Long。
例 2-13 略显复杂的类型推断
BinaryOperator addLongs = (x, y) -> x + y;
类型推断系统相当智能,但若信息不够,类型推断系统也无能为力。类型系统不会漫无边
际地瞎猜,而会中止操作并报告编译错误,寻求帮助。比如,如果我们删掉例 2-13 中的某
些类型信息,就会得到例 2-14 所示的代码。
例 2-14 没有泛型,代码则通不过编译
BinaryOperator add = (x, y) -> x + y;
编译器给出的报错信息如下:
Operator '& #x002B;' cannot be applied to java.lang.Object, java.lang.Object.
报错信息让人一头雾水,到底怎么回事? BinaryOperator 毕竟是一个具有泛型参数的函数
接口,该类型既是参数 x 和 y 的类型,也是返回值的类型。上面的例子中并没有给出变量
add 的任何泛型信息,给出的正是原始类型的定义。因此,编译器认为参数和返回值都是
java.lang.Object 实例。
4.3 节还会讲到类型推断,但就目前来说,掌握以上类型推断的知识就已经足够了。
2.6 要点回顾
Lambda 表达式是一个匿名方法,将行为像数据一样进行传递。•
Lambda 表达式的常见结构:• BinaryOperator add = (x, y) → x + y。
函数接口指仅具有单个抽象方法的接口,用来表示 Lambda 表达式的类型。•
2.7 练习
每章最后都附有一组练习,帮助读者实践并巩固本章的知识和新概念。练习答案可在
GitHub(https://github.com/RichardWarburton/java-8-Lambdas-exercises)上本书所对应的代
码仓库中找到。
1. 请看例 2-15 中的 Function 函数接口并回答下列问题。
例 2-15 Function 函数接口
public interface Function {
R apply(T t);
}
a. 请画出该函数接口的图示。
Lambda表达式 | 13
b. 若要编写一个计算器程序,你会使用该接口表示什么样的 Lambda 表达式?
c. 下列哪些 Lambda 表达式有效实现了 Function ?
x -> x + 1;
(x, y) -> x + 1;
x -> x == 1;
2. ThreadLocal Lambda 表达式。Java 有一个 ThreadLocal 类,作为容器保存了当前线程里
局部变量的值。Java 8 为该类新加了一个工厂方法,接受一个 Lambda 表达式,并产生
一个新的 ThreadLocal 对象,而不用使用继承,语法上更加简洁。
a. 在 Javadoc 或集成开发环境(IDE)里找出该方法。
b. DateFormatter 类是非线程安全的。使用构造函数创建一个线程安全的 DateFormatter
对象,并输出日期,如“01-Jan-1970”。
3. 类型推断规则。下面是将 Lambda 表达式作为参数传递给函数的一些例子。javac 能正
确推断出 Lambda 表达式中参数的类型吗?换句话说,程序能编译吗?
a. Runnable helloWorld = () -> System.out.println("hello world");
b. 使用 Lambda 表达式实现 ActionListener 接口:
JButton button = new JButton();
button.addActionListener(event ->
System.out.println(event.getActionCommand()));
c. 以如下方式重载 check 方法后,还能正确推断出 check(x -> x > 5) 的类型吗?
interface IntPred {
boolean test(Integer value);
}
boolean check(Predicate predicate);
boolean check(IntPred predicate);
你可能需要查阅 Javadoc 或在 IDE 里查看方法的参数类型,验证重载是否有效。
15
第 3 章
流
Java 8 中新增的特性旨在帮助程序员写出更好的代码,其中对核心类库的改进是很关键的
一部分,也是本章的主要内容。对核心类库的改进主要包括集合类的 API 和新引入的流
(Stream)。流使程序员得以站在更高的抽象层次上对集合进行操作。
本章会介绍 Stream 类中的一组方法,每个方法都对应集合上的一种操作。
3.1 从外部迭代到内部迭代
本章及本书其余部分的例子大多围绕 1.3 节介绍的案例展开。
Java 程序员在使用集合类时,一个通用的模式是在集合上进行迭代,然后处理返回的每一
个元素。比如要计算从伦敦来的艺术家的人数,通常代码会写成例 3-1 这样。
例 3-1 使用 for 循环计算来自伦敦的艺术家人数
int count = 0;
for (Artist artist : allArtists) {
if (artist.isFrom("London")) {
count++;
}
}
尽管这样的操作可行,但存在几个问题。每次迭代集合类时,都需要写很多样板代码。将
16 | 第 3 章
for 循环改造成并行方式运行也很麻烦,需要修改每个 for 循环才能实现。
此外,上述代码无法流畅传达程序员的意图。for 循环的样板代码模糊了代码的本意,程
序员必须阅读整个循环体才能理解。若是单一的 for 循环,倒也问题不大,但面对一个满
是循环(尤其是嵌套循环)的庞大代码库时,负担就重了。
就其背后的原理来看,for 循环其实是一个封装了迭代的语法糖,我们在这里多花点时间,
看看它的工作原理。首先调用 iterator 方法,产生一个新的 Iterator 对象,进而控制整
个迭代过程,这就是外部迭代。迭代过程通过显式调用 Iterator 对象的 hasNext 和 next
方法完成迭代。展开后的代码如例 3-2 所示,图 3-1 展示了迭代过程中的方法调用。
例 3-2 使用迭代器计算来自伦敦的艺术家人数
int count = 0;
Iterator iterator = allArtists.iterator();
while(iterator.hasNext()) {
Artist artist = iterator.next();
if (artist.isFrom("London")) {
count++;
}
}
ᆌᆩپஓ णپࢇஓ
پ۞
ᇮ໎
图 3-1:外部迭代
然而,外部迭代也有问题。首先,它很难抽象出本章稍后提及的不同操作;此外,它从本
质上来讲是一种串行化操作。总体来看,使用 for 循环会将行为和方法混为一谈。
另一种方法就是内部迭代,如例 3-3 所示。首先要注意 stream() 方法的调用,它和例 3-2
中调用 iterator() 的作用一样。该方法不是返回一个控制迭代的 Iterator 对象,而是返
回内部迭代中的相应接口:Stream。
例 3-3 使用内部迭代计算来自伦敦的艺术家人数
long count = allArtists.stream()
.filter(artist -> artist.isFrom("London"))
.count();
流 | 17
图 3-2 展示了使用类库后的方法调用流程,与图 3-1 形成对比。
ᆌᆩپஓ णپࢇஓ
پ۞
ॺ֡ፕࠓ
ࡕ
图 3-2:内部迭代
Stream 是用函数式编程方式在集合类上进行复杂操作的工具。
例 3-3 可被分解为两步更简单的操作:
找出所有来自伦敦的艺术家;•
计算他们的人数。•
每种操作都对应 Stream 接口的一个方法。为了找出来自伦敦的艺术家,需要对 Stream 对
象进行过滤:filter。过滤在这里是指“只保留通过某项测试的对象”。测试由一个函数完
成,根据艺术家是否来自伦敦,该函数返回 true 或者 false。由于 Stream API 的函数式编
程风格,我们并没有改变集合的内容,而是描述出 Stream 里的内容。count() 方法计算给
定 Stream 里包含多少个对象。
3.2 实现机制
例 3-3 中,整个过程被分解为两种更简单的操作:过滤和计数,看似有化简为繁之嫌——
例 3-1 中只含一个 for 循环,两种操作是否意味着需要两次循环?事实上,类库设计精妙,
只需对艺术家列表迭代一次。
通常,在 Java 中调用一个方法,计算机会随即执行操作:比如,System.out.println
("Hello World"); 会在终端上输出一条信息。Stream 里的一些方法却略有不同,它们虽是
普通的 Java 方法,但返回的 Stream 对象却不是一个新集合,而是创建新集合的配方。现
在,尝试思考一下例 3-4 中代码的作用,一时毫无头绪也没关系,稍后会详细解释。
18 | 第 3 章
例 3-4 只过滤,不计数
allArtists.stream()
.filter(artist -> artist.isFrom("London"));
这行代码并未做什么实际性的工作,filter 只刻画出了 Stream,但没有产生新的集合。像
filter 这样只描述 Stream,最终不产生新集合的方法叫作惰性求值方法;而像 count 这样
最终会从 Stream 产生值的方法叫作及早求值方法。
如果在过滤器中加入一条 println语句,来输出艺术家的名字,就能轻而易举地看出其中的不
同。例 3-5 对例 3-4 作了一些修改,加入了输出语句。运行这段代码,程序不会输出任何信息!
例 3-5 由于使用了惰性求值,没有输出艺术家的名字
allArtists.stream()
.filter(artist -> {
System.out.println(artist.getName());
return artist.isFrom("London");
});
如果将同样的输出语句加入一个拥有终止操作的流,如例 3-3 中的计数操作,艺术家的名
字就会被输出(见例 3-6)。
例 3-6 输出艺术家的名字
long count = allArtists.stream()
.filter(artist -> {
System.out.println(artist.getName());
return artist.isFrom("London");
})
.count();
以披头士乐队的成员作为艺术家列表,运行上述程序,命令行里输出的内容如例 3-7 所示。
例 3-7 显示披头士乐队成员名单的示例输出
John Lennon
Paul McCartney
George Harrison
Ringo Starr
判断一个操作是惰性求值还是及早求值很简单:只需看它的返回值。如果返回值是 Stream,
那么是惰性求值;如果返回值是另一个值或为空,那么就是及早求值。使用这些操作的理
想方式就是形成一个惰性求值的链,最后用一个及早求值的操作返回想要的结果,这正是
它的合理之处。计数的示例也是这样运行的,但这只是最简单的情况:只含两步操作。
整个过程和建造者模式有共通之处。建造者模式使用一系列操作设置属性和配置,最后调
用一个 build 方法,这时,对象才被真正创建。
读者一定会问:“为什么要区分惰性求值和及早求值?”只有在对需要什么样的结果和操
流 | 19
作有了更多了解之后,才能更有效率地进行计算。例如,如果要找出大于 10 的第一个数
字,那么并不需要和所有元素去做比较,只要找出第一个匹配的元素就够了。这也意味着
可以在集合类上级联多种操作,但迭代只需一次。
3.3 常用的流操作
为了更好地理解 Stream API,掌握一些常用的 Stream 操作十分必要。除此处讲述的几种重
要操作之外,该 API 的 Javadoc 中还有更多信息。
3.3.1 collect(toList())
collect(toList())方法由 Stream里的值生成一个列表,是一个及早求值操作。
Stream 的 of 方法使用一组初始值生成新的 Stream。事实上,collect 的用法不仅限于此,
它是一个非常通用的强大结构,第 5 章将详细介绍它的其他用途。下面是使用 collect 方
法的一个例子:
List collected = Stream.of("a", "b", "c") n
.collect(Collectors.toList()); o
assertEquals(Arrays.asList("a", "b", "c"), collected); p
这段程序展示了如何使用 collect(toList()) 方法从 Stream 中生成一个列表。如上文所述,
由于很多 Stream 操作都是惰性求值,因此调用 Stream 上一系列方法之后,还需要最后再
调用一个类似 collect 的及早求值方法。
这个例子也展示了本节中所有示例代码的通用格式。首先由列表生成一个 Stream ➊,然后
进行一些 Stream 上的操作,继而是 collect 操作,由 Stream 生成列表➋,最后使用断言
判断结果是否和预期一致➌。
形象一点儿的话,可以将 Stream 想象成汉堡,将最前和最后对 Stream 操作的方法想象成
两片面包,这两片面包帮助我们认清操作的起点和终点。
3.3.2 map
如果有一个函数可以将一种类型的值转换成另外一种类型,map 操作就可以
使用该函数,将一个流中的值转换成一个新的流。
20 | 第 3 章
读者可能已经注意到,以前编程时或多或少使用过类似 map 的操作。比如编写一段 Java 代
码,将一组字符串转换成对应的大写形式。在一个循环中,对每个字符串调用 toUppercase
方法,然后将得到的结果加入一个新的列表。代码如例 3-8 所示。
例 3-8 使用 for 循环将字符串转换为大写
List collected = new ArrayList<>();
for (String string : asList("a", "b", "hello")) {
String uppercaseString = string.toUpperCase();
collected.add(uppercaseString);
}
assertEquals(asList("A", "B", "HELLO"), collected);
如果你经常实现例 3-8 中这样的 for 循环,就不难猜出 map 是 Stream 上最常用的操作之一
(如图 3-3 所示)。例 3-9 展示了如何使用新的流框架将一组字符串转换成大写形式。
图 3-3:map 操作
例 3-9 使用 map 操作将字符串转换为大写形式
List collected = Stream.of("a", "b", "hello")
.map(string -> string.toUpperCase()) n
.collect(toList());
assertEquals(asList("A", "B", "HELLO"), collected);
传给 map ➊的 Lambda 表达式只接受一个 String 类型的参数,返回一个新的 String。参数
和返回值不必属于同一种类型,但是 Lambda 表达式必须是 Function 接口的一个实例(如
图 3-4 所示),Function 接口是只包含一个参数的普通函数接口。
图 3-4:Function 接口
流 | 21
3.3.3 filter
遍历数据并检查其中的元素时,可尝试使用 Stream 中提供的新方法 filter
(如图 3-5 所示)。
图 3-5:filter 操作
上面就是一个使用 filter 的例子,如果你已熟悉这一概念,也可以选择跳过本节。啊哈!
您还没跳过本节?那太好了,我们一起来看看这个方法有什么用。假设要找出一组字符串
中以数字开头的字符串,比如字符串 "1abc" 和 "abc",其中 "1abc" 就是符合条件的字符串。
可以使用一个 for 循环,内部用 if 条件语句判断字符串的第一个字符来解决这个问题,代
码如例 3-10 所示。
例 3-10 使用循环遍历列表,使用条件语句做判断
List beginningWithNumbers = new ArrayList<>();
for(String value : asList("a", "1abc", "abc1")) {
if (isDigit(value.charAt(0))) {
beginningWithNumbers.add(value);
}
}
assertEquals(asList("1abc"), beginningWithNumbers);
你可能已经写过很多类似的代码:这被称为 filter 模式。该模式的核心思想是保留 Stream
中的一些元素,而过滤掉其他的。例 3-11 展示了如何使用函数式风格编写相同的代码。
例 3-11 函数式风格
List beginningWithNumbers
= Stream.of("a", "1abc", "abc1")
.filter(value -> isDigit(value.charAt(0)))
.collect(toList());
22 | 第 3 章
assertEquals(asList("1abc"), beginningWithNumbers);
和 map 很像,filter 接受一个函数作为参数,该函数用 Lambda 表达式表示。该函数和前面
示例中 if 条件判断语句的功能一样,如果字符串首字母为数字,则返回 true。若要重构
遗留代码,for 循环中的 if 条件语句就是一个很强的信号,可用 filter 方法替代。
由于此方法和 if 条件语句的功能相同,因此其返回值肯定是 true 或者 false。经过过滤,
Stream 中符合条件的,即 Lambda 表达式值为 true 的元素被保留下来。该 Lambda 表达式
的函数接口正是前面章节中介绍过的 Predicate(如图 3-6 所示)。
图 3-6:Predicate 接口
3.3.4 flatMap
flatMap 方法可用 Stream 替换值,然后将多个 Stream 连接成一个 Stream
(如图 3-7 所示)。
图 3-7:flatMap 操作
前面已介绍过 map 操作,它可用一个新的值代替 Stream 中的值。但有时,用户希望让 map
操作有点变化,生成一个新的 Stream 对象取而代之。用户通常不希望结果是一连串的流,
此时 flatMap 最能派上用场。
我们看一个简单的例子。假设有一个包含多个列表的流,现在希望得到所有数字的序列。
该问题的一个解法如例 3-12 所示。
流 | 23
例 3-12 包含多个列表的 Stream
List together = Stream.of(asList(1, 2), asList(3, 4))
.flatMap(numbers -> numbers.stream())
.collect(toList());
assertEquals(asList(1, 2, 3, 4), together);
调用 stream 方法,将每个列表转换成 Stream 对象,其余部分由 flatMap 方法处理。
flatMap 方法的相关函数接口和 map 方法的一样,都是 Function 接口,只是方法的返回值
限定为 Stream 类型罢了。
3.3.5 max和min
Stream 上常用的操作之一是求最大值和最小值。Stream API 中的 max 和 min 操作足以解决
这一问题。例 3-13 是查找专辑中最短曲目所用的代码,展示了如何使用 max 和 min 操作。
为了方便检查程序结果是否正确,代码片段中罗列了专辑中的曲目信息,我承认,这张专
辑是有点冷门。
例 3-13 使用 Stream 查找最短曲目
List