Java Streamsが一度オフになるのはなぜですか?


239

C#とは異なりIEnumerable、実行パイプラインは何度でも実行できますが、Javaではストリームを1回だけ「反復」できます。

端末操作を呼び出すと、ストリームが閉じて使用できなくなります。この「機能」は多くの力を奪います。

これの理由技術的ではないと思います。この奇妙な制限の背後にある設計上の考慮事項は何でしたか?

編集:私が話していることを示すために、C#でのQuick-Sortの次の実装を検討してください。

IEnumerable<int> QuickSort(IEnumerable<int> ints)
{
  if (!ints.Any()) {
    return Enumerable.Empty<int>();
  }

  int pivot = ints.First();

  IEnumerable<int> lt = ints.Where(i => i < pivot);
  IEnumerable<int> gt = ints.Where(i => i > pivot);

  return QuickSort(lt).Concat(new int[] { pivot }).Concat(QuickSort(gt));
}

確かに、これがクイックソートの適切な実装であることを私は主張していません。ただし、これは、ラムダ式とストリーム操作を組み合わせた表現力の優れた例です。

そして、それはJavaではできません!ストリームを使用不可にすることなく、ストリームが空かどうかを確認することもできません。


4
ストリームを閉じると「力が奪われる」という具体的な例を挙げていただけますか?
ホジェリオ

23
ストリームからのデータを複数回使用する場合は、コレクションにダンプする必要があります。これはかなりそれがどのように持って仕事に:どちらかあなたはストリームを生成するために、計算をやり直す必要があります。または、中間結果を格納する必要があります。
Louis Wasserman

5
わかりましたが、同じ計算を同じストリームでやり直す間違って聞こえます。反復は、反復ごとにイテレータが作成されるのと同じように、計算が実行される前に特定のソースからストリームが作成されます。まだ実際の具体例を見たいと思います。結局、C#の列挙型に対応する方法が存在することを前提として、1回限りのストリームで各問題を解決するためのクリーンな方法があると思います。
ホジェリオ

2
この質問はC#IEnumerableを次のストリームに関連付けると思ったので、これは最初は私を混乱させましたjava.io.*
SpaceTrucker

9
C#でIEnumerableを複数回使用することは脆弱なパターンであるため、質問の前提に少し欠陥がある可能性があることに注意してください。IEnumerableの多くの実装では許可されていますが、許可されていないものもあります。コード分​​析ツールは、そのようなことをしないように警告する傾向があります。
サンダー

回答:


368

Streams APIの初期の設計から、設計の理論的根拠を明らかにするかもしれない思い出がいくつかあります。

2012年には、言語にラムダを追加していました。並列処理を容易にする、ラムダを使用してプログラムされたコレクション指向または「バルクデータ」操作セットが必要でした。操作を遅延して連鎖させるという考えは、この時点で十分に確立されていました。また、中間操作に結果を保存したくありませんでした。

決定する必要がある主な問題は、チェーン内のオブジェクトがAPIでどのように見えるか、およびそれらがどのようにデータソースに接続するかでした。ソースは多くの場合コレクションでしたが、ファイルやネットワークからのデータ、または乱数ジェネレーターなどのオンザフライで生成されたデータもサポートする必要がありました。

デザインに対する既存の作業の影響が多かった。最も影響力のあるのは、GoogleのGuavaライブラリーとScalaコレクションライブラリーでした。(誰かがグアバからの影響に驚いている場合、グアバの主任開発者であるケビンブーリリオンJSR-335 Lambdaエキスパートグループに参加していたことに注意してください。)Scalaコレクションでは、Martin Oderskyによるこの講演が特に興味深いことがわかりました:Future- Scalaコレクションの校正:可変から永続、並列へ。(スタンフォードEE380、2011年6月1日)

当時の私たちのプロトタイプ設計はに基づいていましたIterable。おなじみの操作filtermapなどは、の拡張(デフォルト)メソッドIterableでした。1つを呼び出すと、チェーンに操作が追加され、もう1つが返されIterableます。のようなターミナル操作はソースへのチェーンをcount呼び出しiterator()、操作は各ステージのイテレーター内に実装されました。

これらはIterableであるため、iterator()メソッドを複数回呼び出すことができます。それから何が起こるでしょうか?

ソースがコレクションの場合、これはほとんど問題なく機能します。コレクションは反復可能で、を呼び出すたびiterator()に、他のアクティブなインスタンスから独立した個別のIteratorインスタンスが生成され、それぞれが独立してコレクションを走査します。すごい。

ファイルから行を読み取るなど、ソースがワンショットの場合はどうでしょうか。たぶん、最初のイテレータはすべての値を取得するはずですが、2番目以降のイテレータは空でなければなりません。おそらく値はイテレータの間でインターリーブされるべきです。または、各イテレータはすべて同じ値を取得する必要があります。次に、イテレータが2つあり、一方がもう一方よりも先に進んでいる場合はどうでしょうか。誰かが値が読み込まれるまで、2番目のイテレータの値をバッファする必要があります。さらに悪いことに、1つのイテレーターを取得してすべての値を読み取り、その後 2番目のイテレーターを取得した場合はどうなるでしょうか。値は今どこから来ますか?誰かが2番目のイテレータを必要とする場合に備えて、それらすべてをバッファする必要がありますか?

