Java - Lambda 表達式與 Stream 接口

Java - Lambda 表達式與 Stream 接口

sschrodinger

2019/10/28


引用

java 8 Stream 原理深度解析 - Dorae

深入理解 Java Stream 流水線 - CarpenterLee


Lambda 表達式


lamda 表達式提供了豐富的語法糖,簡化自己的程序,并優化自己的程序速度。

Lambda 實例

Lambda 表達式僅支持函數式編程接口,所謂函數式接口,指的是只有一個抽象方法的接口。

函數式接口可以被隱式轉換為 Lambda 表達式。函數式接口可以用 @FunctionalInterface 注解標識。

Java 中有大量的函數式編程接口,如 RunnableCallable 等。

JDK 1.8 之后,又添加了一個 Java 包存放函數式接口,如下:

// java.util.function.*

public interface Consumer<T>{
    /**
     * 函數式接口,無返回值,接受一個參數,代表一個消費者
     **/
    void accept(T t); 
    // ...
}

public interface Supplier<T>{
    /**
     * 函數式接口,有返回值,無參數,代表一個生產者
     **/
    T get();
}

public interface Function<T, R>{
    /**
     * 函數式接口,有返回值,有參數
     **/
    R apply(T t);
    // ...
}

// ...

舉個例子,當我們需要創建一個線程時,標準的寫法如下(使用匿名內部類):

public class Demo {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
               System.out.println("hello,world");
            }
        }).start();
    }
}

lambda 表達式可以更加簡略的寫代碼,如上代碼的等效 lambda 表達式如下:

public class Demo {
    public static void main(String[] args) {
        new Thread
        (
            () -> { System.out.println("hello,world");}
        ).start();
    }
}

Lambda 語法

在 Java 中,使用 (paramters) -> expression; 或者 (paramters) -> {statements;} 來定義 lambda 表達式。其中,有部分寫法可以省略,規則如下:

note

  • 可選類型聲明:不需要聲明參數類型,編譯器可以統一識別參數值。
  • 可選的參數圓括號:一個參數無需定義圓括號,但多個參數需要定義圓括號。
  • 可選的大括號:如果主體包含了一個語句,就不需要使用大括號。
  • 可選的返回關鍵字:如果主體只有一個表達式返回值則編譯器會自動返回值,大括號需要指定明表達式返回了一個數值。

lambda 表達式在只有一條代碼時還可以引用其他方法或構造器并自動調用,可以省略參數傳遞,代碼更加簡潔,引用方法的語法需要使用 :: 符號。lambda 表達式提供了四種引用方法和構造器的方式:

  1. 引用對象的方法 類::實例方法
  2. 引用類方法 類::類方法
  3. 引用特定對象的方法 特定對象::實例方法
  4. 引用類的構造器 類::new

最常見的方法,比如說遍歷打印字符串或者排序,如下:

public class LambdaDemo {

    public static void main(String[] args) {
        List<String> list = new LinkedList<>();
        list.add("1");
        list.add("2");
        list.sort(String::compareTo);
        list.forEach(System.out::println);
    }

}

其中 forEach 的實現如下:

default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

復合 lambda 表達式

實現原理

lambda 表達式是通過 MethodHandlerinvokeDynamic 實現的。


Stream 接口


stream 接口主要是為了快速處理一些聚合操作,如下一個實實例:

返回以字符 A 開頭的字符串的最長長度。

最傳統的寫法如下:

public long maxLength(List<String> data) {
    List<String> list = new LinkedList<>();
    for (String s : data) {
        if (s.startsWith("A")) list.add(s);
    }
    long max = -1;
    for (String s: list) {
        if (max < s.length()) max = s.length();
    }
    return max;
}

通過兩次循環獲得最大的值,但是,這樣寫因為有兩次循環,所以會有效率上的損失,改進寫法如下:

public long maxLength(List<String> data) {
    long max = -1;
    for (String s: data) {
        if (s.startsWith("A")) {
            if (max < s.length()) max = s.length();
        }
    }
    return max;
}

Stream 接口等效于第二種方式,在一次迭代中盡可能多的執行用戶指定的操作,避免多次循環導致的效率問題。如下:

public long maxLength(List<String> data) {
    return data.stream().filter(s -> s.startsWith("A")).mapToLong(String::length).max().getAsLong();
}

Stream 接口總共分為兩種操作,一種為中間操作(Intermediate operations),一種為結束操作(Terminal operations)。

