web-dev-qa-db-ja.com

コレクションまたはストリームを返す必要がありますか?

メンバーリストに読み取り専用ビューを返すメソッドがあるとします。

class Team {
    private List < Player > players = new ArrayList < > ();

    // ...

    public List < Player > getPlayers() {
        return Collections.unmodifiableList(players);
    }
}

さらに、クライアントが行うことは、リストを1回、すぐに繰り返すことです。プレイヤーをJListか何かに入れるかもしれません。クライアントはnot後で検査するためにリストへの参照を保存します!

この一般的なシナリオを考えると、代わりにストリームを返す必要がありますか?

public Stream < Player > getPlayers() {
    return players.stream();
}

または、Javaで非イディオムではないストリームを返していますか?ストリームは、作成された同じ表現内で常に「終了」するように設計されていましたか?

147
fredoverflow

答えは、いつものように「依存する」です。返されるコレクションの大きさに依存します。結果が時間とともに変化するかどうか、および返される結果の一貫性がどれほど重要かによって異なります。そして、ユーザーがどのように答えを使用する可能性があるかに大きく依存します。

まず、ストリームからいつでもコレクションを取得でき、その逆も可能です。

// If API returns Collection, convert with stream()
getFoo().stream()...

// If API returns Stream, use collect()
Collection<T> c = getFooStream().collect(toList());

質問は、発信者にとってより便利です。

結果が無限である可能性がある場合、選択肢は1つだけです:ストリーム。

結果が非常に大きい場合は、Streamを優先します。一度にすべてを具体化することには価値がない可能性があり、そうすると大きなヒーププレッシャーが発生する可能性があります。

呼び出し元がそれを反復するだけの場合(検索、フィルター、集約)、Streamを優先する必要があります。Streamにはこれらが既に組み込まれており、コレクションを具体化する必要がないためです(特に、ユーザーが全体の結果。)これは非常に一般的なケースです。

ユーザーがそれを複数回繰り返すか、または保持することがわかっている場合でも、それを配置するために選択したコレクション(ArrayListなど)が単純でないため、代わりにStreamを返したい場合があります。彼らが望むフォーム、そして呼び出し側はとにかくそれをコピーする必要があります。ストリームを返す場合、彼らはcollect(toCollection(factory))を実行し、希望する形式で正確に取得できます。

上記の「ストリームを優先」のケースは、ストリームがより柔軟であるという事実からほとんど派生しています。コレクションに具体化するためのコストと制約を受けることなく、使用方法に遅延バインドできます。

コレクションを返さなければならないケースの1つは、強い一貫性の要件があり、移動するターゲットの一貫したスナップショットを作成する必要がある場合です。次に、要素を変更しないコレクションに入れます。

そのため、ほとんどの場合、Streamが正しい答えであると言えます。より柔軟で、通常は不必要な実体化コストを課さず、必要に応じて簡単に選択のコレクションに変換できます。ただし、コレクションを返す必要がある場合があります(一貫性の要件が厳しいためなど)。または、ユーザーがどのように使用するかを知っており、これがユーザーにとって最も便利なことであるため、コレクションを返したい場合があります。

201
Brian Goetz

Brian Goetzの優れた答え に追加するポイントがいくつかあります。

「getter」スタイルのメソッド呼び出しからStreamを返すことは非常に一般的です。 Java 8 javadocの Stream usage page を参照し、Java.util.Stream以外のパッケージの「Streamsを返すメソッド...」を探してください。これらのメソッドは通常、複数の値または何かの集約を表すか、または含むことができるクラス上にあります。そのような場合、APIは通常、それらのコレクションまたは配列を返しました。ブライアンが答えで指摘したすべての理由から、ここにストリームを返すメソッドを追加することは非常に柔軟です。これらのクラスの多くは、コレクションまたは配列を返すメソッドを既に持っています。これは、クラスがStreams APIよりも前のことです。新しいAPIを設計していて、ストリームを返すメソッドを提供することが理にかなっている場合、コレクションを返すメソッドを追加する必要はないかもしれません。

ブライアンは、値をコレクションに「具体化」するコストに言及しました。この点を拡大するために、実際には2つのコストがあります。コレクションに値を保存するコスト(メモリの割り当てとコピー)と、そもそも値を作成するコストです。後者のコストは、多くの場合、Streamの遅延を求める動作を利用することで削減または回避できます。この良い例は、Java.nio.file.FilesのAPIです:

static Stream<String>  lines(path)
static List<String>    readAllLines(path)

readAllLinesは、結果リストに保存するためにファイルの内容全体をメモリに保持する必要があるだけでなく、リストを返す前に最後までファイルを読み取る必要もあります。 linesメソッドは、セットアップを実行した直後に、ファイルの読み取りと改行を必要なときまで、またはまったく必要としなくなるまで、すぐに返すことができます。たとえば、発信者が最初の10行のみに関心がある場合、これは大きな利点です。

try (Stream<String> lines = Files.lines(path)) {
    List<String> firstTen = lines.limit(10).collect(toList());
}

もちろん、呼び出し側がストリームをフィルタリングして、パターンなどに一致する行のみを返す場合、かなりのメモリ空間を節約できます。

出現しそうなイディオムは、getプレフィックスなしで、それが表すまたは含むものの名前の複数形の後にストリームを返すメソッドに名前を付けることです。また、返される値のセットが1つしかない場合、stream()はストリームを返すメソッドの適切な名前ですが、複数のタイプの値の集合を持つクラスがある場合もあります。たとえば、属性と要素の両方を含むオブジェクトがあるとします。 2つのストリームを返すAPIを提供できます。

Stream<Attribute>  attributes();
Stream<Element>    elements();
62
Stuart Marks

コレクションとは対照的に、ストリームには 追加の特性 があります。メソッドによって返されるストリームは次のとおりです。

  • 有限または 無限
  • パラレル またはシーケンシャル(アプリケーションの他の部分に影響を与える可能性があるデフォルトのグローバル共有スレッドプールを使用)
  • 順序付きまたは順序なし

これらの違いはコレクションにも存在しますが、そこには明らかな契約の一部があります。

  • すべてのコレクションにはサイズがあり、Iterator/Iterableは無限にすることができます。
  • コレクションは明示的に順序付けされているか、順序付けされていません
  • ありがたいことに、並列性はコレクションがスレッドの安全性を超えて気にするものではありません。

ストリームのコンシューマーとして(メソッドリターンから、またはメソッドパラメーターとして)、これは危険でわかりにくい状況です。アルゴリズムが正しく動作することを確認するために、ストリームのコンシューマーは、アルゴリズムがストリームの特性について間違った仮定をしないことを確認する必要があります。そして、それは非常に難しいことです。単体テストでは、すべてのテストを繰り返して同じストリームコンテンツで繰り返すが、次のストリームが必要になることを意味します。

  • (有限、順序、順次)
  • (有限、順序付け、並列)
  • (有限、順序なし、順次)...

ストリームのメソッドガードを記述する 入力ストリームにアルゴリズムを壊す特性がある場合、IllegalArgumentExceptionをスローするプロパティは非表示であるため、困難です。

上記の問題がほとんどない場合、Streamはメソッドシグネチャの有効な選択肢としてのみ残りますが、これはめったにありません。

明示的なコントラクト(および暗黙的なスレッドプール処理を伴わない)でメソッドシグネチャで他のデータ型を使用すると、順序、サイズ、または並列性(およびスレッドプールの使用)に関する誤った仮定でデータを誤って処理することができなくなります。

1
tkruse

ストリームは、作成された同じ表現内で常に「終了」するように設計されていましたか?

それがほとんどの例で使用されている方法です。

注:ストリームを返すことは、イテレータを返すこととそれほど違いはありません(より表現力豊かに認められています)

私見の最良の解決策は、あなたがこれをしている理由をカプセル化し、コレクションを返さないことです。

例えば.

public int playerCount();
public Player player(int n);

またはそれらを数えるつもりなら

public int countPlayersWho(Predicate<? super Player> test);
1
Peter Lawrey

ストリームが有限であり、チェックされた例外をスローする、返されたオブジェクトに予想される/通常の操作がある場合、常にコレクションを返します。なぜなら、チェック例外をスローする可能性のある各オブジェクトで何かをするつもりなら、ストリームを嫌うからです。ストリームの1つの本当の欠如は、チェックされた例外をエレガントに処理できないことです。

さて、おそらくそれはチェックされた例外を必要としない兆候です。これは公平ですが、時には避けられないこともあります。

1
designbygravity

おそらく、Streamファクトリーがより良い選択でしょう。ストリーム経由でのみコレクションを公開することの大きなメリットは、ドメインモデルのデータ構造をより適切にカプセル化できることです。ストリームを公開するだけでは、ドメインクラスを使用してリストまたはセットの内部動作に影響を与えることはできません。

また、ドメインクラスのユーザーに、より現代的なJava 8スタイルでコードを記述することを推奨します。既存のゲッターを保持し、新しいストリームを返すゲッターを追加することで、このスタイルに段階的にリファクタリングすることができます。時間が経つにつれて、リストまたはセットを返すすべてのゲッターを最終的に削除するまで、レガシーコードを書き換えることができます。この種のリファクタリングは、すべてのレガシコードを削除した後は本当にいい感じです!

0
Vazgen Torosyan

それはあなたのシナリオに依存すると思います。おそらく、TeamIterable<Player>に実装すれば十分です。

for (Player player : team) {
    System.out.println(player);
}

または機能的なスタイルで:

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

しかし、より完全で流なAPIが必要な場合は、ストリームが優れたソリューションになる可能性があります。

0
gontard