明らかに、ワンショットソースに対して複数のイテレータを許可すると、多くの疑問が生じます。彼らには良い答えがありませんでした。iterator()2回呼び出すとどうなるかについて、一貫した予測可能な動作が必要でした。これにより、複数のトラバーサルを禁止し、パイプラインをワンショットにすることができました。

また、他のユーザーがこれらの問題にぶつかることも確認しました。JDKでは、ほとんどのIterableはコレクションまたはコレクションのようなオブジェクトであり、複数の走査が可能です。どこにも指定されていませんが、Iterablesが複数のトラバーサルを許可するという予期せぬ期待があったようです。注目すべき例外は、NIO DirectoryStreamインターフェースです。その仕様には、この興味深い警告が含まれています。

DirectoryStreamはIterableを拡張しますが、単一のイテレータのみをサポートするため、汎用のIterableではありません。イテレータメソッドを呼び出して2番目以降のイテレータを取得すると、IllegalStateExceptionがスローされます。

【原文で太字】

これは異常で不愉快に思えたため、1回だけである可能性のある新しいIterableの全体を作成したくありませんでした。これにより、Iterableを使用する必要がなくなりました。

このころ、ブルース・エッケルによる記事が現れ、彼がScalaで抱えていた問題点を説明した。彼はこのコードを書きました:

// Scala
val lines = fromString(data).getLines
val registrants = lines.map(Registrant)
registrants.foreach(println)
registrants.foreach(println)

とても簡単です。テキストの行を解析してRegistrantオブジェクトに変換し、2回出力します。ただし、実際に印刷されるのは1回だけです。registrants実際にはイテレータであるにもかかわらず、彼はそれをコレクションだと思ったことがわかりました。への2番目の呼び出しでforeachは、すべての値を使い尽くした空のイテレータが検出されるため、何も出力されません。

この種の経験から、複数のトラバーサルを試行する場合、明確に予測可能な結果を​​得ることが非常に重要であることがわかりました。また、遅延パイプラインのような構造と、データを格納する実際のコレクションを区別することの重要性も強調されました。これにより、レイジーパイプライン操作が新しいStreamインターフェースに分離され、熱心な変異操作のみが直接コレクションに保持されます。ブライアン・ゲッツはその理由を説明しています。

コレクションベースのパイプラインでは複数のトラバーサルを許可し、非コレクションベースのパイプラインでは許可しないことはどうですか?一貫性はありませんが、理にかなっています。ネットワークから値を読み取っている場合は、もちろんそれらを再びトラバースすることはできません。それらを複数回トラバースする場合は、それらを明示的にコレクションにプルする必要があります。

しかし、コレクションベースのパイプラインから複数のトラバーサルを許可する方法を見てみましょう。あなたがこれをしたとしましょう:

Iterable<?> it = source.filter(...).map(...).filter(...).map(...);
it.into(dest1);
it.into(dest2);

into操作は今スペルされていcollect(toList())ます。)

ソースがコレクションの場合、最初のinto()呼び出しはイテレーターのチェーンをソースに戻し、パイプライン操作を実行して、結果を宛先に送信します。への2番目の呼び出しinto()は、イテレータの別のチェーンを作成し、パイプライン操作を再度実行ます。これは明らかに間違っているわけではありませんが、各要素に対してすべてのフィルター操作とマップ操作をもう一度実行する効果があります。多くのプログラマーがこの振る舞いに驚いたと思います。

上で述べたように、私たちはグアバの開発者と話していました。彼らが持っているクールなものの1つは、アイデアの墓地で、実装しないことに決めた機能とその理由を説明しています。レイジーコレクションのアイデアはかなりクールに聞こえますが、これについて彼らが言わなければならないことがあります。List.filter()を返す操作を考えてみましょうList

ここでの最大の懸念は、あまりにも多くの操作が費用のかかる線形時間の命題になることです。コレクションやIterableだけでなく、リストをフィルタリングしてリストを取得したい場合は、を使用できますImmutableList.copyOf(Iterables.filter(list, predicate))。これは、何をしているのか、どのくらいの費用がかかるかを「前もって表明」します。

具体的な例を取るために、コストの何get(0)size()一覧には?のような一般的に使用されるクラスの場合ArrayList、それらはO(1)です。しかし、遅延フィルタリングされたリストでこれらの1つを呼び出す場合、バッキングリストに対してフィルターを実行する必要があり、突然これらの操作はすべてO(n)になります。さらに悪いことに、すべての操作でバッキングリストを走査する必要があります。

これはあまりにも怠惰であるように見えました。いくつかの操作を設定し、実際に実行を延期することは1つです。潜在的に大量の再計算を隠すような方法で設定することも別の方法です。

