Java8实战-总结3
- 基础知识
- 流
- 多线程并非易事
- 默认方法
基础知识
流
几乎每个Java应用都会制造和处理集合。但集合用起来并不总是那么理想。比方说,从一个列表中筛选金额较高的交易,然后按货币分组。需要写一大堆套路化的代码来实现这个数据处理命令,如下所示:
//建立累积交易分组的Map
Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>();
//遍历交易的List
for(Transaction transaction : transactions) {
//筛选金额
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);
}
//将当前遍历的交易添加到具有同一货币的交易List中
transactionsForCurrency.add(transaction);
}
}
此外,很难一眼看出来这些代码是做什么的,因为有好几个嵌套的控制流指令。有了Stream API
,可以这样解决这个问题了:
import static java.util.stream.Collectors.toList;
Map<Currency, List<Transaction>> transactionsByCurrencies = transactions.stream()
.filter((Transaction t) -> t.getPrice() > 1000)
.collect(groupingBy(Transaction::getcurrency));
值得注意的是,和Collection API
相比,Stream API
处理数据的方式非常不同。用集合的话,得自己去做迭代的过程。得用for-each
循环一个个去迭代元素,然后再处理元素。把这种数据迭代的方法称为外部迭代。相反,有了Stream API
,根本用不着操心循环的事情。数据处理完全是在库内部进行的。把这种思想叫作内部迭代。
使用集合的另一个头疼的地方是,想想看,要是交易量非常庞大,要怎么处理这个巨大的列表呢?单个CPU
根本搞不定这么大量的数据。一台多核电脑,理想的情况下,可能想让这些CPU
内核共同分担处理工作,以缩短处理时间。理论上来说,要是有八个核,那并行起来,处理数据的速度应该是单核的八倍。
多核
所有新的台式和笔记本电脑都是多核的。它们不是仅有一个CPU,而是有四个、八个,甚至更多CPU,通常称为"内核"。问题是,经典的Java程序只能利用其中一个核,
其他核的处理能力都浪费了。类似地,很多公司利用计算集群(用高速网络连接起来的多台计算机)来高效处理海量数据。Java 8提供了新的编程风格,
可更好地利用这样的计算机。
Google的搜索引擎就是一个无法在单台计算机上运行的代码的例子。它要读取互联网上的每个页面并建立索引,将每个互联网网页上出现的每个词都映射到包含该词的网址上。
然后,如果用多个单词进行搜索,软件就可以快速利用索引,返回一个包含这些词的网页集合。想想看,如何在Java中实现这个算法,哪怕是比Google小的引擎也需要利用计算机上所有的核。
多线程并非易事
通过多线程代码来利用并行(使用先前Java
版本中的Thread
API
)并非易事线程可能会同时访问并更新共享变量。因此,如果没有协调好,数据可能会被意外改变。相比一步步执行的顺序模型,这个模型不太好理解。比如,下图就展示了如果没有同步好,两个线程同时向共享变量sum
加上一个数时,可能出现的问题。
Java 8
也用Stream API
(java.util.stream
)解决了这两个问题:集合处理时的套路和晦涩,以及难以利用多核。这样设计的第一个原因是,有许多反复出现的数据处理模式,类似于filterApples
或SQL
等数据库查询语言里熟悉的操作,如果在库中有这些就会很方便:根据标准筛选数据(比如较重的苹果),提取数据(例如抽取列表中每个苹果的重量字段),或给数据分组(例如,将一个数字列表分组,奇数和偶数分别列表)等。第二个原因是,这类操作常常可以并行化。例如下图所示,在两个CPU上筛选列表,可以让一个CPU处理列表的前一半,第二个CPU处理后一半,这称为分支步骤(1)。CPU随后对各自的半个列表做筛选(2)。最后(3),一个CPU会把两个结果合并起来(Google搜索这么快就与此紧密相关,当然他们用的CPU远远不止两个了)。
到这里,只是说新的Stream API
和Java
现有的集合API
的行为差不多:它们都能够访问数据项目的序列。不过,现在最好记得,Collection
主要是为了存储和访问数据,而Stream
则主要用于描述对数据的计算。这里的关键点在于,Stream
允许并提倡并行处理一个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 8
设计者发现的一个现实问题就是现有的接口也在改进。比如,Collections.sort
方法真的应该属于List
接口,但却从来没有放在后者里。理想的情况下,会希望做list.sort(comparator)
,而不是Collections.sort(list, comparator)
。这看起来无关紧要,但是在Java8
之前,可能会更新一个接口,然后发现把所有实现它的类也给更新了——简直是逻辑灾难!这个问题在Java 8
里由默认方法解决了。
Java中的并行与无共享可变状态
都说Java里面并行很难,而且和synchronized相关的玩意儿都容易出问题。那Java8里面有什么“灵丹妙药”呢?事实上有两个。首先,库会负责分块,
即把大的流分成几个小的流,以便并行处理。其次,流提供的这个几乎免费的并行,只有在传递给filter之类的库方法的方法不会互动(比方说有可变的共享对象)时才能工作。
但是其实这个限制对于程序员来说挺自然的,举个例子,Apple::isGreenApple就是这样。确实,虽然函数式编程中的函数的主要意思是“把函数作为一等值”,
不过它也常常隐含着第二层意思,即“执行时在元素之间无互动”。
默认方法
Java 8
中加入默认方法主要是为了支持库设计师,让他们能够写出更容易改进的接口。这一方法很重要,因为会在接口中遇到越来越多的默认方法,但由于真正需要编写默认方法的程序员相对较少,而且它们只是有助于程序改进,而不是用于编写任何具体的程序,这里举个例子。
下面这段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());
但这里有个问题:在Java8
之前,List<T>
并没有stream
或parallelStream
方法,它实现的Collection<T>
接口也没有,因为当初还没有想到这些方法。可没有这些方法,这些代码就不能编译。换作自己的接口的话,最简单的解决方案就是让Java8
的设计者把stream
方法加入Collection
接口,并加入ArrayList
类的实现。
可要是这样做,对用户来说就是噩梦了。有很多的替代集合框架都用Collection API
实现了接口。但给接口加入一个新方法,意味着所有的实体类都必须为其提供一个实现。语言设计者没法控制Collections
所有现有的实现,这下就进退两难了:如何改变已发布的接口而不破坏已有的实现呢?
Java 8
的解决方法就是打破最后一环——接口如今可以包含实现类没有提供实现的方法签名了!那谁来实现它呢?**缺失的方法主体随接口提供了(因此就有了默认实现),而不是由实现类提供。**这就给接口设计者提供了一个扩充接口的方式,而不会破坏现有的代码。Java 8
在接口声明中使用新的default
关键字来表示这一点。
例如,在Java8
里,现在可以直接对List
调用sort
方法。它是用Java8
List
接口中如下所示的默认方法实现的,它会调用Collections.sort
静态方法:
default void sort(Comparator<? super E> c) {
Collections.sort(this, c);
}
这意味着List
的任何实体类都不需要显式实现sort
,而在以前的Java
版本中,除非提供了sort
的实现,否则这些实体类在重新编译时都会失败。不过一个类可以实现多个接口,不是吗?那么,如果在好几个接口里有多个默认实现,是否意味着Java中有了某种形式的多重继承?是的,在某种程度上是这样。