初识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的将来版本。)html

并行性是这项工做的重要推进力。所以,要鼓励那些成语是很是重要的两个 sequential-和并行友好。咱们主要经过较少地关注就地突变而更多地关注产生新值的计算来实现这一目标。在使并行变得容易但不能使其变得不可见之间取得平衡也是重要的; 咱们的目标是 为新旧系列提供明确但不干扰的并行性。java

内部与外部迭代(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者决定的,可是对于内部迭代,至少是可能的,而对于外部迭代,则不是。)闭包

内部迭代使其具备一种编程样式,其中能够将操做“管道化”在一块儿。例如,若是咱们只想将蓝色块涂成红色,咱们能够说:app

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 >)进行建模。例如,考虑构造的直方图的问题。文档中单词的长度。若是咱们将文档建模为单词流,则能够对流进行“分组”操做(按长度分组),而后对与给定键关联的值进行“reduce”(sum)操做以得到从单词长度映射到具备该长度的单词数:

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)

这些将做为扩展方法添加到适当的接口上。

官方原文
State of the Lambda: Libraries Edition

相关文章
相关标签/搜索