非線形または「再利用しない」ストリームを禁止することを提案する際に、Paul Sandozは、それらを許可することの潜在的な結果が「予期しないまたは混乱する結果」を引き起こすと説明しました。彼はまた、並列実行は物事をさらに難しくするだろうと述べました。最後に、予期しない操作が複数回、または少なくともプログラマーの予想とは異なる回数実行された場合、副作用のあるパイプライン操作は困難で不明瞭なバグにつながることを付け加えておきます。(しかし、Javaプログラマーは副作用のあるラムダ式を記述しませんか?それらを実行しますか?)

つまり、これがJava 8 Streams API設計の基本的な根拠であり、ワンショットトラバーサルを可能にし、厳密に線形(分岐なし)のパイプラインを必要とします。複数の異なるストリームソース間で一貫した動作を提供し、遅延操作と積極的な操作を明確に分離し、簡単な実行モデルを提供します。


に関してはIEnumerable、私はC#と.NETの専門家とはほど遠いので、間違った結論を出した場合は(穏やかに)修正されることを願っています。ただし、IEnumerable複数のトラバーサルがソースごとに異なる動作をすることは可能です。また、ネストされたIEnumerable操作の分岐構造を許可するため、かなりの再計算が必要になる場合があります。システムによってトレードオフが異なることを理解していますが、これらはJava 8 Streams APIの設計で回避しようとした2つの特性です。

OPが提供するクイックソートの例は興味深く、不可解で、申し訳ありませんが、少し恐ろしいものです。呼び出しがQuickSortかかるIEnumerableと返しIEnumerable、最終的にまではソートが実際に行われていないので、IEnumerable横断されます。ただし、呼び出しで行われているように見えるのIEnumerablesは、実際にそれを行わずに、クイックソートが行うパーティション分割を反映するツリー構造を構築することです。(結局のところ、これは遅延計算です。)ソースにN個の要素がある場合、ツリーは最大でN個の要素の幅になり、深さはlg(N)レベルになります。

これは、C#や.NETの専門家ではないようですが、これによりints.First()、を介したピボット選択などの特定の無害に見える呼び出しが、見た目よりも高価になるようです。もちろん、第1レベルではO(1)です。ただし、ツリーの深い右側のパーティションを検討してください。このパーティションの最初の要素を計算するには、ソース全体をトラバースする必要があります(O(N)操作)。ただし、上記のパーティションは遅延しているため、再計算する必要があり、O(lg N)比較が必要です。したがって、ピボットの選択はO(N lg N)演算であり、これはソート全体と同じくらい高価です。

しかし、返されたを走査するまで、実際にはソートしませんIEnumerable。標準のクイックソートアルゴリズムでは、パーティション化のレベルごとにパーティション数が2倍になります。各パーティションのサイズは半分だけなので、各レベルはO(N)の複雑さのままです。パーティションのツリーはO(lg N)高であるため、総作業量はO(N lg N)です。

遅延IEnumerableのツリーでは、ツリーの下部にN個のパーティションがあります。各パーティションの計算には、N個の要素のトラバーサルが必要です。各要素のトラバースには、ツリー全体でlg(N)の比較が必要です。ツリーの下部にあるすべてのパーティションを計算するには、O(N ^ 2 lg N)の比較が必要です。

(これで正しいですか?信じられません。誰か確認してください。)

いずれにせよ、IEnumerable複雑な計算構造を構築するためにこの方法を使用できることは確かに素晴らしいです。しかし、それが計算の複雑さを私が考えているほど増加させた場合、この方法でのプログラミングは、非常に注意深い場合を除いて避けられるべきものであるように思われます。


35
まず、素晴らしくて非屈辱的な答えをありがとう!これは断然最も正確であり、私が得た説明のとおりです。QuickSortの例に関する限り、intについては正しいようです。再帰レベルが高くなるにつれて、最初に肥大化します。これは「gt」と「lt」を熱心に計算することで簡単に修正できると思います(ToArrayで結果を収集することにより)。とはいえ、このスタイルのプログラミングでは予想外のパフォーマンスコストが発生する可能性があるという点は確かに支持されます。(2番目のコメントで続行)
Vitaliy 2015

18
一方、C#での経験(5年以上)から、パフォーマンスの問題にぶつかった(または禁止された場合、考えられないものを作成して導入した場合、「冗長な」計算を根絶することはそれほど難しくないことがわかります副作用があります)。C#のような可能性を犠牲にして、APIの純粋さを保証するためにあまりにも多くの妥協が行われたように私には思えました。あなたは間違いなく私の見方を調整するのを助けてくれました。
Vitaliy 2015

7
@Vitaliy公正な考えの交換をありがとう。この回答を調査して作成することで、C#と.NETについて少し学びました。
スチュアートマークス

