Jdk 8のJava.util.stream
に含まれる幅広いクエリメソッドを利用するために、*
多重度(0個以上のインスタンス)との関係のゲッターがStream<T>
またはIterable<T>
ではなくIterator<T>
を返すドメインモデルを設計しようとしました。
私の疑問は、Stream<T>
と比較してIterator<T>
によって追加のオーバーヘッドが発生するかどうかです。
それで、Stream<T>
を使用してドメインモデルを侵害することの欠点はありますか?
または代わりに、常にIterator<T>
またはIterable<T>
を返し、イテレータをStreamUtils
に変換することにより、ストリームを使用するかどうかを選択する決定をエンドユーザーに委ねるべきですか?
注Collection
を返すことは有効なオプションではありません。この場合、ほとんどの関係が遅延しており、サイズが不明です。
ここにはパフォーマンスに関する多くのアドバイスがありますが、残念なことにその多くは当て推量であり、実際のパフォーマンスに関する考慮事項を示すものはほとんどありません。
@Holger 正しく パフォーマンスの尾がAPI設計犬を揺さぶる一見圧倒的な傾向に抵抗する必要があることを指摘することで。
どのような場合でも、ストリームを他の形式のトラバーサルよりも遅く、同じ、または速くできる無数の考慮事項がありますが、重要なのは、ストリームがパフォーマンス上の優位性を持っていることを示すいくつかの要因がありますデータセット。
Stream
の作成と比較して、creatingIterator
の追加の固定スタートアップオーバーヘッドがいくつかあります-計算を開始する前のいくつかのオブジェクト。データセットが大きい場合は問題ではありません。それは多くの計算で償却される小さなスタートアップ費用です。 (そして、あなたのデータセットが小さいなら、それはおそらく重要ではありません-あなたのプログラムが小さなデータセットで動作しているなら、パフォーマンスは一般的にあなたの一番の関心事でもないからです。)これは問題は並行するときです。パイプラインのセットアップに費やした時間は、アムダールの法則の連続した部分になります。実装を見ると、ストリームのセットアップ中にオブジェクトのカウントダウンを維持するために一生懸命働きますが、並列が勝ち始める損益分岐点のデータセットのサイズに直接影響するので、それを減らす方法を見つけたいですシーケンシャル。
ただし、固定起動コストよりも重要なのは、要素ごとのアクセスコストです。ここでは、ストリームが実際に勝ちます-そしてしばしば大きく勝ちます-一部は驚くかもしれません。 (パフォーマンステストでは、対応するCollection
よりもforループを上回ることができるストリームパイプラインを定期的に確認します。)また、これについて簡単な説明があります。Spliterator
は、基本的に要素ごとのアクセスコストがIterator
よりも基本的に低く、順次でもです。これにはいくつかの理由があります。
Iteratorプロトコルは基本的に効率が劣ります。各要素を取得するには、2つのメソッドを呼び出す必要があります。さらに、イテレータはnext()
なしでhasNext()
を呼び出す、またはhasNext()
なしでnext()
を複数回呼び出すなど、これらのメソッドの両方に対して堅牢でなければならないため、一般に、防御的なコーディング(および一般に、より多くのステートフル性と分岐)を行う必要があり、非効率性が増します。一方、スプリッテレーター(tryAdvance
)をゆっくりと移動する方法でも、この負担はありません。 (next
/hasNext
の双対性は基本的に際どいため、同時データ構造の場合はさらに悪化します。また、Iterator
の実装は、Spliterator
の実装よりも多くの作業を行う必要があります。)
Spliterator
は、「高速パス」反復(forEachRemaining
)をさらに提供します。これは、ほとんどの場合(削減、forEach)使用でき、データ構造内部へのアクセスを仲介する反復コードのオーバーヘッドをさらに削減します。また、これは非常に適切にインライン化される傾向があり、コードモーション、境界チェックの除去など、他の最適化の有効性が向上します。
さらに、Spliterator
を介したトラバースでは、Iterator
を使用した場合よりもヒープ書き込みが少なくなる傾向があります。 Iterator
を使用すると、すべての要素が1つまたは複数のヒープ書き込みを引き起こします(Iterator
をエスケープ分析でスカラー化して、そのフィールドをレジスターに引き上げる場合を除きます)。一方、Spliterators
は状態が少ない傾向があり、産業用のforEachRemaining
実装は、トラバーサルの終わりまでヒープへの書き込みを延期する傾向があり、代わりに、レジスターに自然にマップされる反復状態をローカルに保存して、メモリを削減しますバスアクティビティ。
要約:心配しないで、幸せになってください。 Spliterator
は、並列処理がなくても優れたIterator
です。 (また、一般的に書くのが簡単で、間違えにくいです。)
ソースがArrayList
であると仮定して、すべての要素を反復する一般的な操作を比較しましょう。次に、これを達成するための3つの標準的な方法があります。
_final E[] elementData = (E[]) this.elementData; final int size = this.size; for (int i=0; modCount == expectedModCount && i < size; i++) { action.accept(elementData[i]); }
_
_final Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) { throw new ConcurrentModificationException(); } while (i != size && modCount == expectedModCount) { consumer.accept((E) elementData[i++]); }
_
_Stream.forEach
_呼び出してしまう _Spliterator.forEachRemaining
_
_if ((i = index) >= 0 && (index = hi) <= a.length) { for (; i < hi; ++i) { @SuppressWarnings("unchecked") E e = (E) a[i]; action.accept(e); } if (lst.modCount == mc) return; }
_
ご覧のとおり、これらの操作が終了する実装コードの内部ループは基本的に同じで、インデックスを反復処理し、配列を直接読み取り、Consumer
に要素を渡します。
JREのすべての標準コレクションにも同様のことが当てはまります。読み取り専用のラッパーを使用している場合でも、それらはすべて、あらゆる方法で実装を適合させています。後者の場合、Stream
APIはわずかに勝ちます。元のコレクションのforEach
に委任するには、読み取り専用ビューで_Collection.forEach
_を呼び出す必要があります。同様に、remove()
メソッドを呼び出そうとする試みから保護するために、イテレーターをラップする必要があります。対照的に、spliterator()
は変更のサポートがないため、元のコレクションのSpliterator
を直接返すことができます。したがって、読み取り専用ビューのストリームは、元のコレクションのストリームとまったく同じです。
前述のように、実際のパフォーマンスを測定する場合、これらのすべての違いに気付くことはほとんどありませんが、内部ループはすべての場合に同じ。
問題は、そこからどの結論を引き出すかです。呼び出し元は依然としてstream().forEach(…)
を呼び出して元のコレクションのコンテキストで直接反復するため、読み取り専用のラッパービューを元のコレクションに返すことができます。
パフォーマンスに大きな違いはないため、 「コレクションまたはストリームを返す必要がありますか?」