概述部分
使用Java8新的API,流(Stream),它支持许多处理数据的并行操作,由Stream库来选择最佳低级执行机制可以避免使用Synchronized
编写代码,这种代码不仅容易出错,并且在多核CPU上的执行成本也很高。
因为多核CPU每个处理器内核都有独立的高速缓存。加锁需要这些高速缓存同步运行,而这又需要内核间进行缓慢的缓存一致性协议通信。
Streams的作用不仅仅是把代码传递给方法,它提供了一种新的间接地表达行为参数化的方法。比如,对于两个只有几行代码不同的方法,只需要把不同的那部分代码作为参数传递进去就可以。
流处理
流是一系列数据项,一次只生成一项。
程序从输入流中一个一个读取数据项。以同样的方式将数据项写入输出流。
基于Unix操作流的思想,Java 8在java.util.stream
中添加了一个Stream API
;Stream API
的很多方法可以链接起来形成一个复杂的流水线。
Java 8可以透明的把输入的不相关部分拿到几个CPU内核上去分别执行Stream操作流水线,并不需要Thread。
Unix操作流示例:
如cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3
将两个文件连接起来创建一个流,tr会转换流中的字符,sort会对流中的行进行排序,而tail -3则给出流的最后三行,这些程序通过管道(|)连接在一起。
用行为参数化把代码传递给方法
Java 8增加了通过API来传递代码的能力,把方法(代码)作为参数传递给另一个方法。
并行与共享的可变数据
行为要能够同时对不同的输入安全地执行,这意味着写代码不能访问共享的可变数据。
Java8的流实现并行比Java现有的线程API更容易,虽然可以使用Synchronized
来打破“不能有共享的可变数据”的规则,但是同步迫使代码按照顺序执行,这与并行处理相悖。
没有共享的可变数据和将方法和函数(即代码)传递给其他方法的能力是函数式编程范式的基石。
不能有共享的可变数据的要求意味着,一个方法可以通过它将参数值转换为结果的方法完全描述的,就像是一个数学函数,没有可见的副作用。
Java中的函数
Java 8的函数指的是 方法,尤其是静态方法,没有副作用的函数。
Java 可能操作的值: 原始值,对象(严格来说是对象的引用)。其他的结构(二等公民)虽然有助于表示值的结构,但它们程序执行期间并不能传递。程序之间期间能传递的是值(一等公民)。虽然用方法来定义类,类可以通过实例化产生值,但是类和方法本身都不是值。人们又发现,通过在运行时传递方法*能够将方法变成值来传递。
让方法等概念作为值(一等公民)会让编程变得容易很多。
方法和Lambda作为一等公民
Java 8的一个新功能是方法引用。
比如要编写一个方法,然后给它一个File,它会告诉你文件是不是隐藏的,在Java 8之前可能要这么写:
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
public boolean accept(File file) {
return file.isHidden();
}
});
有点绕,在Java8里,可以把代码重写成这个样子:
File[] hiddenFiles = new File(".").listFiles(File::isHidden);
要将方法作为值传给另一个方法,只需用方法引用::
语法(即“把这个方法作为值”)将其传给另一个方法即可(这里还使用了函数代表方法)。
这样做的好处是代码读起来更接近问题的陈述,方法生成升为了“一等公民”。
与对象引用传递对象类似(对象引用使用new创建),当写下XXX::MethodName
的时候,就创建了一个方法引用,同样也可以传递它。以前的Java版本只能把方法包裹在FileFilter对象里,然后才能传递给别的方法。
Lambda --- 匿名函数
除了允许函数成为一等值外,Java 8还体现了更广义的将函数作为值的思想------Lambda(匿名函数)
函数式编程风格,即“编写把函数作为一等值来传递的程序”
传递代码的例子
假设有一个Apple类,它有一个getColor方法,还有一个变量inventory保存着一个Apples的列表。要选出所有的绿苹果并返回一个列表。在Java 8之前,可能会写一个这样的方法:
public static List filterGreenApples(List inventory){
List result = new ArrayList<>();
for (Apple apple: inventory){
//代码会仅仅选出绿色苹果,并加入result中
if ("green".equals(apple.getColor())) {
result.add(apple);
}
}
return result;
}
接下来,又想要选出重的苹果,比如超过150克的,于是又写了如下方法:
public static List filterGreenApples(List inventory){
List result = new ArrayList<>();
for (Apple apple: inventory){
//代码会仅仅选出重量大于苹果,并加入result中
if (apple.equals(apple.getWeight() > 150 ) {
result.add(apple);
}
}
return result;
}
这两个方法只有一行不同,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 static List filterApples(List inventory, Predicate p){
List result = new ArrayList<>();
for(Apple apple : inventory){
if(p.test(apple)){
result.add(apple);
}
}
return result;
}
注意filterApples
的参数: Predicate
,这里是从java.util.function.predicate
导入的,它的作用是定义一个泛型接口:
public interface Predicate {
boolean test(T t);
}
通过这样,可以将方法通过谓语(Predicate)参数p传递进filterApples。
要使用 filterApples
的话,可以写成这样:
List greenApples = filterApples(inventory, FilteringApples::isGreenApple);
或者
List heavyApples = filterApples(inventory, FilteringApples::isHeavyApple);
Predicate(谓语)在数学上常用来代表一个类似函数的东西,它接受一个参数值,并返回true或false。
从传递方法到Lamda
除了把方法作为值来传递以外,Java 8还引入了一套记法(匿名函数或Lambda),于是上面的代码可以写成:
List greenApples2 = filterApples(inventory,
(Apple a) -> "green".equals(a.getColor()));
或者
List heavyApples2 = filterApples(inventory,
(Apple a) -> a.getWeight() > 150);
甚至
List weirdApples = filterApples(inventory,
(Apple a) -> a.getWeight() < 80 || "brown".equals(a.getColor()));
通过这种方法,你甚至不需要为只用一次的方法写定义;代码更干净清晰。
Q:什么时候使用方法作为值传递,什么时候使用Lambda?
A:要是代码的长度多于几行,使用Lambda表示的效果并不是一目了然,这样还是应该使用方法引用来指向一个方法,而不是使用匿名的Lambda。简言之,使用哪种方法应该以代码的清晰度为准绳。
流
通过Stream和Lamdba表达式,可以将之前的筛选代码改进成如下形式:
import static java.util.stream.Collector.toList;
List heavyApples =
inventory.stream().filter((Apple a) -> a.getWeight() > 150).collect(toList());
并行处理:
import static java.util.stream.Collector.toList;
List heavyApples =
inventory.parallelstream().filter((Apple a) -> a.getWeight() > 150).collect(toList());
和Collection API相比,Stream API处理数据的方式非常不同,用集合需要自己去做迭代的过程。你需要用for-each循环一个个去迭代元素、处理元素,这种数据迭代的方法被称为外部迭代。而有了Stream API,数据处理完全在库内部进行,这种迭代思想被称为内部迭代。
Java 8用Stream API
解决了两个问题:
- 集合处理时的套路和晦涩
- 难以利用多核
Colletion主要是为了存储和访问数据,而Stream则主要用于描述对数据的计算。关键点在于,Stream允许并提倡并行处理一个Stream中的元素。