10
小さなコメント:ReSharperは、C#を支援するVisual Studio拡張機能です。上記のQuickSortコードを使用するとints、ReSharperは使用ごとに「IEnumerableの複数の列挙が可能」という警告追加します。同じものをIEenumerable複数回使用することは疑わしいため、避けてください。この質問(私が回答しました)も指摘します。これは、.Netアプローチ(パフォーマンスの低下に加えて)に関する警告のいくつかを示しています。List<T>とIEnumerableの違い
Kobi

4
@Kobi ReSharperにそのような警告があることは非常に興味深いです。あなたの答えへのポインターをありがとう。C#/。NETがわからないので注意深く確認する必要がありますが、前述の設計上の懸念と同様の問題が発生しているようです。
Stuart Marks

122

バックグラウンド

質問は単純に見えますが、実際の答えには意味のある背景が必要です。結論にスキップしたい場合は、下にスクロールしてください...

比較ポイントを選択してください-基本機能

基本的な概念を使用すると、C#のIEnumerable概念は、必要な数のイテレーターを作成できるJavaIterableとより密接に関連します。を作成します。Javaの作成IEnumerablesIEnumeratorsIterableIterators

それぞれの概念の歴史は似ています。どちらもIEnumerableIterableデータコレクションのメンバーに対して「for-each」スタイルのループを許可するという基本的な動機があります。彼らは両方ともそれ以上のものを可能にするのでそれは単純化しすぎです、そして彼らはまた異なる進行を経てその段階に到達しましたが、それは関係なく重要な共通機能です。

