初识Java8新特性Lambda 之collections
Posted 音译昌
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了初识Java8新特性Lambda 之collections相关的知识,希望对你有一定的参考价值。
背景(Background)
如果从一开始就将lambda表达式(闭包)作为Java语言的一部分,那么我们的Collections API肯定会与今天的外观有所不同。随着Java语言获得作为JSR 335一部分的lambda表达式,这具有使我们的Collections接口看起来更加过时的副作用。尽管可能很想从头开始并构建替换的Collection框架(“ Collections II”),但是替换Collection框架将是一项主要任务,因为Collections接口遍布JDK库。相反,我们将继续增加扩展方法,现有的接口(如进化策略Collection,List或Iterable),或者以新的接口(如“流”)被改装到现有的类,使许多希望的成语,而没有让人们买卖其值得信赖的ArrayListS和HashMap秒。(这并不是说Java永远不会有一个新的Collections框架;很明显,现有的Collections框架存在局限性,而不仅仅是为lambda设计的。除此以外,创建一个经过改进的Collections框架是一个很好的考虑因素。 JDK的未来版本。)
并行性是这项工作的重要推动力。因此,要鼓励那些成语是非常重要的两个 sequential-和并行友好。我们主要通过较少地关注就地突变而更多地关注产生新值的计算来实现这一目标。在使并行变得容易但不能使其变得不可见之间取得平衡也是重要的; 我们的目标是 为新旧系列提供明确但不干扰的并行性。
内部与外部迭代(Internal vs external iteration)
Collections框架依赖于外部迭代的概念,其中a Collection 提供了一种方法来为其客户端枚举其元素(Collectionextends Iterable),并且客户端使用它来顺序地遍历一个集合的元素。例如,如果我们想将一组块中的每个块的颜色设置为红色,则可以这样写:
for (Block b : blocks) {
b.setColor(RED);
}
这个例子说明了外部迭代。for-each循环调用的iterator() 方法blocks,然后一步一步地遍历集合。外部迭代非常简单,但是存在几个问题:
- 它本质上是串行的(除非该语言提供了用于并行迭代的结构,而Java没有提供),并且需要按集合指定的顺序处理元素。
- 它使库方法失去了管理控制流的机会,该方法可能能够利用数据的重新排序,并行性,短路或惰性来提高性能。
有时需要严格指定for-each loop (sequential, in-order) ,但这有时会妨碍性能。
外部迭代的替代方法是内部迭代,它不是控制迭代,而是将其委托给库,并传递代码片段以在计算的各个点执行。
上一个示例的内部迭代等效项是:
blocks.forEach(b -> { b.setColor(RED); });
这种方法将控制流管理从客户端代码转移到库代码,从而使库不仅可以对通用控制流操作进行抽象,而且还可以使它们潜在地使用延迟,并行和无序执行来提高性能。 。(是否实现forEach这些操作实际上是由库实现forEach者决定的,但是对于内部迭代,至少是可能的,而对于外部迭代,则不是。)
内部迭代使其具有一种编程样式,其中可以将操作“管道化”在一起。例如,如果我们只想将蓝色块涂成红色,我们可以说:
blocks.filter(b -> b.getColor() == BLUE)
.forEach(b -> { b.setColor(RED); });
该滤波器操作产生匹配所提供的条件值的流,并且所述滤波操作的结果被管道输送到forEach。
如果我们想将蓝色块收集到一个新的中List,我们可以说:
List<Block> blue = blocks.filter(b -> b.getColor() == BLUE)
.into(new ArrayList<>());
如果每个方框都包含在一个Box中,并且我们想知道哪些方框至少包含一个蓝色方框,我们可以说:
Set<Box> hasBlueBlock = blocks.filter(b -> b.getColor() == BLUE)
.map(b -> b.getContainingBox())
.into(new HashSet<>());
如果我们希望将蓝色块的总重量相加,则可以表示为:
int sum = blocks.filter(b -> b.getColor() == BLUE)
.map(b -> b.getWeight())
.sum();
到目前为止,我们还没有写下这些操作的签名-这些将在后面显示。此处的示例仅说明了内部迭代可以轻松解决的问题类型,并说明了我们希望在集合中公开的功能。
惰性作用(The role of laziness)
像过滤或映射这样的操作,可以“急切地”执行(过滤在从过滤方法返回时完成),也可以“懒惰地”执行(过滤只在开始迭代过滤方法结果的元素时完成)。将自己应用于懒惰的实现,这通常会导致显著的性能改进。我们可以将这些操作视为“自然懒惰”,不管它们是否被实现。另一方面,像积累这样的操作,或者产生副作用,比如把结果倾注到一个集合中或者为每一个元素做一些事情(比如打印出来),都是“自然渴望的”。
基于对许多现有循环的检查,可以从数据源(数组或集合)中绘制大量操作,进行一系列的惰性操作(过滤、映射等),然后进行一个急切操作(如过滤器映射累加),重述(通常在过程中明显变小)。因此,大多数自然懒惰操作倾向于用来计算临时中间结果,并且我们可以利用这个属性来生成更高效的库。(例如,一个延迟地进行过滤或映射的库可以将filter map accumulate之类的管道融合到数据的一个传递中,而不是三个不同的传递中;一个急切地进行过滤或映射的库不能。类似地,如果我们在寻找与某一特性匹配的第一个元素,那么一个懒惰的方法让我们得到了检查较少元素的答案。
这个观察结果提供了一个关键的设计选择:filter和map的返回值应该是什么?其中一个候选者是list.filter返回一个新的list,这将推动我们朝着一个全力以赴的方向前进。这是直截了当的,但最终可能做得比我们真正需要的更多。另一种方法是为显式懒惰创建一个全新的抽象集——LaZyLIST、LaZySeT等(但请注意,懒惰的集合仍然具有触发急切计算的操作——例如大小)。并且,这种方法有可能演变成像MutableSynchronizedLazySortedSet等类型的组合爆炸。
我们首选的方法是将自然懒惰操作当作返回一个流(例如迭代)而不是一个新的集合(无论如何它可能被下一个流水线阶段丢弃)。将此应用于上面的示例,过滤器从源(可能是另一个流)中提取并生成与所提供的谓词匹配的值流。在大多数潜在的懒惰操作被应用到聚合的情况下,这恰好是我们想要的——一个可以传递到流水线中的下一个阶段的值流。目前,迭代是流的抽象,但这是一个明确的临时选择,我们将很快重新访问,可能创建一个流抽象,它没有迭代问题(固有检查然后行为;假设底层源的可变异性;生活在Java.Lang.)中。
流方法的优点是,当用于源代码惰性惰性渴望管道时,惰性通常是看不见的,因为管道两端都是“密封的”,但是在不显著增加库的概念表面积的情况下,可以获得良好的可用性和性能。
流(Streams)
下面显示了一组stream操作。这些方法本质上是顺序的,在上游迭代器返回的顺序中处理元素(遇到顺序)。在当前的实现中,我们使用Iterable作为这些方法的宿主。返回Iterable的方法是懒惰的;那些不急于返回的方法是懒惰的。所有这些操作都可以通过默认的方法单独实现Iterator(),因此现有Collection实现不需要额外的工作来获取新的功能。还请注意,Stream功能仅与集合成切线关系;如果备用收集框架想要获取这些方法,则它们所需要做的只是实现 Iterable。
流(Stream)在几个方面与集合(Collections )不同:
- 没有存储空间。 流没有存储值。它们通过一系列操作从数据结构中携带值。
- 本质上是功能性的。 对流的操作会产生结果,但不会修改其基础数据源。可以将Collection用作流的源(取决于适当的无干扰要求,请参见下文)。
- 懒惰寻求。 许多流操作(例如过滤,映射,排序或重复删除)都可以延迟实施,这意味着我们只需要检查流中要查找所需答案的元素数量即可。例如,“查找第一个大于20个字符的字符串”不需要检查所有输入字符串。
- 边界可选。 有很多问题可以明智地表达为无限流,让客户消费价值直到满意为止。(如果我们要枚举完美的数字,则可以很容易地将其表示为对所有整数流进行过滤的操作。)集合不允许您这样做,但是流可以这样做。
下面显示了一组基本的流操作,表示为上的扩展方法Iterable。
public interface Iterable<T> {
// Abstract methods
Iterator<T> iterator();
// Lazy operations
Iterable<T> filter(Predicate<? super T> predicate) default ...
<U> Iterable<U> map(Mapper<? super T, ? extends U> mapper) default ...
<U> Iterable<U> flatMap(Mapper<? super T, ? extends Iterable<U>> mapper) default ...
Iterable<T> cumulate(BinaryOperator<T> op) default ...
Iterable<T> sorted(Comparator<? super T> comparator) default ...
<U extends Comparable<? super U>> Iterable<T> sortedBy(Mapper<? super T, U> extractor) default ...
Iterable<T> uniqueElements() default ...
<U> Iterable<U> pipeline(Mapper<Iterable<T>, ? extends Iterable<U>> mapper) default ...
<U> BiStream<T, U> mapped(Mapper<? super T, ? extends U> mapper) default ...
<U> BiStream<U, Iterable<T>> groupBy(Mapper<? super T, ? extends U> mapper) default ...
<U> BiStream<U, Iterable<T>> groupByMulti(Mapper<? super T, ? extends Iterable<U>> mapper) default ...
// Eager operations
boolean isEmpty() default ...;
long count() default ...
T getFirst() default ...
T getOnly() default ...
T getAny() default ...
void forEach(Block<? super T> block) default ...
T reduce(T base, BinaryOperator<T> reducer) default ...
<A extends Fillable<? super T>> A into(A target) default ...
boolean anyMatch(Predicate<? super T> filter) default ...
boolean noneMatch(Predicate<? super T> filter) default ...
boolean allMatch(Predicate<? super T> filter) default ...
<U extends Comparable<? super U>> T maxBy(Mapper<? super T, U> extractor) default ...
<U extends Comparable<? super U>> T minBy(Mapper<? super T, U> extractor) default ...
}
懒惰和短路(Laziness and short-circuiting)
类似anyMatch的方法,虽然是急性的,但一旦可以确定最终结果,便可以使用short-circuiting来停止处理-它只需要对足够多的元素进行谓词评估就可以找到该谓词为真的单个元素。
在像这样的传输中:
int sum = blocks.filter(b -> b.getColor() == BLUE)
.map(b -> b.getWeight())
.sum();
在filter和map操作是惰性的。这意味着在sum步骤开始之前,我们不会从源头开始绘制元素,从而最大程度地减少了管理中间元素所需的簿记成本。
另外,给定一个类似的传输方式:
Block firstBlue = blocks.filter(b -> b.getColor() == BLUE)
.getFirst();
由于筛选器步骤是惰性的,因此该getFirst步骤将仅在上游进行,Iterator直到获得一个元素为止,这意味着我们只需要对元素上的谓词求值,直到找到该谓词为真的元素为止,而不是所有元素都为真。
请注意,用户不必询问懒惰,甚至不必考虑太多。正确的事情发生了,库安排了尽可能少的计算。
用户可以按以下方式调用:
Iterable<Block> it = blocks.filter(b -> b.getColor() == BLUE);
并从中获得一个Iterator,尽管我们尝试将功能集设计为不需要这种用法。在这种情况下,此操作只会创建一个Iterable,但除了保留对上游Iterable(blocks)及其Predicate过滤对象的引用之外,不会做任何其他工作。Iterator 从this获得an以后,所有工作都将完成Iterable。
通用功能接口(Common functional interfaces)
Java中的Lambda表达式将转换为一种方法接口(功能性接口)的实例。该软件包java.util.functions包含功能接口的“入门套件”:
- Predicate -- 作为参数传递的对象的属性
- Block -- 将对象作为参数传递时要执行的操作
- Mapper -- 将T转换为U
- UnaryOperator -- 来自T-> T的一元运算符
- BinaryOperator -- 来自(T,T)-> T的二进制运算符
出于性能原因,可能需要提供这些核心接口的专门的原始版本。(在这种情况下,可能不需要完整的原始特征的补充;如果我们提供Integer、Long和Double,则可以通过转换来容纳其他原始类型。)。
不干扰假设(Non-interference assumptions)
因为Iterable可以描述一个可变的集合,所以如果在遍历集合时修改它,就有可能产生干扰。Iterable上的新操作将在操作期间保持基础源不变的情况下使用。(这种情况一般容易维持;如果集合仅限于当前线程,只需确保传递给filter、map等的lambda表达式不会改变底层集合。这个条件与当前迭代集合的限制没有本质的不同;如果一个集合在迭代时被修改,大多数实现都会抛出ConcurrentModificationException。)在上面的示例中,我们通过过滤一个集合来创建一个Iterable,遍历过滤后的Iterable时遇到的元素是基于底层集合的迭代器返回的元素。因此,对iterator()的重复调用将导致对上游迭代的重复遍历;这里没有缓存延迟计算的结果。(因为大多数管道看起来都是源代码-延迟-延迟-等待,所以大多数时候底层集合只会被遍历一次。)
实例(Examples)
下面是JDK类class (getEnclosingMethod方法)的一个例子,它遍历所有声明的方法、匹配的方法名、返回类型以及参数的数量和类型。原始代码如下:
for (Method m : enclosingInfo.getEnclosingClass().getDeclaredMethods()) {
if (m.getName().equals(enclosingInfo.getName()) ) {
Class<?>[] candidateParamClasses = m.getParameterTypes();
if (candidateParamClasses.length == parameterClasses.length) {
boolean matches = true;
for(int i = 0; i < candidateParamClasses.length; i++) {
if (!candidateParamClasses[i].equals(parameterClasses[i])) {
matches = false;
break;
}
}
if (matches) { // finally, check return type
if (m.getReturnType().equals(returnType) )
return m;
}
}
}
}
throw new InternalError("Enclosing method not found");
使用filter和getFirst,我们可以消除所有临时变量,并将控制逻辑移到库中。我们从反射中获取方法列表,将其转换为一个可迭代的数组。asList(我们也可以向数组类型中注入类似流的接口),然后使用一系列过滤器来拒绝不匹配名称、参数类型或返回类型的过滤器:
Method matching =
Arrays.asList(enclosingInfo.getEnclosingClass().getDeclaredMethods())
.filter(m -> Objects.equals(m.getName(), enclosingInfo.getName())
.filter(m -> Arrays.equals(m.getParameterTypes(), parameterClasses))
.filter(m -> Objects.equals(m.getReturnType(), returnType))
.getFirst();
if (matching == null)
throw new InternalError("Enclosing method not found");
return matching;
这个版本的代码更紧凑,更不易出错。
流操作对于集合上的特别查询非常有效。考虑一个假设的“音乐库”应用程序,其中一个库有一个专辑列表,一个专辑有一个标题和一个曲目列表,一个曲目有一个名称、歌手和评级。
考虑这样的查询“为我找到至少有一首排名在4或4以上的专辑的名字,按名字排序。”为了构造这个集合,我们可以这样写:
List<Album> favs = new ArrayList<>();
for (Album a : albums) {
boolean hasFavorite = false;
for (Track t : a.tracks) {
if (t.rating >= 4) {
hasFavorite = true;
break;
}
}
if (hasFavorite)
favs.add(a);
}
Collections.sort(favs, new Comparator<Album>() {
public int compare(Album a1, Album a2) {
return a1.name.compareTo(a2.name);
}});
我们可以使用流操作来简化三个主要步骤中的每一个——确定专辑中的任何曲目是否至少在(anyMatch)上有一个评级,排序,以及将符合我们标准的专辑集合放入一个列表:
List<Album> sortedFavs =
albums.filter(a -> a.tracks.anyMatch(t -> (t.rating >= 4)))
.sortedBy(a -> a.name)
.into(new ArrayList<>());
非线性流(Nonlinear streams)
如上所述,“obvious”的流形状是简单的线性值流,例如可以由数组或Collection管理的值。我们可能还想表示其他常见的形状,例如(键,值)对的流(可能限制了键的唯一性。)
将双值流表示为值流可能会很方便Pair<X,Y>。这将很容易,并允许我们重用现有的流机制,但会产生一个新问题:如果我们可能想对键值流执行新操作(例如将其拆分为a keys或 values流),则擦除操作会进入方式-我们无法表达仅在类的类型变量满足某些约束(例如a)的情况下存在的方法Pair。(这是C#静态扩展方法的一个优点,它被注入实例化的泛型类型而不是类中,这是毫无价值的。)此外,将双值流建模为的流。Pair对象可能会有大量的“装箱”开销。通常,每个不同的流“形状”可能都需要其自己的流抽象,但这并不是不合理的,因为每个不同的形状将具有在该形状上有意义的自己的一组操作。
因此,我们使用一个单独的抽象为双值流建模,我们将其暂时称为BiStream。因此我们的流库具有两个基本的流形状:linear (Iterable) 和map-shaped (BiStream),就像Collections框架具有两个基本形状(Collection and Map)一样。
双值流可以对“ zip”运算的结果,地图的内容或分组运算的结果(其中结果为BiStream<U, Stream
Map<Integer, Integer>
counts = document.words() // stream of strings
.groupBy(s -> s.length()) // bi-stream length -> stream of words with that length
.mapValues(stream -> stream.count()) // bi-stream length -> count of words
.into(new HashMap<>()); // Map length -> count
并行性(Parallelism)
虽然内部迭代的使用使得操作可以并行完成,但是我们不希望给用户带来任何“透明的并行性”。相反,用户应该能够以一种显式但不显眼的方式选择并行性。我们通过允许客户显式地请求集合的“并行视图”来实现这一点,集合的操作是并行执行的;这是通过parallel()方法在集合上公开的。如果我们想要并行计算我们的“蓝色块的权重和”查询,我们只需要添加一个调用parallel():
int sum = blocks.parallel()
.filter(b -> b.getColor() == BLUE)
.map(b -> b.getWeight())
.sum();
这看起来与串行版本非常相似,但是被明确地标识为并行的,而没有并行机制压倒代码。
有了Java SE 7中添加的Fork/Join框架,我们就有了实现并行操作的高效机制。然而,这项工作的目标之一是减少相同计算的串行和并行版本之间的差距,目前使用Fork/Join并行化计算与串行代码看起来非常不同(而且比串行代码大得多)——这是并行化的障碍。通过公开流操作的并行版本,并允许用户显式地在串行和并行执行之间进行选择,我们可以极大地缩小这一差距。
使用Fork/Join实现并行计算所涉及的步骤是:将问题划分为子问题,按顺序解决子问题,并组合子问题的结果。Fork/Join机制被设计成自动化这个过程。
我们对Fork/Join的结构需求进行了建模,并使用了一个称为Splittable的分割抽象,它描述了可以进一步分割成更小块的子聚合,或者其元素可以按顺序迭代的子聚合。
public interface Splittable<T, S extends Splittable<T, S>> {
/** Return an {@link Iterator} for the elements of this split. In general, this method is only called
* at the leaves of a decomposition tree, though it can be called at any level. */
Iterator<T> iterator();
/** Decompose this split into two splits, and return the left split. If further splitting is impossible,
* {@code left} may return a {@code Splittable} representing the entire split, or an empty split.
*/
S left();
/** Decompose this split into two splits, and return the right split. If further splitting is impossible,
* {@code right} may return a {@code Splittable} representing the entire split, or an empty split.
*/
S right();
/**
* Produce an {@link Iterable} representing the contents of this {@code Splittable}. In general, this method is
* only called at the top of a decomposition tree, indicating that operations that produced the {@code Spliterable}
* can happen in parallel, but the results are assembled for sequential traversal. This is designed to support
* patterns like:
* collection.filter(t -> t.matches(k))
* .map(t -> t.getLabel())
* .sorted()
* .sequential()
* .forEach(e -> println(e));
* where the filter / map / sort operations can occur in parallel, and then the results can be traversed
* sequentially in a predicatable order.
*/
Iterable<T> sequential();
}
为常见的数据结构(如基于数组的列表、二叉树和映射)实现Splittable非常简单。
我们使用Iterable来描述顺序集合,这意味着一个集合知道如何按顺序分配它的成员。Iterable的并行模拟体现了可拆分的行为,以及类似于Iterable上的聚合操作。我们目前将其称为ParallelIterable。
public interface ParallelIterable<T> extends Splittable<T, ParallelIterable<T>> {
// Lazy operations
ParallelIterable<T> filter(Predicate<? super T> predicate) default ...
<U> ParallelIterable<U> map(Mapper<? super T, ? extends U> mapper) default ...
<U> ParallelIterable<U> flatMap(Mapper<? super T, ? extends Iterable<U>> mapper) default ...
ParallelIterable<T> cumulate(BinaryOperator<T> op) default ...
ParallelIterable<T> sorted(Comparator<? super T> comparator) default ...
<U extends Comparable<? super U>> ParallelIterable<T> sortedBy(Mapper<? super T, U> extractor) default ...
ParallelIterable<T> uniqueElements() default ...
// Eager operations
boolean isEmpty() default ...;
long count() default ...
T getFirst() default ...
T getOnly() default ...
T getAny() default ...
void forEach(Block<? super T> block) default ...
T reduce(T base, BinaryOperator<T> reducer) default ...
<A extends ParallelFillable<? super T>> A into(A target) default ...
<A extends Fillable<? super T>> A into(A target) default ...
boolean anyMatch(Predicate<? super T> filter) default ...
boolean noneMatch(Predicate<? super T> filter) default ...
boolean allMatch(Predicate<? super T> filter) default ...
<U extends Comparable<? super U>> T maxBy(Mapper<? super T, U> extractor) default ...
<U extends Comparable<? super U>> T minBy(Mapper<? super T, U> extractor) default ...
}
您将注意到ParallelIterable上的操作集与Iterable上的操作非常相似,只是延迟操作返回的是ParallelIterable而不是Iterable。这意味着顺序集合上的操作管道也将以相同的方式(仅以并行方式)在并行集合上工作。
需要的最后一步是从(顺序的)集合中获得ParallelIterable的方法;这是新的parallel()方法在集合上返回的结果。
interface Collection<T> {
....
ParallelIterable<T> parallel();
}
我们在这里实现的是将递归分解的结构特性与可在可分解数据结构上并行执行的算法分离开来。数据结构的作者只需要实现Splittable方法,然后就可以立即访问filter、map和friends的并行实现。类似地,向ParallelIterable添加新方法可以立即在任何知道如何分割自身的数据结构上使用。
变异运算(Mutative operations)
对集合进行批量操作的许多用例会产生新的值、集合或副作用。然而,有时我们确实希望对集合进行就地修改。我们打算在采集上添加的主要原位突变有:
- 删除与谓词(Collection)匹配的所有元素
- 用新元素(List)替换与谓词匹配的所有元素
- 对列表进行排序(List)
这些将作为扩展方法添加到适当的接口上。
以上是关于初识Java8新特性Lambda 之collections的主要内容,如果未能解决你的问题,请参考以下文章