为什么要关心Java8
本章内容
- Java怎么又变了
- 日新月异的计算应用背景:多核和处理大型数据集(大数据)
- 改进的压力:函数式比命令式更适应新的体系架构
- Java 8的核心新特性:Lambda(匿名函数)、流、默认方法
在Java 8里面,你可以编写更为简洁的代码,这些代码读起来更接近问题的描述:
inventory.sort(comparing(Apple::getWeight));
在Java 8之前,专家们可能会告诉你,必须利用线程才能使用多个内核。问题是,线程用起 来很难,也容易出现错误。从Java的演变路径来看,它一直致力于让并发编程更容易、出错更少。
Java 1.0里有线程和锁,甚至有一个内存模型——这是当时的最佳做法,但事实证明,不具备专 门知识的项目团队很难可靠地使用这些基本模型。Java 5添加了工业级的构建模块,如线程池和 并发集合。Java 7添加了分支/合并(fork/join)框架,使得并行变得更实用,但仍然很困难。而 Java 8对并行有了一个更简单的新思路,不过你仍要遵循一些规则,本书中会谈到。
Java8中的一些想法
-
Stream API
-
向方法传递代码的技巧
-
接口中的默认方法
Java 8提供了一个新的API(称为“流”,Stream),它支持许多处理数据的并行操作,其思路 和在数据库查询语言中的思路类似——用更高级的方式表达想要的东西,而由“实现”(在这里 是Streams库)来选择最佳低级执行机制。这样就可以避免用synchronized编写代码,这一代码 不仅容易出错,而且在多核CPU上执行所需的成本也比你想象的要高。
多核CPU的每个处理器内核都有独立的高速缓存。加锁需要这些高速缓存同步运行,然而这又需要在内核间进行 较慢的缓存一致性协议通信。
从有点修正主义的角度来看,在Java 8中加入Streams可以看作把另外两项扩充加入Java 8 的直接原因:把代码传递给方法的简洁方式(方法引用、Lambda)和接口中的默认方法。
Java 8里面将代码传递给方法的功能(同时也能够返回代码并将其包含在数据结构中)还让 我们能够使用一整套新技巧,通常称为函数式编程。一言以蔽之,这种被函数式编程界称为函数的代码,可以被来回传递并加以组合,以产生强大的编程语汇。
Java怎么还在变
Java在编程语言生态系统中的位置
流处理
第一个编程概念是流处理。介绍一下,流是一系列数据项,一次只生成一项。程序可以从输 入流中一个一个读取数据项,然后以同样的方式将数据项写入输出流。一个程序的输出流很可能是另一个程序的输入流。
用行为参数化把代码传递给方法
并行与共享的可变数据
你的行为必须能够同时对不同的输入安全地执行。一般情况下这就意味着,你写代码时不能访问共享的可变数据。这些函数有时被称为“纯 函数”或“无副作用函数”或“无状态函数”,这一点我们会在第7章和第13章详细讨论。前面说 的并行只有在假定你的代码的多个副本可以独立工作时才能进行。但如果要写入的是一个共享变 量或对象,这就行不通了。
Java 8的流实现并行比Java现有的线程API更容易,因此,尽管可以使用synchronized来打 破“不能有共享的可变数据”这一规则,但这相当于是在和整个体系作对,因为它使所有围绕这一规则做出的优化都失去意义了。在多个处理器内核之间使用synchronized,其代价往往比你 预期的要大得多,因为同步迫使代码按照顺序执行,而这与并行处理的宗旨相悖。
这两个要点(没有共享的可变数据,将方法和函数即代码传递给其他方法的能力)是我们平常所说的函数式编程范式的基石,我们在第13章和第14章会详细讨论。
Java需要演变
语言需要不断改进以跟进硬件的更新或满足程序员的期待(如果你还不够信服,想想COBOL还一度是商业上最重要的语言之一呢)。
Java中的函数
编程语言的整个目的就在于操作值,要是按照历史 上编程语言的传统,这些值因此被称为一等值(或一等公民,这个术语是从20世纪60年代美国民 权运动中借用来的)。编程语言中的其他结构也许有助于我们表示值的结构,但在程序执行期间 不能传递,因而是二等公民。前面所说的值是Java中的一等公民,但其他很多Java概念(如方法 和类等)则是二等公民。用方法来定义类很不错,类还可以实例化来产生值,但方法和类本身都 不是值。这又有什么关系呢?还真有,人们发现,在运行时传递方法能将方法变成一等公民。这在编程中非常有用,因此Java 8的设计者把这个功能加入到了Java中。顺便说一下,你可能会想, 让类等其他二等公民也变成一等公民可能也是个好主意。有很多语言,如Smalltalk和JavaScript, 都探索过这条路。
方法和Lambda作为一等公民
方法引用
|
|
语法: 对象::方法引用
(即把这个方法作为值)
在Java 8里写下 File::isHidden的时候,你就创建了一个方法引用,你同样可以传递它。第3章会详细讨论这 一概念。只要方法中有代码(方法中的可执行部分),那么用方法引用就可以传递代码。
Lambda——匿名函数
除了允许(命名)函数成为一等值外,Java 8还体现了更广义的将函数作为值的思想,包括 ① Lambda (或匿名函数)。比如,你现在可以写(int x) -> x + 1,表示“调用时给定参数x, 就返回x + 1值的函数”。你可能会想这有什么必要呢?因为你可以在MyMathsUtils类里面定义 一个add1方法,然后写MyMathsUtils::add1嘛!确实是可以,但要是你没有方便的方法和类可用,新的Lambda语法更简洁。第3章会详细讨论Lambda。我们说使用这些概念的程序为函数式编程风格,这句话的意思是“编写把函数作为一等值来传递的程序”。
传递代码:一个例子
|
|
从传递方法到Lambda
你甚至都不需要为只用一次的方法写定义;代码更干净、更清晰,因为你用不着去找自己到底传递了什么代码。但要是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中有一整套新的类集合 API——Stream,它有一套函数式程序员熟悉的、类似于filter的操作,比如map、reduce,还 有我们接下来要讨论的在Collections和Streams之间做转换的方法。
流
用集合的话,你得自己去做迭代的过程。你得用for-each循环一个个去迭代元素,然后再处理元素。我们把这种数据迭代的方法称为外部迭代。相反,有了Stream API,你根本用不着操心循环的事情。数据处 理完全是在库内部进行的。我们把这种思想叫作内部迭代。
多线程并非易事
通过多线程代码来利用并行(使用先前Java版本中的Thread API)并非易事。你 得换一种思路:线程可能会同时访问并更新共享变量。因此,如果没有协调好 ,数据可能会被 意外改变。
传统上是利用synchronized关键字,但是要是用错了地方,就可能出现很多难以察觉的错误。Java 8基于Stream的并行提倡很少使用synchronized的函数式编程风格,它关注数据分块而不是协调访问。
Java 8也用Stream API(java.util.stream)解决了这两个问题:集合处理时的套路和晦涩,以及难以利用多核。
Collection主要是为了存储和访问数据,而Stream则主要用 于描述对数据的计算。这里的关键点在于,Stream允许并提倡并行处理一个Stream中的元素。
Java中的并行与无共享可变状态
大家都说Java里面并行很难,而且和synchronized相关的玩意儿都容易出问题。那Java 8里面有什么“灵丹妙药”呢?事实上有两个。首先,库会负责分块,即把大的流分成几个小的流,以便并行处理。其次,流提供的这个几乎免费的并行,只有在传递给filter之类的库方法的方法不会互动(比方说有可变的共享对象)时才能工作。但是其实这个限制对于程序员来说挺自然的,举个例子,我们的Apple::isGreenApple就是这样。确实,虽然函数式编程中的函数的主要意思是“把函数作为一等值”,不过它也常常隐含着第二层意思,即“执行时在元素之间无互动”。
默认方法
Java 8中加入默认方法主要是为了支持库设计师,让他们能够写出更容易改进的接口。
接口如今可以包含实现类没有提供实现的方法签名 了!那谁来实现它呢?缺失的方法主体随接口提供了(因此就有了默认实现),而不是由实现类提供。
这就给接口设计者提供了一个扩充接口的方式,而不会破坏现有的代码。Java 8在接口声明 中使用新的default关键字来表示这一点。
eg:
|
|
如果在好几个接口里有多个默认实现, 是否意味着Java中有了某种形式的多重继承?是的,在某种程度上是这样。我们在第9章中会谈 到,Java 8用一些限制来避免出现类似于C++中臭名昭著的菱形继承问题。
java 8中抽象类与接口的异同
相同点
- 都是抽象类型;
- 都可以有实现方法(以前接口不行);
- 都可以不需要实现类或者继承者去实现所有方法,(以前不行,现在接口中默认方法不需要实现者实现)
不同点
- 抽象类不可以多重继承,接口可以(无论是多重类型继承还是多重行为继承);
- 抽象类和接口所反映出的设计理念不同。其实抽象类表示的是"is-a"关系,接口表示的是"like-a"关系;
- 接口中定义的变量默认是public static final 型,且必须给其初值,所以实现类中不能重新定义,也不能改变其值;抽象类中的变量默认是 friendly(不加修饰符) 型,其值可以在子类中重新定义,也可以重新赋值。
来自函数式编程的其他好思想
Optional<T>
常见的函数式语言,如SML、OCaml、Haskell,还提供了进一步的结构来帮助程序员。其中 之一就是通过使用更多的描述性数据类型来避免null。确实,计算机科学巨擘之一托尼·霍尔 (Tony Hoare)在2009年伦敦QCon上的一个演讲中说道:
我把它叫作我的“价值亿万美金的错误”。就是在1965年发明了空引用……我无法 抗拒放进一个空引用的诱惑,仅仅是因为它实现起来非常容易。
在Java 8里有一个Optional
(结构)模式匹配
这在数学中也有使用,例如: f(0) = 1 f(n) = n*f(n-1) otherwise
在Java中,你可以在这里写一个if-then-else语句或一个switch语句。其他语言表明,对 于更复杂的数据类型,模式匹配可以比if-then-else更简明地表达编程思想。
,Java 8对模式匹 配的支持并不完全,虽然我们会在第14章中介绍如何对其进行表达。与此同时,我们会用一个以 Scala语言(另一个使用JVM的类Java语言,启发了Java在一些方面的发展;请参阅第15章)表达 的例子加以描述。比方说,你要写一个程序对描述算术表达式的树做基本的简化。给定一个数据 类型Expr代表这样的表达式,在Scala里你可以写以下代码,把Expr分解给它的各个部分,然后 返回另一个Expr:
|
|
这里,Scala的语法expr match就对应于Java中的switch (expr)。
模式匹配这个术语有两个意思, 这里我们指的是数学和函数式编程上所用的, 即函数是分情况定义的, 而不是使用 if-then-else。它的另一个意思类似于“在给定目录中找到所有类似于IMG*.JPG形式的文件”,和所谓的正则 表达式有关。
小结
以下是你应从本章中学到的关键概念。
- 请记住语言生态系统的思想,以及语言面临的“要么改变,要么衰亡”的压力。虽然Java 可能现在非常有活力,但你可以回忆一下其他曾经也有活力但未能及时改进的语言的命 运,如COBOL。
- Java 8中新增的核心内容提供了令人激动的新概念和功能,方便我们编写既有效又简洁的程序。
- 现有的Java编程实践并不能很好地利用多核处理器。
- 函数是一等值;记得方法如何作为函数式值来传递,还有Lambda是怎样写的。
- Java 8中Streams的概念使得Collections的许多方面得以推广,让代码更为易读,并允许并行处理流元素。
- 你可以在接口中使用默认方法,在实现类没有实现方法时提供方法内容。
- 其他来自函数式编程的有趣思想,包括处理null和使用模式匹配。