その機能を比較してみましょう。両方の言語で、クラスがIEnumerable/を実装する場合、Iterableそのクラスは少なくとも1つのメソッドを実装する必要があります(C#の場合はそれGetEnumerator、Javaの場合はそれですiterator())。どちらの場合も、その(IEnumerator/ Iterator)から返されるインスタンスを使用して、データの現在のメンバーと後続のメンバーにアクセスできます。この機能は、for-each言語の構文で使用されます。

比較ポイントを選択してください-拡張機能

IEnumerableC#では、他の多くの言語機能(主にLinqに関連)を許可するように拡張されています。追加された機能には、選択、射影、集計などがあります。これらの拡張機能には、SQLやリレーショナルデータベースの概念と同様に、セット理論での使用から強い動機があります。

Java 8には、StreamsとLambdaを使用したある程度の関数型プログラミングを可能にする機能も追加されています。Java 8ストリームの主な動機は集合論ではなく、関数型プログラミングです。とにかく、多くの類似点があります。

これが2点目です。C#に加えられた拡張機能は、IEnumerable概念の拡張機能として実装されました。ただし、Javaでは、LambdaとStreamsの新しい基本概念を作成し、さらにStreams Iteratorsとの間で変換する比較的簡単な方法を作成することによって実装された拡張機能が実装されましたIterables

したがって、IEnumerableとJavaのStreamコンセプトの比較は不完全です。これをJavaのStreams APIとCollections APIを組み合わせたものと比較する必要があります。

Javaでは、StreamsはIterablesまたはIteratorsと同じではありません

ストリームは、イテレータと同じ方法で問題を解決するようには設計されていません。

  • イテレータは、データのシーケンスを記述する方法です。
  • ストリームは、一連のデータ変換を記述する方法です。

を使用するIteratorと、データ値を取得して処理した後、別のデータ値を取得できます。

Streamsを使用すると、一連の関数をチェーン化し、入力値をストリームにフィードして、結合されたシーケンスから出力値を取得します。Java用語では、各関数は単一のStreamインスタンスにカプセル化されることに注意してください。Streams APIを使用するとStream、一連の変換式をチェーンする方法でインスタンスのシーケンスをリンクできます。

このStream概念を完成させるには、ストリームをフィードするデータのソースと、ストリームを消費するターミナル関数が必要です。

ストリームに値をフィードする方法は、実際にはからのものである可能性IterableがありStreamますが、シーケンス自体はではなく、Iterable複合関数です。

A Streamは、値を要求した場合にのみ機能するという意味で、遅延を意図しています。

ストリームのこれらの重要な前提と機能に注意してください。

  • A StreamJavaでは、それは別の状態であることに、一つの状態のデータ項目を変換し、変換エンジンです。
  • ストリームには、データの順序や位置の概念はありません。要求されたものを単に変換するだけです。
  • ストリームには、他のストリーム、イテレータ、イテラブル、コレクションなど、多くのソースからのデータを提供できます。
  • 「変換の再プログラミング」のようなストリームを「リセット」することはできません。データソースのリセットは、おそらくあなたが望んでいることです。
  • 論理的には、ストリーム内に「進行中」のデータ項目は常に1つしかありません(ストリームが並列ストリームでない限り、その時点でスレッドごとに1つの項目があります)。これは、ストリームに提供するために現在のアイテムよりも「準備ができている」より多くのデータソース、または複数の値を集計および削減する必要があるストリームコレクターとは無関係です。
  • ストリームは非バインド(無限)にすることができ、データソースまたはコレクタ(無限にすることもできます)によってのみ制限されます。
  • ストリームは「チェーン可能」であり、1つのストリームのフィルタリングの出力は別のストリームです。ストリームに入力され、ストリームによって変換された値は、別の変換を行う別のストリームに順番に供給できます。変換された状態のデータは、あるストリームから次のストリームに流れます。データを介入して1つのストリームからプルし、次のストリームに接続する必要はありません。

C#の比較

Javaストリームが供給、ストリーム、収集システムの一部であり、ストリームとイテレーターがコレクションと一緒に使用されることが多いと考えると、同じ概念に関連するのが難しいのも不思議ではありません。ほとんどすべてIEnumerableがC#の単一の概念に組み込まれています。

IEnumerableの一部(および密接に関連する概念)は、Java Iterator、Iterable、Lambda、およびStreamのすべての概念で明らかです。

IEnumerableの方が難しいJavaの概念で実行できる小さなことや、その逆もあります。


結論

  • ここではデザインの問題はなく、言語間の概念の一致の問題だけです。
  • ストリームは別の方法で問題を解決します
  • ストリームはJavaに機能を追加します(物事を実行する別の方法を追加し、機能を奪いません)

ストリームを追加すると、問題を解決する際により多くの選択肢が提供されます。これは、問題を「削減」、「削除」、または「制限」するのではなく、「強化」として分類するのが妥当です。

Java Streamsが一度オフになるのはなぜですか?

ストリームはデータではなく関数シーケンスであるため、この質問は誤解されています。ストリームをフィードするデータソースに応じて、データソースをリセットし、同じまたは異なるストリームをフィードできます。

実行パイプラインを何度でも実行できるC#のIEnumerableとは異なり、Javaでは、ストリームを「反復」できるのは1回だけです。

とを比較するIEnumerableStreamは間違っています。言うのに使用しているコンテキストはIEnumerable、何回でも実行できるため、何回Iterablesでも反復できるJavaと比較すると最適です。Java StreamIEnumerable概念のサブセットであり、データを提供するサブセットではないため、「再実行」することはできません。

端末操作を呼び出すと、ストリームが閉じて使用できなくなります。この「機能」は多くの力を奪います。

最初のステートメントは、ある意味で真実です。「力を奪う」ステートメントはそうではありません。Streams it IEnumerablesをまだ比較しています。ストリームのターミナル操作は、forループの「break」句のようなものです。必要に応じて、必要なデータを再供給できる場合は、いつでも別のストリームを自由に使用できます。あなたが考える場合は、再度、IEnumerableより多くのようになりIterable、この文で、Javaはうまくそれをしません。

これの理由は技術的ではないと思います。この奇妙な制限の背後にある設計上の考慮事項は何でしたか?

その理由は技術的であり、単純な理由で、Streamはそれが何であるかを考えるサブセットです。ストリームのサブセットはデータ供給を制御しないため、ストリームではなく供給をリセットする必要があります。その文脈では、それはそれほど奇妙ではありません。

QuickSortの例

クイックソートの例にはシグネチャがあります:

IEnumerable<int> QuickSort(IEnumerable<int> ints)

入力IEnumerableをデータソースとして扱います。

IEnumerable<int> lt = ints.Where(i => i < pivot);

また、戻り値IEnumerableもデータの供給です。これはソート操作であるため、供給の順序は重要です。Listは保証された順序または反復を持つデータの供給であるため、Java Iterableクラスがこれに適したもの、特にのList特殊化であると考える場合Iterable、コードと同等のJavaコードは次のようになります。

Stream<Integer> quickSort(List<Integer> ints) {
    // Using a stream to access the data, instead of the simpler ints.isEmpty()
    if (!ints.stream().findAny().isPresent()) {
        return Stream.of();
    }

    // treating the ints as a data collection, just like the C#
    final Integer pivot = ints.get(0);

    // Using streams to get the two partitions
    List<Integer> lt = ints.stream().filter(i -> i < pivot).collect(Collectors.toList());
    List<Integer> gt = ints.stream().filter(i -> i > pivot).collect(Collectors.toList());

    return Stream.concat(Stream.concat(quickSort(lt), Stream.of(pivot)),quickSort(gt));
}    

並べ替えが重複した値を適切に処理しないというバグ(私が再現したもの)があることに注意してください。これは「一意の値」の並べ替えです。

また、Javaコードがデータソース(List)をどのように使用しているか、およびストリームの概念をさまざまな時点で使用していることにも注意してくださいIEnumerable。また、Listベースタイプとして使用しましたが、より一般的なを使用することもできCollection、小さな反復子からストリームへの変換を使用すると、さらに一般的なIterable


9
ストリームを「反復」することを考えている場合、それは間違っています。ストリームは、一連の変換の特定の時点におけるデータの状態を表します。データはストリームソースでシステムに入り、最後に収集、削減、またはダンプされるまで、あるストリームから次のストリームに流れ、状態が変化します。A Stream
特定の

7
ストリームを使用すると、Xのように見えるストリームに入り、Yのように見えるストリームを出るデータがあります。ストリームがその変換を実行する関数があります。ストリームは関数をf(x)カプセル化しますが、流れるデータはカプセル化しません
rolfl

4
IEnumerableデータが存在する前に、ランダムな値を提供し、バインドを解除してアクティブにすることもできます。
アルトゥーロトーレスサンチェス

6
@Vitaliy:IEnumerable<T>複数回繰り返される可能性がある有限のコレクションを表すことが期待される多くのメソッド。反復可能であるがそれらの条件を満たさないものはIEnumerable<T>、他の標準的なインターフェースが適切でないために実装されていますが、複数回反復できる有限のコレクションを期待するメソッドは、それらの条件を満たさない反復可能なものが与えられるとクラッシュする傾向があります。
スーパーキャット2015

5
あなたのquickSort例はそれがを返した場合、はるかに単純になるでしょうStream。2つの.stream()呼び出しと1 つの呼び出しが保存され.collect(Collectors.toList())ます。その後、コードを置き換えるCollections.singleton(pivot).stream()Stream.of(pivot)、ほぼ読みやすくなります...
Holger

22

StreamSpliteratorは、ステートフルで変更可能なオブジェクトであるを中心に構築されます。それらには「リセット」アクションがなく、実際には、そのような巻き戻しアクションをサポートする必要があると、「多くの力を奪う」ことになります。Random.ints()そのようなリクエストをどのように処理するべきですか?

一方、Streamリトレース可能なオリジンを持つsの場合、同等物Streamを簡単に作成して再利用できます。を構築するために行った手順をStream再利用可能なメソッドに入れるだけです。これらの手順はすべて遅延操作であるため、これらの手順を繰り返すことは負荷の高い操作ではないことに注意してください。実際の作業は端末操作から始まり、実際の端末操作によってはまったく異なるコードが実行される場合があります。

メソッドを2回呼び出すことの意味を指定するのは、そのようなメソッドの作成者であるあなた次第です。変更されていない配列またはコレクション用に作成されたストリームが行うのとまったく同じシーケンスを再現しますか、それとも、セマンティクスは似ていますが、ランダムな整数のストリームやコンソール入力行のストリームなど、さまざまな要素があります。


ところで、混乱を避けるために、端末操作が消費するStreamとは区別されるが閉鎖Stream呼び出すようにclose()ストリームに(例えばによって生成、など関連したリソースを有するストリームに要求されるんFiles.lines())。


多くの混乱が比較misguiding由来と思われるIEnumerableとしStreamIEnumerable実際に提供する能力を表しIEnumerator、そうそのようなIterableジャワインチ 対照的に、a Streamは一種のイテレータであり、aに匹敵するIEnumeratorので、この種のデータ型は.NETで複数回使用できると主張するのは誤りIEnumerator.Resetです。サポートはオプションです。ここで説明する例では、新しいIEnumerableをフェッチするためにを使用でき、Java でも機能するという事実を使用しています。あなたは新しいを得ることができます。Java開発者が操作を直接に追加することを決定した場合、中間操作は別の操作を返します IEnumeratorCollectionStreamStreamIterableIterable、それは本当に同等であり、同じように機能することができました。

ただし、開発者はこれに反対し、決定はこの質問で議論されます。最大のポイントは、熱心なコレクション操作と遅延ストリーム操作に関する混乱です。.NET APIを見ると、私(はい、個人的に)は正当化されています。IEnumerable単独で見ると合理的に見えますが、特定のコレクションには、コレクションを直接操作する多くのメソッドと、レイジーを返す多くのメソッドがありますが、メソッドIEnumerableの特定の性質は常に直感的に認識できるとは限りません。(私はそれを見て、数分以内)私が見つけた最悪の例では、あるList.Reverse()名前が一致して正確に継承されたの名前を(これは拡張メソッドのための右の末端である?)Enumerable.Reverse()完全に矛盾する行動を持ちながら。


もちろん、これらは2つの異なる決定です。最初Streamの型はIterable/ Collectionと型を区別し、2番目の型Streamは別の種類の反復可能型ではなく、ある種類の1回の反復子を作成します。しかし、これらの決定は一緒に行われたため、これらの2つの決定を分離することは決して考慮されなかったのは事実かもしれません。.NETに匹敵するように作成されていません。

実際のAPI設計の決定は、改良されたタイプのイテレーターを追加することでしたSpliteratorSpliteratorsは、古いIterables(これらがどのように改造されたか)または完全に新しい実装によって提供できます。次に、Stream高レベルのフロントエンドとして、低レベルSpliteratorのs に追加されました。それでおしまい。別のデザインの方が良いかどうかについて話し合うかもしれませんが、それは生産的ではありません。現在のデザインの方法を考えると、それは変更されません。

考慮しなければならない別の実装面があります。不変のデータ構造でStreamはありません。各中間操作はStream、古いインスタンスをカプセル化する新しいインスタンスを返す場合がありますが、代わりに独自のインスタンスを操作してそれ自体を返す場合もあります(同じ操作で両方を実行することもできます)。一般的に知られている例は次のように動作しているparallelか、unordered別のステップを追加するが、パイプライン全体を操作しません)。そのような変更可能なデータ構造を持ち、再利用を試みる(さらに悪いことに、同時に複数回使用する)とうまく機能しません…


