Java 8实战(txt+pdf+epub+mobi电子书下载)


发布时间:2020-06-04 03:22:14

点击下载

作者:厄马(Raoul-Gabriel Urma)

出版社:人民邮电出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

Java 8实战

Java 8实战试读:

序言

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

那么,你可能会问,这本书是怎么来的呢?

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

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

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

15个月后,到2014年7月,在经历无数个漫漫长夜的辛苦工作、无数次的编辑和永生难忘的体验后,我们的工作成果终于送到了你的手上。希望你会喜欢它!Raoul-Gabriel Urma于剑桥大学

致谢

如果没有许多杰出人士的支持,这本书是不可能完成的。● 自愿提供宝贵审稿建议的朋友:Richard Walker、Jan

Saganowski、Brian Goetz、Stuart Marks、Cem Redif、Paul

Sandoz、Stephen Colebourne、Íñigo Mediavilla、Allahbaksh

Asadullah、Tomasz Nurkiewicz和Michael Müller。● 曼宁早期访问项目(Manning Early Access Program,

MEAP)中在作者在线论坛上发表评论的读者。● 在编撰过程中提供有益反馈的审阅者: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的开发编辑Susan Conant耐心回答了我们所有的问

题和疑虑,并为每一章的初稿提供了详尽的反馈,并尽其所能支

持我们。● Ivan Todorović和Jean-François Morin在本书付印前进行了全

面的技术审阅,Al Scherer则在编撰过程中提供了技术帮助。

Raoul-Gabriel Urma

首先,我要感谢我的父母在生活中给予我无尽的爱和支持。我写一本书的小小梦想如今成真了!其次,我要向信任并且支持我的博士生导师和合著者Alan Mycroft表达无尽的感激。我也要感谢合著者Mario Fusco陪我走过这段有趣的旅程。最后,我要感谢在生活中为我提供指导、有用建议,给予我鼓励的朋友们:Sophia Drossopoulou、Aidan Roche、Warris Bokhari、Alex Buckley、Martijn Verburg、Tomas Petricek和Tian Zhao。你们真是太棒啦!

Mario Fusco

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

Alan Mycroft

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

关于本书

简单地说,Java 8中的新增功能是自Java 1.0发布18年以来,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 8编程”和“超越Java 8”。我们强烈建议你按顺序阅读,因为很多概念都需要前面的章节作为基础。大多数章节都有几个小测验,帮助你学习和掌握这些内容。

第一部分包括3章,旨在帮助你初步使用Java 8。学完这一部分,你将会对Lambda表达式有充分的了解,并可以编写简洁而灵活的代码,能够轻松适应不断变化的需求。● 在第1章中,我们总结了Java的主要变化(Lambda表达式、

方法引用、流和默认方法),并为学习后面的内容做好准备。● 在第2章中,你将了解行为参数化,这是Java 8非常依赖的

一种软件开发模式,也是引入Lambda表达式的主要原因。● 第3章全面地解释了Lambda表达式和方法引用,每一步都有

代码示例和测验。

第二部分仔细讨论了新的Stream API。学完这一部分,你将充分理解流是什么,以及如何在Java应用程序中使用它们来简洁而高效地处理数据集。● 第4章介绍了流的概念,并解释它们与集合有何异同。● 第5章详细讨论了表达复杂数据处理查询可以使用的流操

作。我们会谈到很多模式,如筛选、切片、查找、匹配、映射和

归约。● 第6章讲到了收集器——Stream API的一个功能,可以让你

表达更为复杂的数据处理查询。● 在第7章中,你将了解流如何得以自动并行执行,并利用多

核架构的优势。此外,你还会学到为正确而高效地使用并行流,

要避免的若干陷阱。

第三部分探讨了能让你高效使用Java 8并在代码中运用现代语汇的若干内容。● 第8章探讨了如何利用Java 8的新功能和一些秘诀来改善你

现有的代码。此外,该章还探讨了一些重要的软件开发技术,如

设计模式、重构、测试和调试。● 在第9章中,你将了解到默认方法是什么,如何利用它们来

以兼容的方式演变API,一些实际的应用模式,以及有效使用默

认方法的规则。● 第10章谈到了新的java.util.Optional类,它能让你设计出更

