1. 程式人生 > >JDK8的隨筆(06)_Aggregate聚合操作之stream的拋磚引玉

JDK8的隨筆(06)_Aggregate聚合操作之stream的拋磚引玉

Aggregate 聚合操作

嗯。專案開始小忙碌,最近一直沒有更新。不能犯懶。。
JDK8中引進了Lambda表示式,Method Reference方法參照,以及default方法,static方法在interface中的使用。其實,這些也還都是鋪墊,雖然說Lambda表示式的概念在JDK8沒有出來的時候就開始炒作,但是我認為JDK8最引人注意的應該還是Aggregate 聚合操作的引入以及這個概念帶來的一些思維方式的改變。
先不管寫法是否醜陋效果是否有強加性,其實Aggregate的出現和實現方式都體現了當下一個簡單著名演算法的影子:MapReduce。

原創原文:blog.csdn.net/forevervip


下面開始依託javase的文件來粗粗瞭解一下聚合操作。

管道與資料流 pipelines and streams

在JDK8中,我們的collections後會“點”出來一個stream。

Person[] rosterAsArray = roster.toArray(new Person[roster.size()]);

        roster
        .stream()

這個stream即是這次的主角,資料流。
先看一個例子:
假設roster是一個List<Person>的list,那麼我們如果迴圈這個list,可以採用下面的寫法

for
(Person p : roster) { System.out.println(p.getName()); }

而,如果使用stream的資料流寫法則:

roster
    .stream()
    .forEach(e -> System.out.println(e.getName());

去掉回車的話,其實只有一行:

roster.stream().forEach(e -> System.out.println(e.getName());

stream後緊接著進行了一個forEach的操作,這個操作其實很多弱語言都有,比如javascript。而forEach中呼叫了一個Lambda表示式,表示式的內容很簡單,其實就是一個列印。
forEach方法中使用的引數是:

forEach(Consumer<? super Integer>)

Consumer和上一篇中寫到的Supplier類似,都是java.util.function中的函式式介面中的一員。
程式碼如下:

/*
 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.

 */
package java.util.function;

import java.util.Objects;

/**
 * Represents an operation that accepts a single input argument and returns no
 * result. Unlike most other functional interfaces, {@code Consumer} is expected
 * to operate via side-effects.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #accept(Object)}.
 *
 * @param <T> the type of the input to the operation
 *
 * @since 1.8
 */
@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);

    /**
     * Returns a composed {@code Consumer} that performs, in sequence, this
     * operation followed by the {@code after} operation. If performing either
     * operation throws an exception, it is relayed to the caller of the
     * composed operation.  If performing this operation throws an exception,
     * the {@code after} operation will not be performed.
     *
     * @param after the operation to perform after this operation
     * @return a composed {@code Consumer} that performs in sequence this
     * operation followed by the {@code after} operation
     * @throws NullPointerException if {@code after} is null
     */
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

可以看到,這裡的accept方法即使我們的Lambda表示式要去實現的方法,這個實現以及方法很簡單。不做多的說明。

繼續看一個例子:
我們需要一個條件,符合條件的資料再進行列印的處理:

for (Person p : roster) {
    if (p.getGender() == Person.Sex.MALE) {
        System.out.println(p.getName());
    }
}

OK,採用stream的寫法如下(一行太長,採用多行的寫法):

roster
    .stream()
    .filter(e -> e.getGender() == Person.Sex.MALE)
    .forEach(e -> System.out.println(e.getName()));

其中,forEach和上一個一樣,而多出來的是filter的這個中間方法的呼叫。
可以看到,filter中也是一個Lambda表示式,filter的呼叫原型:

filter(Predicate<? super T> predicate)

是的,Predicate也是java.util.function的函式式介面,程式碼內部有興趣可以去看看原始碼。

以上的方法呼叫即為管道聚合操作,這種操作有如下的特點

  1. 要有一個數據源。一般可以是collection,陣列(array),函式生成器(generator function 其實這個的使用可能還是會歸結到陣列和collection),或者I/O資料通道。例子中我們使用了一個collection。
  2. 0個或者多個的中間操作過程。例子中我們使用了一個filter,從而生成了一個新的stream資料流。
    資料流是一系列的元素,和collection不同的是,它並不作為儲存結構來儲存資料,而形象點的形容,它就是一個可以傳輸資料的通道,例子中的Predicate這個函式式介面的方法為boolean test(T t),所以Lambda表示式的e -> e.getGender() == Person.Sex.MALE 當性別是男性的時候可以返回一個true。filter根據返回值會重新生成一個性別都是男性的stream。
  3. 一個結束操作。這個操作可以做成一個非stream的結果,也可以是一個java的原始資料型別,也可以是一個collection,也或者是例子中的forEach並不返回任何資料。例子中採用的是Lambda表示式列印名字。

再看一個例子:

double average = roster
    .stream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .mapToInt(Person::getAge)
    .average()
    .getAsDouble();

這個例子中,到filter為止和上面那個例子是一致的。
而mapToInt開始則不同。mapToInt中呼叫的是ToIntFunction的函式式介面。這裡採用的是一個方法參照的寫法,其實我們利用e -> e.getAge 也可以代替。之前曾經說過方法參照的幾種型別,但是這裡稍有不同,後面在說collect方法的時候還會繼續說。average()是一個結束操作,類似於一般mapreduce中的reduce的過程,最終getAsDouble會把結果變成一個double型別。

與collection中的迭代操作的不同

首先,使用結束操作動作。
stream不會像collection的迭代那樣一個一個利用next去找尋資料,而是利用一個結束操作(方法,函式)。利用這個操作你可以嵌入自己的邏輯來告訴java你需要迭代什麼樣的資料,但是“迭代”(準確來說不是迭代的簡單操作)的過程是JDK幫你做的。以前使用的普通的collection的迭代卻是自己既編寫邏輯也編寫迭代的過程,然後這種迭代只能是線性順序的迭代演算法。新的stream的內部的“迭代”過程打破這個常規,它可以利用多執行緒來分散計算,把一個大問題切碎成很多小問題,最後合併計算結果,利用並行併發來解決原來的順序計算,我們後面會深入討論。
其次,執行過程中是從stream的管道中取得元素,而不是從collection中直接取得元素進行計算。所有計算過程都是一個stream的操作過程。
最後,引數化的方法呼叫。大多數計算過程中引數可以利用Lambda表示式以及Method Reference來代替,給編寫帶來了更大的方便及靈活性。

原文原創:blog.csdn.net/forevervip

つづく・・・