stream and lambda(12) - 終止操作之查詢與匹配(findAny、findFirst、allMatch、anyMatch、noneMatch)

語言: CN / TW / HK

Stream 提供的一系列方法,在經過中間操作之後,最後還是為了得到確定的元素。因此,Stream 還提供了大量的終止操作,以便我們能得到想到的資料。

三個 Match 操作

方法定義

boolean allMatch(Predicate<? super T> predicate) boolean anyMatch(Predicate<? super T> predicate) boolean noneMatch(Predicate<? super T> predicate)

*Match 方法有三個: allMatch 表示所有元素都要通過 Predicate 的考驗, anyMatch 表示只要有任何一個元素通過考驗就行, noneMatch 表示沒有任何一個掉進 Predicate 的陷阱才可以。

看起來是不是有些熟悉?沒錯,是有點像 &&||! 的條件表示式。

使用舉例

public void matchTest(){
    List<Integer> integers = Arrays.asList(1, 23, 4, 5, 6, 7, 0);
    //所有元素都大於5
    System.out.println(integers.stream().allMatch(i -> i > 5));
    //存在某個元素大於5
    System.out.println(integers.stream().anyMatch(i -> i > 5));
    //沒有任何元素大於5
    System.out.println(integers.stream().noneMatch(i -> i > 5));
}

不出意料,三次分別輸出的是 false, true, false

Stream 內沒有任何元素呢?

出乎意料的結果

api 的使用太過簡單,膨脹的我忽然很好奇一個問題: 如果 Stream 裡沒有任何元素呢?會不會報錯?執行結果又如果呢?

public void emptyStreamMatchTest(){
    List<Integer> integers = Collections.emptyList();
    System.out.println(integers.stream().allMatch(i -> i > 5));
    System.out.println(integers.stream().anyMatch(i -> i > 5));
    System.out.println(integers.stream().noneMatch(i -> i > 5));
}

執行之前,根據經驗先預測一下:執行無異常,結果分別是 false, false, true 。理由很簡單,沒有元素,所以不會滿足所有元素都大於5,也就是沒有元素大於5。

執行一下,結果如下:

true, false, true

哎呀媽呀,我和我的小夥伴都驚呆了,沒有任何元素,怎麼會是 allMatch 呢!這他媽即興一試還有意外收穫!!此刻的我好開心呀:我發現 JDK 原始碼的 bug 了!!!

一探原始碼查究竟

// allMatch 方法實現
public final boolean allMatch(Predicate<? super P_OUT> predicate) {
    return evaluate(MatchOps.makeRef(predicate, MatchOps.MatchKind.ALL));
}

// 三個 match 方法實際都用到了這裡
public static <T> TerminalOp<T, Boolean> makeRef(Predicate<? super T> predicate,MatchKind matchKind) {
    Objects.requireNonNull(predicate);
    Objects.requireNonNull(matchKind);
    class MatchSink extends BooleanTerminalSink<T> {
        MatchSink() {
            super(matchKind);
        }

        @Override
        public void accept(T t) {
            if (!stop && predicate.test(t) == matchKind.stopOnPredicateMatches) {
                stop = true;
                value = matchKind.shortCircuitResult;
            }
        }
    }

    return new MatchOp<>(StreamShape.REFERENCE, matchKind, MatchSink::new);
}

經過上面的程式碼,發現瞭如下重點部分:

public void accept(T t) {
    if (!stop && predicate.test(t) == matchKind.stopOnPredicateMatches) {
        stop = true;
        value = matchKind.shortCircuitResult;
    }
}

已經問過湯師爺,他翻譯的結果是:如果還有元素的話,判斷一下,如果元素的判斷值等於預設值,就不再判斷下去,此時中斷遍歷,叫做短路。

說實話,我說我不理解,湯師爺說,不,你理解,不信你自己用自己的方法來實現這 allMatch ,和原始碼決定一樣的處理。我不信,我就這樣寫了:

