簡體   English   中英

Java 8 Stream:limit()和skip()之間的區別

[英]Java 8 Stream: difference between limit() and skip()

談論Stream ,當我執行這段代碼時

public class Main {
    public static void main(String[] args) {
        Stream.of(1,2,3,4,5,6,7,8,9)
        .peek(x->System.out.print("\nA"+x))
        .limit(3)
        .peek(x->System.out.print("B"+x))
        .forEach(x->System.out.print("C"+x));
    }
}

我得到了這個輸出

A1B1C1
A2B2C2
A3B3C3

因為將我的流限制為前三個組件會強制執行動作ABC三次。

嘗試使用skip()方法對最后三個元素執行類似計算,顯示不同的行為:this

public class Main {
    public static void main(String[] args) {
        Stream.of(1,2,3,4,5,6,7,8,9)
        .peek(x->System.out.print("\nA"+x))
        .skip(6)
        .peek(x->System.out.print("B"+x))
        .forEach(x->System.out.print("C"+x));
    }
}

輸出這個

A1
A2
A3
A4
A5
A6
A7B7C7
A8B8C8
A9B9C9

在這種情況下,為什么執行A1A6的操作? 它必須與limit是一個短路狀態中間操作這一事實有關,而skip不是,但我不明白這個屬性的實際含義。 是不是“在跳過之前的所有動作都被執行而不是每個人都在限制之前”?

你在這里有兩個流管道。

這些流管道每個都包含一個源,幾個中間操作和一個終端操作。

但是中間操作是懶惰的。 這意味着除非下游操作需要項目,否則不會發生任何事情。 如果是這樣,那么中間操作會完成生成所需項目所需的全部操作,然后再次等待直到請求另一個項目,依此類推。

終端操作通常是“急切的”。 也就是說,他們要求流中的所有項目完成所需的項目。

因此,您應該將管道視為forEach ,將其后面的流請求下一個項目,並且該流請求它后面的流,依此類推,一直到源。

考慮到這一點,讓我們看看我們對您的第一個管道有什么:

Stream.of(1,2,3,4,5,6,7,8,9)
        .peek(x->System.out.print("\nA"+x))
        .limit(3)
        .peek(x->System.out.print("B"+x))
        .forEach(x->System.out.print("C"+x));

所以, forEach要求第一項。 這意味着“B” peek需要一個項目,並詢問它的limit輸出流,這意味着limit將需要詢問“A” peek ,這將進入源。 給出一個項目,並一直到forEach ,你得到你的第一行:

A1B1C1

forEach要求另一個項目,然后另一個項目。 每次,請求都在流上傳播並執行。 但是當forEach要求第四個項目時,當請求達到limit ,它知道它已經給出了允許給出的所有項目。

因此,它不是要求“A”偷看另一個項目。 它立即指示其項目已耗盡,因此不再執行任何操作且forEach終止。

第二個管道會發生什么?

    Stream.of(1,2,3,4,5,6,7,8,9)
    .peek(x->System.out.print("\nA"+x))
    .skip(6)
    .peek(x->System.out.print("B"+x))
    .forEach(x->System.out.print("C"+x));

再次, forEach要求第一項。 這是傳播回來的。 但是當它到達skip ,它知道它必須從其上游請求6個項目才能通過下游。 因此,它從“A” peek上游請求,在不向下游傳遞的情況下使用它,發出另一個請求,等等。 因此,“A”偷看獲得6個項目請求並產生6個打印,但這些項目不會傳遞下來。

A1
A2
A3
A4
A5
A6

skip的第7個請求中,項目向下傳遞到“B”,並從它傳遞到forEach ,因此完整打印完成:

A7B7C7

那就像以前一樣。 skip現在,無論何時獲得請求,都要求上游項目並將其傳遞到下游,因為它“知道”它已經完成了跳過工作。 因此,其余的打印件將通過整個管道,直到源耗盡。

流水管道的流暢表示法正是造成這種混亂的原因。 以這種方式思考:

limit(3)

除了forEach()之外,所有流水線操作都是懶惰地進行評估,這是一個終端操作 ,它觸發“執行管道”

在執行管道時,中間流定義不會對“之前”“之后”發生的事情做出任何假設。 他們所做的就是獲取輸入流並將其轉換為輸出流:

Stream<Integer> s1 = Stream.of(1,2,3,4,5,6,7,8,9);
Stream<Integer> s2 = s1.peek(x->System.out.print("\nA"+x));
Stream<Integer> s3 = s2.limit(3);
Stream<Integer> s4 = s3.peek(x->System.out.print("B"+x));

s4.forEach(x->System.out.print("C"+x));
  • s1包含9個不同的Integer值。
  • s2查看傳遞它的所有值並打印它們。
  • s3將前3個值傳遞給s4並在第三個值之后中止管道。 s3沒有產生進一步的值。 這並不意味着管道中沒有更多的值。 s2仍會生成(並打印)更多值,但沒有人請求這些值,因此執行停止。
  • s4再次查看傳遞它的所有值並打印它們。
  • forEach消耗並打印s4傳遞給它的任何東西。