中間操作具體分為兩種,為不帶狀態的中間操作(Stateless)和帶狀態的中間操作(Stateful)。不帶狀態的中間操作指元素的處理不受到前面元素的影響,而帶狀態的中間操作必須要等到所有元素處理完之后才能知道結果。

結束操作也分為兩種,為短路操作非短路操作,短路操作是指不用處理全部元素就可以返回結果,比如找到第一個滿足條件的元素。非短路操作要找到所有滿足條件的元素才能完成。

如下:

Stream 操作分類
    |
    |---中間操作(Intermediate operations)   
    |           |
    |           |---無狀態(Stateless)
    |           |       |---unordered() // 返回一個無序的流
    |           |       |---filter() // 按照條件過濾
    |           |       |---map() // 處理流,產生新流
    |           |       |---mapToInt() // 獲得一個整數流
    |           |       |---mapToLong() // 獲得一個長整型流
    |           |       |---mapToDouble() // 獲得一個雙實數流
    |           |       |---flatMap() // 扁平化流
    |           |       |---flatMapToInt() // 扁平化流到整形
    |           |       |---flatMapToLong() // 扁平化流到長整形
    |           |       |---flatMapToDouble() // 扁平化流到雙實數類型
    |           |       |---peek() // 沒有返回值的 map
    |           |
    |           |---有狀態(Stateful)   
    |                   |---distinct() // 實現非重復流
    |                   |---sorted() // 排序
    |                   |---limit() // 取前 x 元素
    |                   |---skip() // 跳過前 x 元素
    |
    |
    |---結束操作(Terminal operations)
                |
                |---非短路操作
                |       |---forEach() // 遍歷(使用 parallelStream 不保證順序)
                |       |---forEachOrdered() // 遍歷(保證順序)
                |       |---toArray() // 轉換成數組
                |       |---reduce() // 合并流,比如說對流中所有元素求和
                |       |               data.stream().reduce((a, b) -> a + b);
                |       |---collect() // 收集流
                |       |---max() // 求最大值
                |       |---min() // 求最小值
                |       |---count() // 統計個數
                |
                |---短路操作(short-circuiting)  
                        |---anyMatch() // 任意一個匹配,返回 true
                        |---allMatch() // 所有匹配,返回 true
                        |---noneMatch() // 所有都不匹配,返回 true
                        |---findFirst() // 找到第一個出現的元素并返回
                        |---findAny() // 找到任意出現的元素并返回

Stream 接口實現原理

Stream 接口采用流水線的方式執行整個過程,要構建一個流水線,需要解決三個問題:

  • 流水線構建
  • 流水線方法保存
  • 流水線操作執行
  • 流水線結果保存

流水線構建 使用 pipeline,Java Stream 中,流水線的中間節點使用 PipelineHelper 來代表一個 stage,每個 stage 就是一個用戶指定的操作,如 map、filter 等。多個不同的 stage 連接成一個雙向鏈表,這個雙向鏈表就封裝了用戶的全部操作。

如下實例:

data.stream().filter(s -> s.startsWith("A")).mapToLong(String::length).max();

構成的流水線如下:

                     |---------| ----> |---------| -----> |---------| ------> |---------|
                     |   Head  |       |filter op|        | map  op |         | max  op |
                     |---------| <---- |---------| <----- |---------| <------ |---------|
                     
data source ---------> stage 0 ---------> stage 1 ---------> stage 2 ---------> stage 3
             stream()           filter()          mapToLong()           max()

流水線方法保存 采用的是 sink 接口,提供了最重要的四個函數,如下:

interface Sink<T> extends Consumer<T> {

    // 開始遍歷元素之前調用該方法,通知Sink做好準備。
    default void begin(long size) {}

    // 所有元素遍歷完成之后調用,通知Sink沒有更多的元素了。
    default void end() {}

    // 是否可以結束操作,可以讓短路操作盡早結束。
    default boolean cancellationRequested() {
        return false;
    }
    
    // 遍歷元素時調用,接受一個待處理元素,并對元素進行處理。Stage把自己包含的操作和回調方法封裝到該方法里,前一個Stage只需要調用當前Stage.accept(T t)方法就行了。
    default void accept(int value) {
        throw new IllegalStateException("called wrong accept method");
    }

    default void accept(long value) {
        throw new IllegalStateException("called wrong accept method");
    }