完全を期すために、Java StreamAPIに翻訳したクイックソートの例を次に示します。それはそれが本当に「多くの力を奪う」わけではないことを示しています。

static Stream<Integer> quickSort(Supplier<Stream<Integer>> ints) {

  final Optional<Integer> optPivot = ints.get().findAny();
  if(!optPivot.isPresent()) return Stream.empty();

  final int pivot = optPivot.get();

  Supplier<Stream<Integer>> lt = ()->ints.get().filter(i -> i < pivot);
  Supplier<Stream<Integer>> gt = ()->ints.get().filter(i -> i > pivot);

  return Stream.of(quickSort(lt), Stream.of(pivot), quickSort(gt)).flatMap(s->s);
}

それはのように使用することができます

List<Integer> l=new Random().ints(100, 0, 1000).boxed().collect(Collectors.toList());
System.out.println(l);
System.out.println(quickSort(l::stream)
    .map(Object::toString).collect(Collectors.joining(", ")));

あなたはそれをさらにコンパクトに書くことができます

static Stream<Integer> quickSort(Supplier<Stream<Integer>> ints) {
    return ints.get().findAny().map(pivot ->
         Stream.of(
                   quickSort(()->ints.get().filter(i -> i < pivot)),
                   Stream.of(pivot),
                   quickSort(()->ints.get().filter(i -> i > pivot)))
        .flatMap(s->s)).orElse(Stream.empty());
}