這樣想吧。 整個流是完全懶惰的。 只有終端操作主動從管道中提取新值。 s4 <- s3 <- s2 <- s1拉出3個值后, s3將不再生成新值,並且不再從s2 <- s1提取任何值。 雖然s1 -> s2仍然能夠產生4-9 ,但這些值永遠不會從管道中拉出,因此永遠不會被s2打印。

skip(6)

使用skip()會發生同樣的事情:

Stream<Integer> s1 = Stream.of(1,2,3,4,5,6,7,8,9);
Stream<Integer> s2 = s1.peek(x->System.out.print("\nA"+x));
Stream<Integer> s3 = s2.skip(6);
Stream<Integer> s4 = s3.peek(x->System.out.print("B"+x));

s4.forEach(x->System.out.print("C"+x));
  • s1包含9個不同的Integer值。
  • s2查看傳遞它的所有值並打印它們。
  • s3消耗前6個值, “跳過它們” ,這意味着前6個值不傳遞給s4 ,只有后續值。
  • s4再次查看傳遞它的所有值並打印它們。
  • forEach消耗並打印s4傳遞給它的任何東西。

這里重要的是s2不知道剩余的管道跳過任何值。 s2獨立於之后發生的事情,查看所有值。

另一個例子:

請考慮此博客文章中列出的此管道

IntStream.iterate(0, i -> ( i + 1 ) % 2)
         .distinct()
         .limit(10)
         .forEach(System.out::println);

執行上述操作時,程序將永遠不會停止。 為什么? 因為:

IntStream i1 = IntStream.iterate(0, i -> ( i + 1 ) % 2);
IntStream i2 = i1.distinct();
IntStream i3 = i2.limit(10);

i3.forEach(System.out::println);

意思是:

  • i1產生交替值的無限量: 010101 ,...
  • i2消耗之前遇到的所有值,僅傳遞“新”值,即i2總共有2個值。
  • i3傳遞10個值,然后停止。

這個算法永遠不會停止,因為i301之后等待i2再產生8個值,但是這些值永遠不會出現,而i1永遠不會停止向i2值。

在管道中的某個點上,產生了超過10個值並不重要。 重要的是i3從未見過這10個值。

回答你的問題:

是不是“在跳過之前的所有動作都被執行而不是每個人都在限制之前”?

不。 執行skip()limit()之前的所有操作。 在兩次執行中,你都獲得了A1 - A3 但是limit()可能會使管道短路,一旦發生了感興趣的事件(達到限制),就會中止價值消耗。

單獨查看蒸汽操作完全是褻瀆神靈,因為這不是評估流的方式。

談到限制(3) ,這是一個短路操作,這是有道理的,因為考慮它,無論操作在limit 之前之后 ,在流中有限制都會在獲得n個元素之后停止迭代直到限制操作,但是這並不意味着只處理n個流元素。 以此不同的流操作為例

public class App 
{
    public static void main(String[] args) {
        Stream.of(1,2,3,4,5,6,7,8,9)
        .peek(x->System.out.print("\nA"+x))
        .filter(x -> x%2==0)
        .limit(3)
        .peek(x->System.out.print("B"+x))
        .forEach(x->System.out.print("C"+x));
    }
}

會輸出

A1
A2B2C2
A3
A4B4C4
A5
A6B6C6

這似乎是正確的,因為限制正在等待3個流元素通過操作鏈,盡管處理了6個流元素。

所有流都基於分裂器,它基本上有兩個操作:前進(向前移動一個元素,類似於迭代器)和分割(將自己划分到任意位置,這適合於並行處理)。 你可以隨時停止輸入元素(由limit完成),但你不能只跳到任意位置(在Spliterator界面中沒有這樣的操作)。 因此, skip操作需要實際讀取源中的第一個元素以忽略它們。 請注意,在某些情況下,您可以執行實際跳轉:

List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9);

list.stream().skip(3)... // will read 1,2,3, but ignore them
list.subList(3, list.size()).stream()... // will actually jump over the first three elements

也許這個小圖有助於對流的處理方式產生一些自然的“感覺”。

第一行=>8=>=7= ... ===描繪了流。 元素1..8從左向右流動。 有三個“窗口”:

  1. 在第一個窗口( peek A )中,您可以看到一切
  2. 在第二個窗口( skip 6limit 3 )中進行一種過濾。 第一個或最后一個元素被“消除” - 意味着沒有傳遞進行進一步處理。
  3. 在第三個窗口中,您只能看到傳遞的項目

┌────────────────────────────────────────────────────────────────────────────┐ │ │ │▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸ ▸▸▸▸▸▸▸▸▸▸▸ ▸▸▸▸▸▸▸▸▸▸ ▸▸▸▸▸▸▸▸▸ │ │ 8 7 6 5 4 3 2 1 │ │▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸▸ ▲ ▸▸▸▸▸▸▸▸▸▸▸ ▲ ▸▸▸▸▸▸▸▸▸▸ ▲ ▸▸▸▸▸▸▸▸▸ │ │ │ │ │ │ │ │ skip 6 │ │ │ peek A limit 3 peek B │ └────────────────────────────────────────────────────────────────────────────┘

可能不是這個解釋中的所有東西(甚至可能不是任何東西)在技術上都是完全正確的。 但是當我看到這樣的情況時,我很清楚哪些項目可以達到哪種連接指令。

暫無
暫無

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

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