    default void accept(double value) {
        throw new IllegalStateException("called wrong accept method");
    }
    
    // ... 
}

一般來說,重寫這些函數就可以實現聚合邏輯。例如 filter 實現如下:

@Override
public final Stream<P_OUT> filter(Predicate<? super P_OUT> predicate) {
    Objects.requireNonNull(predicate);
    return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE,
                                 StreamOpFlag.NOT_SIZED) {
        @Override
        Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> sink) {
            return new Sink.ChainedReference<P_OUT, P_OUT>(sink) {
                // 自己開始的同時,激活下一鏈
                @Override
                public void begin(long size) {
                    downstream.begin(-1);
                }

                // 預測是否滿足要求,滿足要求則將其丟給下一鏈處理
                @Override
                public void accept(P_OUT u) {
                    if (predicate.test(u))
                        downstream.accept(u);
                }
            };
        }
    };
}

對于稍微復雜的有狀態操作,如排序,如下:

private static final class RefSortingSink<T> extends AbstractRefSortingSink<T> {
    private ArrayList<T> list;

    // 創建一個臨時的鏈表,保存結果
    @Override
    public void begin(long size) {
        if (size >= Nodes.MAX_ARRAY_SIZE)
            throw new IllegalArgumentException(Nodes.BAD_SIZE);
        list = (size >= 0) ? new ArrayList<T>((int) size) : new ArrayList<T>();
    }

    // 對臨時表進行排序
    // 激活下一鏈處理
    // 發送數據到下一鏈
    @Override
    public void end() {
        list.sort(comparator);
        downstream.begin(list.size());
        if (!cancellationWasRequested) {
            list.forEach(downstream::accept);
        }
        else {
            for (T t : list) {
                if (downstream.cancellationRequested()) break;
                downstream.accept(t);
            }
        }
        downstream.end();
        list = null;
    }

    // 上一連將其所有的元素加入到臨時表中
    @Override
    public void accept(T t) {
        list.add(t);
    }
        
    // ...
}

note

  • 從流水線的執行上來看,對于無狀態的中間操作,是可以通過流水線進行并行的,但是對于有狀態的操作,則必須等到所有的數據處理完之后才能夠進行下一步,所以,每當有有狀態的中間操作,就會多一次循環。

filter 函數為例,函數主題返回一個實現了 Stream 接口的 StatelessOp 對象,如下:

public final Stream<P_OUT> filter(Predicate<? super P_OUT> predicate) {
    Objects.requireNonNull(predicate);
    return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE, StreamOpFlag.NOT_SIZED) // ... 匿名類實現
}

StatelessOp 是屬于 ReferencePipeline 的一個靜態內部類。構造函數如下:

// 將上一級 stage 引用加入到該 stage 中
StatelessOp(AbstractPipeline<?, E_IN, ?> upstream,
            StreamShape inputShape,
            int opFlags) {
    super(upstream, opFlags);
    assert upstream.getOutputShape() == inputShape;
}

那么實際上對于 filter 函數,是將上一級的 stage 引用(他自身)加入到了一個新的 stage 中。并返回了那個新的 stage,如下:

                                                     |--------------------------|
                 |---------|                         |  previousStage = stage n |
                 | stage n |                         |        stage n + 1       |
                 |---------|                         |--------------------------|
stream().func() ------------> stream.func().filter() ---------------------------->

流水線執行 使用結束操作喚醒。結束操作也是一個特殊的 stage。根據流水線的雙向隊列,不考慮結束操作時的執行,如下:

data.stream().filter(s -> s.startsWith("A")).mapToLong(String::length).max();
//   |------|>|----------------------------|>|-----------------------|>|------|
//   | Head | |        filter op           | |       map op          | |max op|
//   |------|<|----------------------------|<|-----------------------|<|------|

當執行 max 函數時,會執行如下的語句:

@Override
public final Optional<P_OUT> max(Comparator<? super P_OUT> comparator) {
    return reduce(BinaryOperator.maxBy(comparator));
}

maxBy 函數很簡單,僅僅是返回一個比較器,如下:

public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
    Objects.requireNonNull(comparator);
    return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
}

reduce 函數才是一個短路操作,如下:

@Override
public final Optional<P_OUT> reduce(BinaryOperator<P_OUT> accumulator) {
    return evaluate(ReduceOps.makeRef(accumulator));
}

makeRef 函數本質上來說也是生成一個 sink,如下:

