簡體   English   中英

使用無序終端操作的Stream.skip行為

[英]Stream.skip behavior with unordered terminal operation

我已經閱讀了這個這個問題,但仍然懷疑是否觀察到Stream.skip行為是由JDK作者所預期的。

讓我們簡單輸入數字1..20:

List<Integer> input = IntStream.rangeClosed(1, 20).boxed().collect(Collectors.toList());

現在讓我們創建一個並行流,以不同的方式組合unordered()skip()並收集結果:

System.out.println("skip-skip-unordered-toList: "
        + input.parallelStream().filter(x -> x > 0)
            .skip(1)
            .skip(1)
            .unordered()
            .collect(Collectors.toList()));
System.out.println("skip-unordered-skip-toList: "
        + input.parallelStream().filter(x -> x > 0)
            .skip(1)
            .unordered()
            .skip(1)
            .collect(Collectors.toList()));
System.out.println("unordered-skip-skip-toList: "
        + input.parallelStream().filter(x -> x > 0)
            .unordered()
            .skip(1)
            .skip(1)
            .collect(Collectors.toList()));

過濾步驟在這里基本沒什么,但為流引擎增加了更多的難度:現在它不知道輸出的確切大小,因此關閉了一些優化。 我有以下結果:

skip-skip-unordered-toList: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
// absent values: 1, 2
skip-unordered-skip-toList: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20]
// absent values: 1, 15
unordered-skip-skip-toList: [1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 19, 20]
// absent values: 7, 18

結果完全沒問題,一切都按預期工作。 在第一種情況下,我要求跳過前兩個元素,然后收集列表,沒有特別的順序。 在第二種情況下,我要求跳過第一個元素,然后轉為無序並跳過一個元素(我不關心哪一個)。 在第三種情況下,我首先轉為無序模式,然后跳過兩個任意元素。

讓我們跳過一個元素並以無序模式收集到自定義集合。 我們的自定義集合將是一個HashSet

System.out.println("skip-toCollection: "
        + input.parallelStream().filter(x -> x > 0)
        .skip(1)
        .unordered()
        .collect(Collectors.toCollection(HashSet::new)));

輸出令人滿意:

skip-toCollection: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
// 1 is skipped

所以一般來說,我希望只要stream是有序的, skip()跳過第一個元素,否則它會跳過任意​​元素。

但是,讓我們使用等效的無序終端操作collect(Collectors.toSet())

System.out.println("skip-toSet: "
        + input.parallelStream().filter(x -> x > 0)
            .skip(1)
            .unordered()
            .collect(Collectors.toSet()));

現在的輸出是:

skip-toSet: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 16, 17, 18, 19, 20]
// 13 is skipped

使用任何其他無序終端操作(如forEachfindAnyanyMatch等)可以實現相同的結果。 在這種情況下刪除unordered()步驟不會改變任何內容。 似乎unordered()步驟正確地使流從當前操作開始無序,無序終端操作使整個流從一開始就開始無序,盡管如果使用skip()這會影響結果。 這對我來說似乎完全是誤導:我希望使用無序收集器與在終端操作之前將流轉換為無序模式並使用等效的有序收集器相同。

所以我的問題是:

  1. 這種行為是打算還是錯誤?
  2. 如果是,它在某處記錄了嗎? 我讀過Stream.skip()文檔:它沒有說明無序的終端操作。 另外, Characteristics.UNORDERED文檔不是很理解,也沒有說整個流的排序會丟失。 最后,包概要中的訂購部分也未涵蓋此案例。 可能我錯過了什么?
  3. 如果無意的終端操作意圖使整個流無序,那么為什么unordered()步驟使它僅在此點之后無序? 我可以依靠這種行為嗎? 或者我很幸運,我的第一次測試工作得很好?

回想一下,流標志(ORDERED,SORTED,SIZED,DISTINCT)的目標是使操作能夠避免做不必要的工作。 涉及流標志的優化示例如下:

  • 如果我們知道流已經排序,那么sorted()是一個no-op;
  • 如果我們知道流的大小,我們可以在toArray()預先分配一個正確大小的數組,避免復制;
  • 如果我們知道輸入沒有有意義的遭遇順序,我們就不需要采取額外的步驟來保留遭遇順序。

管道的每個階段都有一組流標志。 中間操作可以注入,保留或清除流標志。 例如,過濾保留了sorted-ness / distinct-ness但不保留大小; 映射保留大小但不是排序或不同的。 排序注入排序。 中間操作的標志處理相當簡單,因為所有決策都是本地的。

終端操作的標志處理更加微妙。 ORDERED是終端操作最相關的標志。 如果終端操作是UNORDERED,那么我們會反向傳播無序的。

我們為什么要做這個? 好吧,考慮這個管道:

set.stream()
   .sorted()
   .forEach(System.out::println);

由於forEach不受限於按順序操作,因此對列表進行排序的工作完全是浪費精力。 所以我們反向傳播這些信息(直到我們遇到短路操作,例如limit ),以免失去這個優化機會。 同樣,我們可以在無序流上使用distinct的優化實現。

這種行為是打算還是錯誤?

是:)反向傳播是預期的,因為它是一種有用的優化,不應產生不正確的結果。 然而,bug部分是我們正在傳播過去的skip ,我們不應該。 所以UNORDERED標志的反向傳播是過於激進的,這是一個錯誤。 我們會發布一個錯誤。

如果是,它在某處記錄了嗎?

它應該只是一個實現細節; 如果它被正確實現,你不會注意到(除了你的流更快。)

@Ruben,你可能不明白我的問題。 大致問題是:為什么unordered()。collect(toCollection(HashSet :: new))的行為與collect(toSet())不同。 當然我知道toSet()是無序的。

可能,但是,無論如何,我會再試一次。

看一下收集器的Javadocs toSet和toCollection,我們可以看到toSet提供了一個無序的收集器

這是一個{@link Collector.Characteristics#UNORDERED unordered}收集器。

即具有UNORDERED特征的CollectorImpl 看一下Collector.Characteristics#UNORDERED的Javadoc,我們可以讀到:

指示集合操作不承諾保留輸入元素的遭遇順序

在收藏家的Javadocs中我們也可以看到:

對於並發收集器,實現可以(但不是必須)同時實現還原。 並發減少是使用相同的可同時修改的結果容器從多個線程同時調用累加器函數,而不是在累積期間保持結果隔離的情況。 僅當收集器具有{@link Characteristics#UNORDERED}特征或原始數據無序時,才應應用並發減少

這意味着,如果我們設置UNORDERED特性,我們根本不關心流的元素傳遞給累加器的順序,因此,可以按任何順序從管道中提取元素。

順便說一句,如果省略示例中的無序(),則會得到相同的行為:

    System.out.println("skip-toSet: "
            + input.parallelStream().filter(x -> x > 0)
                .skip(1)
                .collect(Collectors.toSet()));

此外,Stream中的skip()方法給出了一個提示:

雖然{@code skip()}通常是順序流管道上的廉價操作,但在有序並行管道上它可能非常昂貴

使用無序流源(例如{@link #generate(Supplier)})或使用{@link #unordered()}刪除排序約束可能會導致顯着的加速

使用時

Collectors.toCollection(HashSet::new)

你正在創建一個普通的“有序”收集器(一個沒有UNORDERED特征的收集器),對我來說意味着你關心排序,因此,元素按順序被提取出來並且你得到了預期的行為。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM