相关知识
lambda表达式(Lambda Expression)
方法引用(Method References)
lambda表达式(Lambda Expression)
也叫匿名函数,lambda表示式的句法:
- 括号包裹的,由逗号分割的参数列表。参数类型可以省略,如果只有一个参数,括号也可以省略
- 箭头
->
- 表达体,由一个表达式或者语句组成。如果是语句的话,必须用花括号包起来
{}
,java运行时会计算表达式并返回它的值,或者,你也可以使用return语句
// example 1
IntegerMath addition = (a, b) -> a + b;
// example 2
p -> {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
方法引用(Method References)
如果lambda表达式只是调用一个现成的方法,那么可以考虑使用方法引用,它更紧凑易读。比如,对某个数组中的成员根据年龄排序:
lambda表示是:
Arrays.sort(rosterAsArray,
(a, b) -> Person.compareByAge(a, b)
);
方法饮用:
Arrays.sort(rosterAsArray, Person::compareByAge);
这两种用法在语义上是相同的:
- 参数列表都是(Person, Person)
- 方法体都调用了Person.compareByAge
聚合
管道和流
聚合操作通过管道和流完成。管道的组成:
- 源:可以是集合、数组、生成器函数、I/O通道,通过调用源的
.stream()
方法可以产生一个流 - 零个或多个中间操作:每个操作产出一个新的流(stream),比如
- filter
- mapToInt
- map
- 一个终点操作:产出一个非流的结果,可以是基本数据类型,集合,或者不产出任何值
- forEach(不产出任何值)
- average,合并流中的元素返回一个结果,这类操作也叫做聚合(reduction),其他还有sum, min, max, count 等
- reduce方法,(类似于python中的reduce)
聚合操作和迭代器的区别
聚合操作,比如forEeach
,和迭代器(或增强的for语句)很像,但是有些本质不同:
- 聚合操作使用内部委托,而迭代器使用外部迭代。聚合操作没有
next
方法来指示它们该处理集合的下一个元素,而是使用内部委托。通过这样方式,你的程序只决定要迭代什么集合,至于怎么迭代,由JDK决定。而外部迭代是,你要决定迭代什么,以及如何迭代,并且只能串行迭代,这样无法利用并行计算的优势。 - 聚合操作从流中处理元素
- 聚合操作支持参数,可以将lambda表达式作为大部分聚合操作的参数,这使你能自定义聚合操作的行为。
reduce方法
reduce方法接收两个参数:
- 初始值(或者默认值)
- 累计器,一个函数,接收两个参数,一个是目前累计计算的结果,一个是流中的下一个元素
示例:
Integer totalAgeReduce = roster
.stream()
.map(Person::getAge)
.reduce(
0,
(a, b) -> a + b);
collect方法
reduce方法每处理一个元素都会创建一个新的值,collect方法是修改现有的值。通过自定义一种容器类型来跟踪值的变化(类似于python中利用字典的可变性来缓存值):
// 定义容器类型
class Averager implements IntConsumer
{
private int total = 0;
private int count = 0;
public double average() {
return count > 0 ? ((double) total)/count : 0;
}
public void accept(int i) { total += i; count++; }
public void combine(Averager other) {
total += other.total;
count += other.count;
}
}
// 使用容器来收集结果
Averager averageCollect = roster.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.map(Person::getAge)
.collect(Averager::new, Averager::accept, Averager::combine);
System.out.println("Average age of male members: " +
averageCollect.average());
collect方法包含三个参数:
- 供应器:一个工厂函数,创建一个新的结果容器
- 累计器:将元素累加进
- 合并器:接收两个结果容器,并合并它们的结果。在这个例子中,它修改一个Averager容器,将另一个Averager容器的count和total值加过来。
尽管已经提供了average
等操作,但如果你需要从流中的元素计算多个值,你可以使用collect
操作和自定义类。
collect方法接收Collector类型的参数
将所有男性成员放入集合中:
List<String> namesOfMaleMembersCollect = roster
.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.map(p -> p.getName())
.collect(Collectors.toList());
我们知道,collect
方法接收3个参数,而Collectors
类封装了一些函数,可以用作collect
方法的参数。Collectors
类的大部分操作返回一个Collector
实例,而不是集合,toList
也不例外。
根据性别分组:
Map<Person.Sex, List<Person>> byGender =
roster
.stream()
.collect(
Collectors.groupingBy(Person::getGender));
groupingBy
操作返回返回一个映射,健是性别,值是该性别下Person的列表。
这个操作也可以接受两个参数,比如,根据性别分组,并且只要每组下成员的名字:
Map<Person.Sex, List<String>> namesByGender =
roster
.stream()
.collect(
Collectors.groupingBy(
Person::getGender,
Collectors.mapping(
Person::getName,
Collectors.toList())));
这里接收两个参数,一个是分类函数,一个是Collector
实例,后者被称作下游收集器。一个管道如果包含一个或多个下游收集器,就称之为多层聚合。
统计每组中年龄的总和:
Map<Person.Sex, Integer> totalAgeByGender =
roster
.stream()
.collect(
Collectors.groupingBy(
Person::getGender,
Collectors.reducing(
0,
Person::getAge,
Integer::sum)));
这里的reducing
操作接收三个参数:
- 初始值,和
Stream.reduce
操作类似 - 映射函数
- 聚合操作
并行
集合不是线程安全的,但是聚合操作和并行流允许你使用集合实现并行计算,前提是你在操作集合时不要修改它。注意,并行不一定比串行更快,除非你有足够的数据和处理器核心。尽管聚合操作使你更容易实现并行,但你还是要评估下你的程序是否适合并行。
并行流
创建的流默认是串行的,要创建一个并行流,调用Collection.parallelStream
方法即可。并行执行一个流时,java运行时将流切分为多个子流,聚合操作以并行的方式遍历并处理子流,然后合并结果。
double average = roster
// 创建并行流
.parallelStream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.mapToInt(Person::getAge)
.average()
.getAsDouble();
并发聚合
使用并发聚合,根据性别分组:
ConcurrentMap<Person.Sex, List<Person>> byGender =
roster
.parallelStream()
.collect(
Collectors.groupingByConcurrent(Person::getGender));
注意例子中包含的关键字:Concurrent
顺序
Integer[] intArray = {1, 2, 3, 4, 5, 6, 7, 8};
List<Integer> listOfIntegers =
new ArrayList<>(Arrays.asList(intArray));
System.out.println("listOfIntegers: ");
listOfIntegers
.stream()
.forEach(e -> System.out.print(e + " "));
System.out.println("");
System.out.println("listOfIntegers sorted in reverse order:");
Comparator<Integer> normal = Integer::compare;
Comparator<Integer> reversed = normal.reversed();
// 对列表进行倒序排列
Collections.sort(listOfIntegers, reversed);
listOfIntegers
.stream()
.forEach(e -> System.out.print(e + " "));
System.out.println("");
System.out.println("parallel stream");
listOfIntegers
.parallelStream()
.forEach(e -> System.out.print(e + " "));
System.out.println("");
System.out.println("Another parallel stream:");
listOfIntegers
.parallelStream()
.forEach(e -> System.out.print(e + " "));
System.out.println("");
System.out.println("With forEachOrdered:");
listOfIntegers
.parallelStream()
.forEachOrdered(e -> System.out.print(e + " "));
System.out.println("");
执行结果如下:
listOfIntegers:
1 2 3 4 5 6 7 8
listOfIntegers sorted in reverse order:
8 7 6 5 4 3 2 1
parallel stream
3 4 6 2 5 8 7 1
Another parallel stream:
3 1 2 7 6 5 4 8
With forEachOrdered:
8 7 6 5 4 3 2 1
第三个和第四个管道打印的元素顺序是随机的。记住,处理流中的元素时,这些操作使用的是内部遍历。结果就是,除非由流操作另行指定,否则在并行执行一个流时,java编译器和运行时决定以什么顺序来处理流中的元素,以最大程度地发挥并行计算的优势。
第五个管道使用forEachOrdered方法,不论流是串行还是并行执行,都以源中指定的顺序来处理流中的元素。需要注意的是,在并行流中使用这种操作可能会无法利用并行计算的优势。
副作用
如果方法或者表达式除了返回或者产出值之外,还修改了计算机的状态,则它具有副作用。比如可变聚合,或者调用系统的打印方法。一个返回空的lambda表达式,比如它只是调用System.out.println
方法,除了有副作用外什么也做不了。尽管JDK能很好地处管道中某些副作用,像forEach和peek这样的方法在设计时也考虑了副作用,但是在并行流中使用这些操作也要小心,因为java运行时会从多个线程并发地调用你指定为参数的lambda表达式。切勿将lambda表达式作为参数传递,它会对filter, map等操作产生副作用。下面讨论干扰和有状态的lambda表达式,这二者是副作用的来源,尤其是在并行流中,可能返回不一致或不可预测的结果。首先需要了解惰性这个概念,因为它直接影响干扰。
惰性
如果一个表达式、方法或者算法的值只有在需要时才求值,那么它们就是惰性的。所有的中间操作都是惰性的,在终点操作开始前,中间操作不会开始处理流中的内容。这种惰性允许java编译器和运行时优化对流的处理。比如管道:filter-mapToInt-average,其中filter、mapToInt是中间操作,average是终点操作。average操作可以从mapToInt操作创建的流中获取前几个元素,而mapToInt则从filer操作创建的流中获取元素。average操作一直重复这一过程,直到它获取了流中所有的元素,然后再计算平均值。(个人理解:average需要几个值,mapToInt就从filter中拿几个,而filter就从源中拿几个)
干扰
如果一个管道正在处理流时,流的源被修改了,就会发生干扰。
try {
List<String> listOfStrings = new ArrayList<>(Arrays.asList("one", "two"));
String concatenatedString = listOfStrings
.stream()
.peek(s -> listOfStrings.add("three"))
.reduce((a, b) -> a + " " + b)
.get();
System.out.println("concatenatedString: " + concatenatedString);
} catch (Exception e) {
// ConcurrentModificationException
System.out.println(e);
}
这个示例用reduce操作(这是一个终点操作)拼接列表中的字符串。但是管道调用了中间操作peek,来尝试往列表中添加一个新的元素。所有的中间操作都是惰性的,当get被调用时,管道才开始执行。当get操作完成后,管道结束执行。peek操作的参数试图在管道执行期间修改流的源,导致异常抛出。
有状态的的lambda表达式
在流的操作中,避免使用有状态的lambda表达式作为参数。这种表达式的结果依赖于某种状态,而这种状态在管道的执行过程中可能会改变。这回导致结果的不可确定性。
Integer[] arrOfIntegers = new Integer[] {3, 2, 1, 4, 5};
List<Integer> listOfIntegers = new ArrayList<>(Arrays.asList(arrOfIntegers));
List<Integer> serialStorage = new ArrayList<>();
System.out.println("serial stream: ");
listOfIntegers
.stream()
.map( e -> {serialStorage.add(e); return e;})
.forEachOrdered(e -> System.out.print(e + " "));
System.out.println("");
serialStorage
.stream()
.forEachOrdered(e -> System.out.print(e + " "));
System.out.println("");
System.out.println("parallel stream: ");
List<Integer> parallelStorage = Collections.synchronizedList(new ArrayList<>());
listOfIntegers
.parallelStream()
// 有状态的lambda表达式:
// ForEachOrdered操作按流指定的顺序处理元素,不论流是串行还是并行执行。
// 但是,当并行执行一个流时,map操作处理由java运行时和编译器指定的流中的元素,
// 结果就是,每次运行时,当前lambda表达式添加元素的顺序都会不一样
.map(e -> {parallelStorage.add(e); return e;})
.forEachOrdered(e -> System.out.print(e + " "));
System.out.println("");
parallelStorage
.stream()
.forEachOrdered(e -> System.out.print(e + " "));
System.out.println("");
运行以上示例执行结果如下:
serial stream:
3 2 1 4 5
3 2 1 4 5
parallel stream:
3 2 1 4 5
3 4 2 1 5 // 这里的结果每次不一样
注意,上面调用synchronizedList
方法是为了确保列表是线程安全的。因为集合不是线程安全的,这意味多个线程不应该同时访问一个集合。假如我们不这么做,比如改为下面这样子:
List<Integer> parallelStorage = new ArrayList<>();
那么执行结果可能是这样的:
//parallel stream:
//3 2 1 4 5
//3 2 4 5