public static <T> TerminalOp<T, Optional<T>> makeRef(BinaryOperator<T> operator) {
        class ReducingSink implements AccumulatingSink<T, Optional<T>, ReducingSink> {
            private boolean empty;
            private T state;

            public void begin(long size) {
                empty = true;
                state = null;
            }

            @Override
            public void accept(T t) {
                if (empty) {
                    empty = false;
                    state = t;
                } else {
                    state = operator.apply(state, t);
                }
            }

            @Override
            public Optional<T> get() {
                return empty ? Optional.empty() : Optional.of(state);
            }

            @Override
            public void combine(ReducingSink other) {
                if (!other.empty)
                    accept(other.state);
            }
        }
        return new ReduceOp<T, Optional<T>, ReducingSink>(StreamShape.REFERENCE) {
            @Override
            public ReducingSink makeSink() {
                return new ReducingSink();
            }
        };
    }

所有的計算工作都是由 evaluate 函數完成的,如下:

final <R> R evaluate(TerminalOp<E_OUT, R> terminalOp) {
    assert getOutputShape() == terminalOp.inputShape();
    if (linkedOrConsumed)
        throw new IllegalStateException(MSG_STREAM_LINKED);
    linkedOrConsumed = true;

    return isParallel()
           ? terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags()))
           : terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags()));
}

實際上, stream 流計算有兩種模式,一種是串行的方式,一種是并行的方式,首先看串行的方式 evaluateSequential,如下:

@Override
public <P_IN> R evaluateSequential(PipelineHelper<T> helper,
                                   Spliterator<P_IN> spliterator) {
    return helper.wrapAndCopyInto(makeSink(), spliterator).get();
}

helper 代表的實際上是除開 max 后的最后一個 stage,即 mapToLong,可以根據如下函數棧追蹤到運行的函數,如下:

copyInto(wrapSink(Objects.requireNonNull(sink)), spliterator);
       /|\
        |
helper.wrapAndCopyInto(makeSink(), spliterator);

copyInto 為運行的核心函數,如下:

final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
    Objects.requireNonNull(wrappedSink);

    if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
        wrappedSink.begin(spliterator.getExactSizeIfKnown());
        spliterator.forEachRemaining(wrappedSink);
        wrappedSink.end();
    }
    else {
        copyIntoWithCancel(wrappedSink, spliterator);
    }
}

就是該函數啟動了整個流水線的執行。

Stream 接口并行化

對于無狀態的 stage,是可以開啟并行來處理的,主要是用到了 Java 提供的 Fork/Join 框架。

來看 evaluateParallel 函數,如下:

@Override
public <P_IN> R evaluateParallel(PipelineHelper<T> helper,
                                 Spliterator<P_IN> spliterator) {
    return new ReduceTask<>(this, helper, spliterator).invoke().get();
}

ReduceTaskForkJoinTask 的一個子類,invoke() 函數就是開啟計算流程,get() 函數等待計算完成并返回結果。

ForkJoinTask 最重要的就是重寫 compute 函數

@Override
public void compute() {
    Spliterator<P_IN> rs = spliterator, ls; // right, left spliterators
    long sizeEstimate = rs.estimateSize();
    long sizeThreshold = getTargetSize(sizeEstimate);
    boolean forkRight = false;
    @SuppressWarnings("unchecked") K task = (K) this;
    while (sizeEstimate > sizeThreshold && (ls = rs.trySplit()) != null) {
        K leftChild, rightChild, taskToFork;
        task.leftChild  = leftChild = task.makeChild(ls);
        task.rightChild = rightChild = task.makeChild(rs);
        task.setPendingCount(1);
        if (forkRight) {
            forkRight = false;
            rs = ls;
            task = leftChild;
            taskToFork = rightChild;
        }
        else {
            forkRight = true;
            task = rightChild;
            taskToFork = leftChild;
        }
        taskToFork.fork();
        sizeEstimate = rs.estimateSize();
    }
    task.setLocalResult(task.doLeaf());
    task.tryComplete();
}

使用 Spliterator 迭代器產生數據,并且交叉左右節點執行 fork 函數(即分叉計算),在 tryComplete 中執行結果的合并操作,流程圖如下:

并行操作

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,908評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,324評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,018評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,675評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,417評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,783評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,960評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,522評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,267評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,471評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,698評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,099評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,386評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,204評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,436評論 2 378

推薦閱讀更多精彩內容