好的API,并减少空指针异常。● 第11章探讨了CompletableFuture,它可以让你用声明性方

式表达复杂的异步计算,从而让Stream API的设计并行化。● 第12章探讨了新的日期和时间API,这相对于以前涉及日期

和时间时容易出错的API是一大改进。

在本书最后一部分,我们会返回来谈谈怎么用Java编写高效的函数式程序,还会将Java 8的功能和Scala作一比较。● 第13章是一个完整的函数式编程教程,介绍了一些术语,

并解释了如何在Java 8中编写函数式风格的程序。● 第14章涵盖了更高级的函数式编程技巧,包括高阶函数、

科里化、持久化数据结构、延迟列表和模式匹配。你可以把这一

章看作一种融合,既有可以用在代码库中的实际技术,也有让你

成为更渊博的程序员的学术知识。● 第15章对比了Java 8的功能与Scala的功能。Scala和Java一

样,是一种实施在JVM上的语言,近年来迅速发展,在编程语言

生态系统中已经威胁到了Java的一些方面。● 在第16章我们会回顾这段学习Java 8并慢慢走向函数式编程

的历程。此外,我们还会猜测,在Java 8之后,未来可能还有哪

些增强和新功能出现。

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

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

书中所有示例代码和执行说明均可见于https://github.com/java8/Java8InAction。你也可以从出版商网站(https://www.manning.com/java8inaction)下载包含本书所有示例的zip文件。作者在线

购买本书即可免费访问Manning Publications运营的一个私有在线论坛,你可以在那里发表关于本书的评论、询问技术问题,并获得作者和其他用户的帮助。如欲访问作者在线论坛并订阅,请用浏览器访问https://www.manning.com/java8inaction。这个页面说明了注册后如何使用论坛,能获得什么类型的帮助,以及论坛上的行为守则。

Manning对读者的承诺是提供一个平台,供读者之间以及读者和作者之间进行有意义的对话。但这并不意味着作者会有任何特定程度的参与。他们对论坛的贡献是完全自愿的(且无报酬)。我们建议你试着询问作者一些有挑战性的问题,以免他们失去兴趣!

只要本书仍在印,你就可以在出版商网站上访问作者在线论坛和先前所讨论内容的归档文件。

关于封面图

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

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

计算机书籍一度也是如此繁荣,Manning出版社在此用杰弗里斯画中复活的三个世纪前风格各异的国家服饰,来象征计算机行业中的发明与创造的异彩纷呈。第一部分基础知识

本书第一部分将介绍Java 8的基础知识。学完第一部分,你将会对Lambda表达式有充分的了解,并可以编写简洁而灵活的代码,能够轻松地适应不断变化的需求。

第1章将总结Java的主要变化(Lambda表达式、方法引用、流和默认方法),并为学习本书做好准备。

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

第3章全面地解释了Lambda表达式和方法引用的概念,每一步都有代码示例和测验。第1章为什么要关心Java 8

本章内容● Java怎么又变了● 日新月异的计算应用背景:多核和处理大型数据集(大数据)● 改进的压力:函数式比命令式更适应新的体系架构● Java 8的核心新特性:Lambda(匿名函数)、流、默认方法

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

