深入理解Java 8 Lambda(类库篇——Streams API,Collectors和并行)
2018-08-01 16:29
274 查看
深入理解Java8Lambda(类库篇——StreamsAPI,Collectors和并行)
发表于2016-09-27|分类于关于
深入理解Java8Lambda(原理篇——Java编译器如何处理lambda)
本文是深入理解Java8Lambda系列的第二篇,主要介绍Java8针对新增语言特性而新增的类库(例如StreamsAPI、Collectors和并行)。
本文是对
JavaSE8增加了新的语言特性(例如lambda表达式和默认方法),为此JavaSE8的类库也进行了很多改进,本文简要介绍了这些改进。在阅读本文前,你应该先阅读
背景(Background)
自从lambda表达式成为Java语言的一部分之后,Java集合(Collections)API就面临着大幅变化。而为现有的接口(例如
在类库中增加新的流(stream,即
改造现有的类型使之可以提供流视图(streamview);
改造现有的类型使之可以容易的使用新的编程模式,这样用户就不必抛弃使用以久的类库,例如
除了上面的改进,还有一项重要工作就是提供更加易用的并行(Parallelism)库。尽管Java平台已经对并行和并发提供了强有力的支持,然而开发者在实际工作(将串行代码并行化)中仍然会碰到很多问题。因此,我们希望Java类库能够既便于编写串行代码也便于编写并行代码,因此我们把编程的重点从具体执行细节(howcomputationshouldbeformed)转移到抽象执行步骤(whatcomputationshouldbeperfomed)。除此之外,我们还需要在将并行变的容易(easier)和将并行变的不可见(invisible)之间做出抉择,我们选择了一个折中的路线:提供显式(explicit)但非侵入(unobstrusive)的并行。(如果把并行变的透明,那么很可能会引入不确定性(nondeterminism)以及各种数据竞争(datarace)问题)
内部迭代和外部迭代(Internalvsexternaliteration)
集合类库主要依赖于外部迭代(externaliteration)。2 3 | shape.setColor(RED); } |
Java的for循环是串行的,而且必须按照集合中元素的顺序进行依次处理;
集合框架无法对控制流进行优化,例如通过排序、并行、短路(short-circuiting)求值以及惰性求值改善性能。
尽管有时for-each循环的这些特性(串行,依次)是我们所期待的,但它对改善性能造成了阻碍。
我们可以使用内部迭代(internaliteration)替代外部迭代,用户把对迭代的控制权交给类库,并向类库传递迭代时所需执行的代码。
下面是前例的内部迭代代码:
外部迭代同时承担了做什么(把形状设为红色)和怎么做(得到
流(Stream)
流是JavaSE8类库中新增的关键抽象,它被定义于流的操作可以被组合成流水线(Pipeline)。以前面的例子为例,如果我们只想把蓝色改成红色:
2 3 | .filter(s->s.getColor()==BLUE) .forEach(s->s.setColor(RED)); |
如果我们想把蓝色的形状提取到新的
2 3 4 | shapes.stream() .filter(s->s.getColor()==BLUE) .collect(Collectors.toList()); |
如果每个形状都被保存在
2 3 4 5 | shapes.stream() .filter(s->s.getColor()==BLUE) .map(s->s.getContainingBox()) .collect(Collectors.toSet()); |
如果我们需要得到蓝色物体的总重量,我们可以这样表达:
2 3 4 5 | shapes.stream() .filter(s->s.getColor()==BLUE) .mapToInt(s->s.getWeight()) .sum(); |
流和集合(StreamsvsCollections)
集合和流尽管在表面上看起来很相似,但它们的设计目标是不同的:集合主要用来对其元素进行有效(effective)的管理和访问(access),而流并不支持对其元素进行直接操作或直接访问,而只支持通过声明式操作在其上进行运算然后得到结果。除此之外,流和集合还有一些其它不同:无存储:流并不存储值;流的元素源自数据源(可能是某个数据结构、生成函数或I/O通道等等),通过一系列计算步骤得到;
天然的函数式风格(Functionalinnature):对流的操作会产生一个结果,但流的数据源不会被修改;
惰性求值:多数流操作(包括过滤、映射、排序以及去重)都可以以惰性方式实现。这使得我们可以用一遍遍历完成整个流水线操作,并可以用短路操作提供更高效的实现;
无需上界(Boundsoptional):不少问题都可以被表达为无限流(infinitestream):用户不停地读取流直到满意的结果出现为止(比如说,枚举
从API的角度来看,流和集合完全互相独立,不过我们可以既把集合作为流的数据源(
惰性(Laziness)
过滤和映射这样的操作既可以被急性求值(以对于过滤和映射这样的操作,我们很自然的会把它当成是惰性求值操作,不过它们是否真的是惰性取决于它们的具体实现。另外,像
以下面的流水线为例:
2 3 4 5 | shapes.stream() .filter(s->s.getColor()==BLUE) .mapToInt(s->s.getWeight()) .sum(); |
大多数循环都可以用数据源(数组、集合、生成函数以及I/O管道)上的聚合操作来表示:进行一系列惰性操作(过滤和映射等操作),然后用一个急性求值操作(
在使用这种数据源—惰性操作—惰性操作—急性操作流水线时,流水线中的惰性几乎是不可见的,因为计算过程被夹在数据源和最终结果(或副作用操作)之间。这使得API的可用性和性能得到了改善。
对于
2 3 4 | shapes.stream() .filter(s->s.getColor()==BLUE) .findFirst(); |
在这种设计下,用户并不需要显式进行惰性求值,甚至他们都不需要了解惰性求值。类库自己会选择最优化的计算方式。
并行(Parallelism)
流水线既可以串行执行也可以并行执行,并行或串行是流的属性。除非你显式要求使用并行流,否则JDK总会返回串行流。(串行流可以通过尽管并行是显式的,但它并不需要成为侵入式的。利用
2 3 4 5 | shapes.parallelStream() .filter(s->s.getColor=BLUE) .mapToInt(s->s.getWeight()) .sum(); |
因为流的数据源可能是一个可变集合,如果在遍历流时数据源被修改,就会产生干扰(interference)。所以在进行流操作时,流的数据源应保持不变(heldconstant)。这个条件并不难维持,如果集合只属于当前线程,只要lambda表达式不修改流的数据源就可以。(这个条件和遍历集合时所需的条件相似,如果集合在遍历时被修改,绝大多数的集合实现都会抛出
我们应避免在传递给流方法的lambda产生副作用。一般来说,打印调试语句这种输出变量的操作是安全的,然而在lambda表达式里访问可变变量就有可能造成数据竞争或是其它意想不到的问题,因为lambda在执行时可能会同时运行在多个线程上,因而它们所看到的元素有可能和正常的顺序不一致。无干扰性有两层含义:
不要干扰数据源;
不要干扰其它lambda表达式,当一个lambda在修改某个可变状态而另一个lambda在读取该状态时就会产生这种干扰。
只要满足无干扰性,我们就可以安全的进行并行操作并得到可预测的结果,即便对线程不安全的集合(例如
实例(Examples)
下面的代码源自JDK中的2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | if(method.getName().equals(enclosingInfo.getName())){ Class<?>[]candidateParamClasses=method.getParameterTypes(); if(candidateParamClasses.length==parameterClasses.length){ booleanmatches=true; for(inti=0;i<candidateParamClasses.length;i+=1){ if(!candidateParamClasses[i].equals(parameterClasses[i])){ matches=false; break; } } if(matches){//finally,checkreturntype if(method.getReturnType().equals(returnType)){ returnmethod; } } } } } thrownewInternalError("Enclosingmethodnotfound"); |
2 3 4 5 6 | .filter(m->Objects.equals(m.getName(),enclosingInfo.getName())) .filter(m->Arrays.equals(m.getParameterTypes(),parameterClasses)) .filter(m->Objects.equals(m.getReturnType(),returnType)) .findFirst() .orElseThrow(()->newInternalError("Enclosingmethodnotfound")); |
流操作特别适合对集合进行查询操作。假设有一个“音乐库”应用,这个应用里每个库都有一个专辑列表,每张专辑都有其名称和音轨列表,每首音轨表都有名称、艺术家和评分。
假设我们需要得到一个按名字排序的专辑列表,专辑列表里面的每张专辑都至少包含一首四星及四星以上的音轨,为了构建这个专辑列表,我们可以这么写:
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | for(Albumalbum:albums){ booleanhasFavorite=false; for(Tracktrack:album.tracks){ if(track.rating>=4){ hasFavorite=true; break; } } if(hasFavorite) favs.add(album); } Collections.sort(favs,newComparator<Album>(){ publicintcompare(Albuma1,Albuma2){ returna1.name.compareTo(a2.name); } }); |
2 3 4 5 | albums.stream() .filter(a->a.tracks.anyMatch(t->(t.rating>=4))) .sorted(Comparator.comparing(a->a.name)) .collect(Collectors.toList()); |
收集器(Collectors)
在之前的例子中,我们利用2 3 | albums.stream() .collect(Collectors.toMap(a->a.getCatalogNumber(),a->a)); |
2 3 4 | tracks.stream() .filter(t->t.rating>=4) .collect(Collectors.groupingBy(t->t.artist)); |
2 3 4 5 | tracks.stream() .filter(t->t.rating>=4) .collect(Collectors.groupingBy(t->t.artist, Collectors.toSet())); |
2 3 4 | tracks.stream() .collect(groupingBy(t->t.artist, groupingBy(t->t.rating))); |
2 3 4 5 | Map<String,Integer>wordFreq= tracks.stream() .flatMap(t->pattern.splitAsStream(t.name))//Stream<String> .collect(groupingBy(s->s.toUpperCase(),counting())); |
并行的实质(Parallelismunderthehood)
JavaSE7引入了为了实现并行计算,我们一般要把计算过程递归分解(recursivedecompose)为若干步:
把问题分解为子问题;
串行解决子问题从而得到部分结果(partialresult);
合并部分结果合为最终结果。
这也是Fork/Join的实现原理。
为了能够并行化任意流上的所有操作,我们把流抽象为
上面的分解方法也同样适用于其它数据结构,数据结构的作者只需要提供分解逻辑,然后就可以直接享用并行流操作带来的遍历。
大多数用户无需去实现
2 3 4 5 6 7 8 9 10 11 12 13 | //Elementaccess booleantryAdvance(Consumer<?superT>action); voidforEachRemaining(Consumer<?superT>action); //Decomposition Spliterator<T>trySplit(); //Optionalmetadata longestimateSize(); intcharacteristics(); Comparator<?superT>getComparator(); } |
出现顺序(Encounterorder)
多数数据结构(例如列表,数组和I/O通道)都拥有自然出现顺序(naturalencounterorder),这意味着它们的元素出现顺序是可预测的。其它的数据结构(例如是否具有明确定义的出现顺序是
2 3 4 | people.parallelStream() .map(Person::getName) .collect(toList()); |
JDK中的流和lambda(StreamsandlambdasinJDK)
除此之外,
最后,我们提供了一系列API用于构建流,类库的编写者可以利用这些API来在流上实现其它聚集操作。实现
比较器工厂(Comparatorfactories)
我们在静态方法
2 3 4 | Function<?superT,?extendsU>keyExtractor){ return(c1,c2)->keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2)); } |
2 | people.sort(comparing(p->p.getLastName())); |
2 3 4 | Comparator.comparing(p->p.getLastName()) .thenComparing(p->p.getFirstName()); people.sort(c); |
可变的集合操作(Mutativecollectionoperation)
集合上的流操作一般会生成一个新的值或集合。不过有时我们希望就地修改集合,所以我们为集合(例如小结(Summary)
引入lambda表达式是Java语言的巨大进步,但这还不够——开发者每天都要使用核心类库,为了开发者能够尽可能方便的使用语言的新特性,语言的演化和类库的演化是不可分割的。未完待续——
相关文章推荐
- 深入理解Java 8 Lambda(类库篇——Streams API,Collectors和并行)
- 深入理解Java 8 Lambda(类库篇——Streams API,Collectors和并行)
- [转]深入理解Java 8 Lambda(类库篇——Streams API,Collectors和并行)
- 深入理解Java 8 Lambda(类库篇——Streams API,Collectors和并行)
- 深入理解Java 8 Lambda(类库篇)
- 深入理解Java 8 Lambda-类库篇
- 深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)
- 【转载】深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)
- 深入理解Java 8 Lambda
- 深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)
- 深入理解Java 8 Lambda 语言篇 类库篇
- 深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)
- 深入理解 Java 8 Lambda
- 深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)
- 深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)
- [转]深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)
- 深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)
- 深入理解Java 8 Lambda-语言篇
- [转载]深入理解Java 8 Lambda
- 深入理解Java 8 Lambda