1
まあ、消費するかどうか、もう一度消費しようとすると、ストリームがすでに閉じられていて消費されていないという例外がスローされます。ランダム整数のストリームのリセットに関する問題については、あなたが言ったように、リセット操作の正確なコントラクトを定義するのはライブラリの作成者次第です。
Vitaliy、2015

2
いいえ、メッセージは「ストリームはすでに操作されているか、閉じられています」であり、「リセット」操作について話しているのではなく、2つ以上の端末操作を呼び出すのStreamに対し、ソースSpliteratorのリセットは暗黙のうちにあります。そして、それが可能であるかどうかは確かです。「なぜcount()2回呼び出すとStream毎回異なる結果が出るのか」などの質問がありました…
Holger

1
count()が異なる結果を返すことは絶対に有効です。count()はストリームに対するクエリであり、ストリームが変更可能である場合(正確には、ストリームは変更可能なコレクションに対するクエリの結果を表します)、予期されます。C#のAPIをご覧ください。彼らはこれらすべての問題を優雅に扱います。
Vitaliy、2015

4
「絶対に有効」と呼ぶのは、直感に反する動作です。結局のところ、結果を処理するためにストリームを複数回使用することについて尋ねる主な動機は、同じであると予想されますが、異なる方法です。再利用不可能な性質についてのSO上のすべてのご質問Streamならば黙って壊れた溶液につながったの、これまでに(そうしないと、通知をしないで、明らかに)、端末の操作を複数回呼び出すことで問題を解決しようとする試みから茎StreamAPIがそれを許さ評価ごとに結果が異なります。ここに良い例があります。
Holger

3
実際、あなたの例は、プログラマーが複数の端末操作を適用することの意味を理解していない場合にどうなるかを完全に示しています。これらの各操作がまったく異なる要素のセットに適用されるとどうなるかを考えてみてください。これは、ストリームのソースが各クエリで同じ要素を返した場合にのみ機能しますが、これは私たちが話している間違った仮定です。
Holger、

8

よく見ると、両者の違いはほとんどないと思います。

一見すると、an IEnumerableは再利用可能な構成要素のように見えます。

IEnumerable<int> numbers = new int[] { 1, 2, 3, 4, 5 };

foreach (var n in numbers) {
    Console.WriteLine(n);
}

ただし、コンパイラーは実際に私たちを助けるために少し作業をしています。次のコードを生成します。

IEnumerable<int> numbers = new int[] { 1, 2, 3, 4, 5 };

IEnumerator<int> enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext()) {
    Console.WriteLine(enumerator.Current);
}

列挙型を実際に反復するたびに、コンパイラーは列挙子を作成します。列挙子は再利用できません。以降の呼び出しはMoveNextfalseを返すだけで、最初にリセットする方法はありません。数値をもう一度繰り返し処理する場合は、別の列挙子インスタンスを作成する必要があります。


IEnumerableがJavaストリームと同じ「機能」を持っている(持っている可能性がある)ことをよりよく示すために、数値のソースが静的コレクションではない列挙型を検討してください。たとえば、5つの乱数のシーケンスを生成する列挙可能なオブジェクトを作成できます。