我们的理由是,Java 8所做的改变,在许多方面比Java历史上任何一次改变都深远。而且好消息是,这些改变会让你编起程来更容易,用不着再写类似下面这种啰嗦的程序了(对inventory中的苹果按照重量进行排序):Collections.sort(inventory, new Comparator() { 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的演变路径来看,它一直致力于让并发编程更容易、出错更少。Java 1.0里有线程和锁,甚至有一个内存模型——这是当时的最佳做法,但事实证明,不具备专门知识的项目团队很难可靠地使用这些基本模型。Java 5添加了工业级的构建模块,如线程池和并发集合。Java 7添加了分支/合并(fork/join)框架,使得并行变得更实用,但仍然很困难。而Java 8对并行有了一个更简单的新思路,不过你仍要遵循一些规则,本书中会谈到。

我们用两个例子(它们有更简洁的代码,且更简单地使用了多核处理器)就可以管中窥豹,看到一座拔地而起相互勾连一致的Java 8大厦。首先让你快速了解一下这些想法(希望能引起你的兴趣,也希望我们总结得足够简洁):● Stream API● 向方法传递代码的技巧● 接口中的默认方法

Java 8提供了一个新的API(称为“流”,Stream),它支持许多处理数据的并行操作,其思路和在数据库查询语言中的思路类似——用更高级的方式表达想要的东西,而由“实现”(在这里是Streams库)来选择最佳低级执行机制。这样就可以避免用synchronized编写代码,这一代码不仅容易出错,而且在多核CPU上1执行所需的成本也比你想象的要高。

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

从有点修正主义的角度来看,在Java 8中加入Streams可以看作把另外两项扩充加入Java 8的直接原因:把代码传递给方法的简洁方式(方法引用、Lambda)和接口中的默认方法。

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

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

本章主要从宏观角度探讨了语言为什么会演变,接下来几节介绍Java 8的核心特性,然后介绍函数式编程思想——其新的特性简化了使用,而且更适应新的计算机体系结构。简而言之,1.1节讨论了Java的演变过程和概念,指出Java以前缺乏以简易方式利用多核并行的能力。1.2节介绍了为什么把代码传递给方法在Java 8里是如此强大的一个新的编程语汇。1.3节对Streams做了同样的介绍:Streams是Java 8表示有序数据,并能灵活地表示这些数据是否可以并行处理的新方式。1.4节解释了如何利用Java 8中的默认方法功能让接口和库的演变更顺畅、编译更少。最后,1.5节展望了在Java和其他共用JVM的语言中进行函数式编程的思想。总的来说,本章会介绍整体脉络,而细节会在本书的其余部分中逐一展开。请尽情享受吧!1.1 Java怎么还在变

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

2  P. 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程序员。在过去15年的时间里,Java已经成功地霸占了编程生态系统中的一大块,同时替代了竞争对手语言。让我们来看看其中的原因。1.1.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新的多核并行中存在的“因为这个所以需要那个”的依赖关系。1.1.2 流处理

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

一个实际的例子是在Unix或Linux中,很多程序都从标准输入(Unix和C中的stdin,Java中的System.in)读取数据,然后把结果写入标准输出(Unix和C中的stdout,Java中的System.out)。首先我们来看一点点背景:Unix的cat命令会把两个文件连接起来创建一个流,tr会转换流中的字符,sort会对流中的行进行排序,而tail -3则给出流的最后三行。Unix命令行允许这些程序通过管道(|)连接在一起,比如cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3

会(假设file1和file2中每行都只有一个词)先把字母转换成小写字母,然后打印出按照词典排序出现在最后的三个单词。我们说sort3把一个行流作为输入,产生了另一个行流(进行排序)作为输出,如图1-2所示。请注意在Unix中,命令(cat、tr、sort和tail)是同时执行的,这样sort就可以在cat或tr完成前先处理头几行。就像汽车组装流水线一样,汽车排队进入加工站,每个加工站会接收、修改汽车,然后将之传递给下一站做进一步的处理。尽管流水线实际上是一个序列,但不同加工站的运行一般是并行的。

3  有语言洁癖的人会说“字符流”,不过认为sort会对行排序比较简单。图 1-2 操作流的Unix命令

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

推动这种做法的关键在于,现在你可以在一个更高的抽象层次上写Java 8程序了:思路变成了把这样的流变成那样的流(就像写数据库查询语句时的那种思路),而不是一次只处理一个项目。另一个好处是,Java 8可以透明地把输入的不相关部分拿到几个CPU内核上去分别执行你的Stream操作流水线——这是几乎免费的并行,用不着去费劲搞Thread了。我们会在第4~7章仔细讨论Java 8的Stream API。1.1.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.2节中概述这种方式,但详细讨论留在第2章和第3章。第13章和第14章将讨论这一功能的高级用法,还有函数式编程自身的一些技巧。1.1.4 并行与共享的可变数据

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

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

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

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

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

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

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

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

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

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

为了帮助回答这个问题,我们要注意到,编程语言的整个目的就在于操作值,要是按照历史上编程语言的传统,这些值因此被称为一等值(或一等公民,这个术语是从20世纪60年代美国民权运动中借用来的)。编程语言中的其他结构也许有助于我们表示值的结构,但在程序执行期间不能传递,因而是二等公民。前面所说的值是Java中的一等公民,但其他很多Java概念(如方法和类等)则是二等公民。用方法来定义类很不错,类还可以实例化来产生值,但方法和类本身都不是值。这又有什么关系呢?还真有,人们发现,在运行时传递方法能将方法变成一等公民。这在编程中非常有用,因此Java 8的设计者把这个功能加入到了Java中。顺便说一下,你可能会想,让类等其他二等公民也变成一等公民可能也是个好主意。有很多语言,如Smalltalk和JavaScript,都探索过这条路。1.2.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还体现了更广义的4将函数作为值的思想,包括Lambda(或匿名函数)。比如,你现在可以写(int x) -> x + 1,表示“调用时给定参数x,就返回x + 1值的函数”。你可能会想这有什么必要呢?因为你可以在MyMathsUtils类里面定义一个add1方法,然后写MyMathsUtils::add1 嘛!确实是可以,但要是你没有方便的方法和类可用,新的Lambda语法更简洁。第3章会详细讨论Lambda。我们说使用这些概念的程序为函数式编程风格,这句话的意思是“编写把函数作为一等值来传递的程序”。

4  最初是根据希腊字母λ命名的。虽然Java中不使用这个符号,名称还是被保留了下来。1.2.2 传递代码:一个例子

来看一个例子,看看它是如何帮助你写程序的,我们在第2章还会进行更详细的讨论。所有的示例代码均可见于本书的GitHub页面(https://github.com/java8/)。假设你有一个Apple类,它有一个getColor方法,还有一个变量inventory保存着一个Apples的列表。你可能想要选出所有的绿苹果,并返回一个列表。通常我们用筛选(filter)一词来表达这个概念。在Java 8之前,你可能会写这样一个方法filterGreenApples:public static List filterGreenApples(List inventory){ List result = new ArrayList<>(); ←─result是用来累积结果的List,开始为空,然后一个个加入绿苹果 for (Apple apple: inventory){ if ("green".equals(apple.getColor())) { ←─高亮显示的代码会仅仅选出绿苹果 result.add(apple); } } return result;}

但是接下来,有人可能想要选出重的苹果,比如超过150克,于是你心情沉重地写了下面这个方法,甚至用了复制粘贴:public static List filterHeavyApples(List inventory){ List 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{ ←─写出来是为了清晰(平常只要从java.util.function导入就可以了) boolean test(T t);}static List filterApples(List inventory, Predicate p) { ←─方法作为Predicate参数p传递进去(见附注栏“什么是谓词?”) List 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参数。谓词(predicate)在数学上

常常用来代表一个类似函数的东西,它接受一个参数值,并

返回true或false。你在后面会看到,Java 8也会允许你写

Function——在学校学过函数却没学过谓

词的读者对此可能更熟悉,但用Predicate是更标准

的方式,效率也会更高一点儿,这避免了把boolean封装在

Boolean里面。1.2.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 || "brown".equals(a.getColor()) );

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

Java 8的设计师几乎可以就此打住了,要是没有多核CPU,可能他们真的就到此为止了。我们迄今为止谈到的函数式编程竟然如此强大,在后面你更会体会到这一点。本来,Java加上filter和几个相关的东西作为通用库方法就足以让人满意了,比如static Collection filter(Collection c, Predicate p);

这样你甚至都不需要写filterApples了,因为比如先前的调用filterApples(inventory, (Apple a) -> a.getWeight() > 150 );

就可以直接调用库方法filter:filter(inventory, (Apple a) -> a.getWeight() > 150 );

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

几乎每个Java应用都会制造和处理集合。但集合用起来并不总是那么理想。比方说,你需要从一个列表中筛选金额较高的交易,然后按货币分组。你需要写一大堆套路化的代码来实现这个数据处理命令,如下所示:Map> transactionsByCurrencies = new HashMap<>(); ←─建立累积交易分组的Mapfor (Transaction transaction : transactions) { ←─遍历交易的List if(transaction.getPrice() > 1000){ ←─筛选金额较高的交易 Currency currency = transaction.getCurrency(); ←─提取交易货币 List 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.toList;Map> transactionsByCurrencies = transactions.stream() .filter((Transaction t) -> t.getPrice() > 1000) ←─筛选金额较高的交易 .collect(groupingBy(Transaction::getCurrency)); ←─按货币分组

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

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

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载