web-dev-qa-db-ja.com

Java 8ストリーム:limit()とskip()の違い

このコードを実行すると、Streamsについて話す

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

ストリームを最初の3つのコンポーネントに制限すると、アクションABおよびCは3回だけ実行されます。

skip()メソッドを使用して最後の3つの要素に対して類似の計算を実行しようとすると、異なる動作が示されます。

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

なぜ、この場合、アクションA1からA6が実行されているのですか? limit短絡ステートフル中間操作 であるという事実と何らかの関係がなければなりませんが、whileskipはありませんが、このプロパティの実際的な意味を理解していません。 「skipの前にすべてのアクションが実行され、一方でlimitis "?

61
Luigi Cortese

ここには、2つのストリームパイプラインがあります。

これらのストリームパイプラインは、それぞれソース、いくつかの中間操作、およびターミナル操作で構成されます。

しかし、中間操作は怠zyです。つまり、ダウンストリーム操作でアイテムが必要にならない限り、何も起こりません。その場合、中間操作は必要なアイテムを作成するために必要なすべてを実行し、別のアイテムが要求されるまで再び待機します。

端末操作は通常「熱心」です。つまり、彼らは完了するために必要なストリーム内のすべてのアイテムを要求します。

したがって、パイプラインは、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が4番目のアイテムを要求するとき、リクエストがlimitに到達すると、それは与えられるすべてのアイテムをすでに与えていることを知っています。

したがって、「A」ピークに別のアイテムを要求することはありません。それは、そのアイテムが使い果たされたことをすぐに示し、したがって、これ以上のアクションは実行されず、forEachは終了します。

2番目のパイプラインで何が起こりますか?

    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に到達すると、ダウンストリームを1つ渡す前に、アップストリームから6つのアイテムを要求する必要があることがわかります。そのため、「A」peekの上流で要求を行い、下流に渡さずにそれを消費し、別の要求を行います。そのため、「A」ピークは、アイテムに対する6つのリクエストを取得し、6つのプリントを生成しますが、これらのアイテムは渡されません。

A1
A2
A3
A4
A5
A6

skipによる7番目の要求で、アイテムは「B」ピークに渡され、そこからforEachに渡されるため、完全な印刷が行われます。

A7B7C7

それは前と同じです。 skipは、リクエストを取得するたびに、すでにスキップジョブを実行したことを「認識」しているため、上流のアイテムを要求し、下流に渡します。したがって、残りのプリントは、ソースが使い果たされるまで、パイプ全体を通過します。

94
RealSkeptic

ストリーム化されたパイプラインの流notな表記が、この混乱の原因です。次のように考えてください:

limit(3)

terminal operation であるforEach()を除くすべてのパイプライン操作は遅延評価されます。これは「パイプラインの実行」をトリガーします。

パイプラインが実行されると、中間ストリームの定義は、"before"または"after"がどうなるかについての仮定を行いません。彼らがしているのは、入力ストリームを取得し、それを出力ストリームに変換することだけです。

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に渡し、3番目の値の後にパイプラインを中止します。 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は、以前に遭遇したすべての値を消費し、"new"値のみを渡します。つまり、i2から合計2つの値が出ます。
  • i3は10個の値を渡し、停止します。

i3i20の後に1がさらに8つの値を生成するのを待つが、i1は決して表示されないため、このアルゴリズムは停止しません。 i2への値の供給を停止します。

パイプラインのある時点で、10を超える値が生成されたことは問題ではありません。重要なのは、i3がこれらの10個の値を見たことがないということです。

質問に答えるには:

「スキップ前のすべてのアクションが実行され、制限前の全員が実行されるわけではない」というだけですか?

いや。 skip()またはlimit()の前のすべての操作が実行されます。両方の実行で、A1-A3を取得します。ただし、limit()はパイプラインを短絡させ、対象のイベント(制限に達した)が発生すると値の消費を中止する場合があります。

11
Lukas Eder

Steamの操作を個別に見るのは完全に冒aspです。ストリームが評価される方法ではないからです。

limit(3)について話すと、これは短絡操作です。これは、beforeおよびafterlimit、ストリームに制限がある場合、n要素を取得した後、反復を停止しますtillこれは、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

ストリームの6つの要素が処理されますが、3つのストリーム要素が操作チェーンを通過するのを制限が待機しているためです。

8
Amm Sokun

すべてのストリームは、基本的に2つの操作があるスプリッテレーターに基づいています:アドバンス(イテレーターと同様に1つの要素を前方に移動)とスプリット(並列処理に適した任意の位置に自分を分割)。好きなときに入力要素の取得を停止できます(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
4
Tagir Valeev

たぶん、この小さな図は、ストリームがどのように処理されるかについての自然な「感覚」を得るのに役立ちます。

最初の行=>8=>=7=...===は、ストリームを示しています。要素1..8は左から右に流れています。 3つの「ウィンドウ」があります。

  1. 最初のウィンドウ(peek A)にすべてが表示されます
  2. 2番目のウィンドウ(skip 6またはlimit 3)では、一種のフィルタリングが行われます。最初の要素または最後の要素のいずれかが「削除」されます-さらなる処理のために渡されないことを意味します。
  3. 3番目のウィンドウには、渡されたアイテムのみが表示されます

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

おそらく、この説明のすべてが技術的に完全に正しいわけではありません。しかし、このように表示される場合、どのアイテムが連結された命令のどれに到達するかは明確です。

0
yaccob