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.2 Java怎么还在变 - 图1

图 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命令行允许这些程序通过管道(|)连接在一起,比如下面这段代码会假设file1file2中每行都只有一个单词,先把字母转换成小写字母,然后打印出按照词典顺序排在最后的三个单词:

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

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

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

1.2 Java怎么还在变 - 图2

图 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.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.2 Java怎么还在变 - 图3

图 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而不只是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中的新概念,并顺便指出哪一章还会详细讨论这些概念。