class Generator : IEnumerator<int> {
    Random _r;
    int _current;
    int _count = 0;

    public Generator(Random r) {
        _r = r;
    }

    public bool MoveNext() {
        _current= _r.Next();
        _count++;
        return _count <= 5;
    }

    public int Current {
        get { return _current; }
    }
 }

class RandomNumberStream : IEnumerable<int> {
    Random _r = new Random();
    public IEnumerator<int> GetEnumerator() {
        return new Generator(_r);
    }
    public IEnumerator IEnumerable.GetEnumerator() {
        return this.GetEnumerator();
    }
}

これで、前の配列ベースの列挙可能なものと非常によく似たコードができましたが、2回目の反復が終了していますnumbers

IEnumerable<int> numbers = new RandomNumberStream();

foreach (var n in numbers) {
    Console.WriteLine(n);
}
foreach (var n in numbers) {
    Console.WriteLine(n);
}

2回目の繰り返しnumbersでは、同じ意味で再利用できない異なる数列が得られます。または、RandomNumberStreamそれを複数回反復しようとすると例外がスローされるようにを記述し、列挙型を実際には使用できなくする(Javaストリームなど)ことができます。

また、列挙可能ベースのクイックソートは、RandomNumberStream


結論

したがって、最大の違いは、.NETでは、シーケンスの要素にアクセスする必要があるときはいつでも、バックグラウンドでIEnumerable新しいIEnumeratorを暗黙的に作成することにより、を再利用できることです。

コレクションを繰り返し反復できるので、この暗黙の動作はしばしば有用です(そして、あなたが述べるように「強力」です)。

しかし、時には、この暗黙の動作が実際に問題を引き起こす可能性があります。データソースが静的でない場合、またはデータベースやWebサイトのようにアクセスにコストがかかる場合は、多くの仮定をIEnumerable破棄する必要があります。再利用はそれほど単純ではありません


2

Stream APIの「1回だけ」の保護の一部をバイパスすることが可能です。たとえばjava.lang.IllegalStateExceptionSpliteratorStream直接ではなく)を参照して再利用することで、(「ストリームは既に操作されているか、閉じられています」というメッセージの付いた)例外を回避できます。

たとえば、次のコードは例外をスローせずに実行されます。

    Spliterator<String> split = Stream.of("hello","world")
                                      .map(s->"prefix-"+s)
                                      .spliterator();

    Stream<String> replayable1 = StreamSupport.stream(split,false);
    Stream<String> replayable2 = StreamSupport.stream(split,false);


    replayable1.forEach(System.out::println);
    replayable2.forEach(System.out::println);

ただし、出力は以下に制限されます

prefix-hello
prefix-world

出力を2回繰り返すのではなく。これはArraySpliteratorStreamソースとして使用されるがステートフルであり、現在の位置を格納するためです。これを再生Streamすると、最後から再開します。

この課題を解決するためのオプションがいくつかあります。

  1. Streamようなステートレスな作成方法を利用できStream#generate()ます。独自のコードで状態を外部で管理し、Stream「リプレイ」間でリセットする必要があります。

    Spliterator<String> split = Stream.generate(this::nextValue)
                                      .map(s->"prefix-"+s)
                                      .spliterator();
    
    Stream<String> replayable1 = StreamSupport.stream(split,false);
    Stream<String> replayable2 = StreamSupport.stream(split,false);
    
    
    replayable1.forEach(System.out::println);
    this.resetCounter();
    replayable2.forEach(System.out::println);
  2. これに対するもう1つの(やや優れているが完全ではない)解決策は、現在のカウンターをリセットするための容量を含む独自のArraySpliterator(または同様のStreamソース)を記述することです。生成に使用すると、Stream正常に再生できる可能性があります。

    MyArraySpliterator<String> arraySplit = new MyArraySpliterator("hello","world");
    Spliterator<String> split = StreamSupport.stream(arraySplit,false)
                                            .map(s->"prefix-"+s)
                                            .spliterator();
    
    Stream<String> replayable1 = StreamSupport.stream(split,false);
    Stream<String> replayable2 = StreamSupport.stream(split,false);
    
    
    replayable1.forEach(System.out::println);
    arraySplit.reset();
    replayable2.forEach(System.out::println);
  3. この問題に対する最善の解決策(私の意見では)は、新しいオペレーターがで呼び出されたときSpliteratorに、Streamパイプラインで使用されるすべてのステートフルの新しいコピーを作成することStreamです。これはより複雑で実装に関与しますが、サードパーティのライブラリを使用しても構わない場合は、cyclops-reactStreamはまさにこれを実行する実装を備えています。(開示:私はこのプロジェクトの主要開発者です。)

    Stream<String> replayableStream = ReactiveSeq.of("hello","world")
                                                 .map(s->"prefix-"+s);
    
    
    
    
    replayableStream.forEach(System.out::println);
    replayableStream.forEach(System.out::println);

これは印刷されます

prefix-hello
prefix-world
prefix-hello
prefix-world

予想通り。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.