public boolean allMatch(List<Integer> integers, Predicate<Integer> p) {
    Objects.requireNonNull(integers);
    boolean b = true;
    for (Integer integer : integers) {
        if (!p.test(integer)) {
            b = false;
            break;
        }
    }
    return b;
}

湯師爺說,你看吧,你先給了預設值 true ,如果有不符合條件的,直接不迴圈了,返回 false 。其他兩個你應該也會這樣實現的。

allMatch:先預設 true,如果有不滿足條件的,短路,返回 false。

anyMatch:先預設 false,如果有滿足條件的,短路,返回 true。

noneMatch:先預設 true,如果有滿足條件的,短路,返回 false。

JDK 實際就是這麼處理的。

public void accept(T t) {
    if (!stop && predicate.test(t) == matchKind.stopOnPredicateMatches) {
        stop = true;
        value = matchKind.shortCircuitResult;
    }
}

enum MatchKind {
    //當 predicate.test == true 時返回並中斷 true
    //有滿足就為 true
    ANY(true, true),
    //當 predicate.test == false 時返回並中斷 false
    //有不滿足就 false
    ALL(false, false),
    //當 predicate.test == true 時返回並中斷 false
    //有滿足的就 false
    NONE(true, false);

    // 終止條件,也就是 predicate.test 的值是這個就終止
    private final boolean stopOnPredicateMatches;
    // 短路後的結果
    private final boolean shortCircuitResult;

    private MatchKind(boolean stopOnPredicateMatches, boolean shortCircuitResult) {
        this.stopOnPredicateMatches = stopOnPredicateMatches;
        this.shortCircuitResult = shortCircuitResult;
    }
}

程式碼裡其實已經解釋的差不多了,那就只剩一個問題了:在 JDK,初始預設值是什麼?

方法 預設值 短路值
anyMatch false true
allMatch true false
noneMatch true false

我猜是 !shortCircuitResult ,你呢?

原始碼再次驗證猜想:

private static abstract class BooleanTerminalSink<T> implements Sink<T> {
    boolean stop;
    boolean value;
    // value 果然是 !shortCircuitResult
    BooleanTerminalSink(MatchKind matchKind) {
        value = !matchKind.shortCircuitResult;
    }
    //計算結果時,是用這個方法,也就是說,value是match返回的結果
    public boolean getAndClearState() {
        return value;
    }
    ...這裡省略
}

兩個 find 操作

方法定義

Optional<T> findAny() Optional<T> findFirst()

findAny ,返回流內任意一個元素; findFirst ,返回流內第一個元素。

這裡需要指出的是,返回結果是 Optional ,而不是直接的物件。

使用舉例

findAny

public void findAnyTest() {
    List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
    // 只會輸出 1
    integers.stream().findAny().ifPresent(System.out::println);
    //輸出結果不一定是 1,任何一個都有可能
    integers.parallelStream().findAny().ifPresent(System.out::println);
}

findAny 返回的是流內的任意一個元素。但是如果是序列流,第一個元素永遠最先取到,所以相當於 FindFirst 。如果是並行流,哪個元素先取到就不好說了,任何一個元素都有可能被先取到。

findFirst

public void findFirstTest() {
    List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
    // 輸出只會是1
    integers.stream().findFirst().ifPresent(System.out::println);
    // 輸出也只會是1
    integers.parallelStream().findFirst().ifPresent(System.out::println);
}

findFirst ,顧名思義,找第一個元素,它不應該因為是並行流或者序列流而有不同。在一個有序流裡,第一個元素是哪個是很明確的。

總結

  • 本文講了 allMatchanyMatchnoneMatchfindFirstfindAny 五種終止操作,對應的含義從方法名上能直接判斷出來。
  • 當流內無元素時, allMatch 會返回 true。
  • findAny 在並行流時才會任意返回,在序列流時只會返回第一個。

注:本文配套程式碼可在 github 檢視: stream-and-lambda