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


163

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

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

    // ...

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

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

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

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

または、Javaで非慣用的なストリームを返していますか?ストリームは、それが作成された同じ式内で常に「終了」するように設計されていましたか?


12
これは慣用句として間違いなく間違いはありません。結局のところ、players.stream()呼び出し元にストリームを返すようなメソッドです。本当の問題は、呼び出し元を単一のトラバーサルに制限し、CollectionAPIを介してコレクションへのアクセスを拒否したいですか?たぶん、発信者はaddAll別のコレクションにそれを望んでいるか?
Marko Topolnik 14

2
それはすべて異なります。常にcollection.stream()およびStream.collect()を実行できます。したがって、その関数を使用するのは、あなたと呼び出し元です。
Raja Anbazhagan 2017

回答:


222

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

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

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

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

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

結果が無限になる可能性がある場合、選択肢はストリームのみです。

結果が非常に大きい場合は、Streamを使用することをお勧めします。これは、一度に実体化することに価値がなく、そうすることで大きなヒープ圧力が発生する可能性があるためです。

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

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

上記の「Streamを優先する」ケースは、ほとんどの場合、Streamの方が柔軟性があるという事実から派生しています。コレクションへの具体化のコストや制約を負うことなく、それを使用方法に後からバインドできます。

コレクションを返す必要があるのは、強い整合性要件があり、移動するターゲットの整合性のあるスナップショットを作成する必要がある場合です。次に、要素を変更されないコレクションに配置します。

したがって、ほとんどの場合、Streamが正解です。より柔軟であり、通常は不要な具体化コストを課すことはなく、必要に応じて選択したコレクションに簡単に変換できます。しかし、場合によっては、コレクションを返す必要があります(たとえば、強い整合性の要件のため)、またはユーザーがコレクションを使用する方法を知っていて、これがユーザーにとって最も便利であることを知っているので、コレクションを返したい場合があります。


6
先ほど述べたように、特にターゲットの整合性要件が強い場合に、ターゲットが移動したときにスナップショットを返したい場合など、飛行しないケースがいくつかあります。しかし、ほとんどの場合、Streamは、それがどのように使用されるかについて特定のことを知らない限り、より一般的な選択のように見えます。
Brian Goetz 2014

8
@Marko質問の範囲を狭めても、結論には同意しません。おそらく、ストリームを作成することは、コレクションを不変のラッパーでラップするよりも、どういうわけかはるかに高価であると想定しているのでしょうか?(そうしない場合でも、ラッパーで取得するストリームビューは、元のストリームビューで取得するものよりも劣ります。UnmodifiableListはspliterator()をオーバーライドしないため、実質的にすべての並列性が失われます。)結論:注意親しみバイアスの; あなたは何年もの間コレクションを知っていて、それはあなたが新人を信用しないようにするかもしれません。
Brian Goetz 14

5
@MarkoTopolnikもちろん。私の目標は、FAQになりつつある一般的なAPI設計の質問に対処することでした。あなたがいる場合は、そのコスト、ノートに関してはまだありません(OPはありませんが、多くの場合、1つが存在しない)マテリアライズドあなたが返すことができ、コレクションやラップを持って、getterメソッドでコレクションをマテリアライズすると、ストリームとさせるを返すより任意の安価ではありません呼び出し元は1つを具体化します(もちろん、呼び出し元がそれを必要としない場合、またはArrayListを返したが呼び出し元がTreeSetを必要とする場合、初期の具体化ははるかに高価になる可能性があります)。そうです。
Brian Goetz 2014

4
@MarkoTopolnikインメモリは非常に重要なユースケースですが、順序付けなしで生成されたストリーム(Stream.generateなど)など、並列化が適切にサポートされているケースもいくつかあります。ただし、Streamsの適合性が低いのは、データがランダムなレイテンシで到着する事後の使用例です。そのため、RxJavaをお勧めします。
Brian Goetz 2014

4
@MarkoTopolnik私たちが意見を異にすることはないと思いますが、あなたが私たちの取り組みに少しずつ焦点を合わせるのを好んでいたのではないかと思います。(私たちはこれに慣れています。すべての人々を幸せにすることはできません。)Streamsのデザインセンターは、メモリ内データ構造に焦点を当てています。RxJavaのデザインセンターは、外部で生成されたイベントに焦点を当てています。どちらも優れたライブラリです。また、デザインセンター以外のケースに適用しようとすると、どちらもうまく機能しません。しかし、ハンマーが針先のひどい道具であるという理由だけで、それはハンマーに何か問題があることを示唆していません。
Brian Goetz 2014

63

ブライアンゲッツのすばらしい答えに付け加えたい点がいくつかあります

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

ブライアンは、値をコレクションに「具体化」するコストについて言及しました。この点を強調するために、実際には2つのコストがあります。コレクションに値を保存するコスト(メモリの割り当てとコピー)と、最初に値を作成するコストです。後者のコストは、ストリームの遅延探索動作を利用することで、削減または回避できることがよくあります。この良い例は、次のAPIですjava.nio.file.Files

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

Stream<Attribute>  attributes();
Stream<Element>    elements();

3
素晴らしい点。そのネーミングイディオムがどこで発生しているのか、そしてそれがどれだけの牽引力(スチーム)を獲得しているのかについて詳しく説明できますか?コレクションと比べてストリームを取得していることを明確にする命名規則のアイデアが気に入っています。ただし、「get」でIDEが完了すると、何が得られるかがわかります。
Joshua Goldberg

1
私はそのネーミングイディオムにも非常に興味があります。
選出

5
@JoshuaGoldberg JDKは、この命名法を採用しているようですが、これだけではありません。検討してください:CharSequence.chars()と.codePoints()、BufferedReader.lines()、およびFiles.lines()はJava 8に存在しました。Java9では、以下が追加されました:Process.children()、NetworkInterface.addresses( )、Scanner.tokens()、Matcher.results()、java.xml.catalog.Catalog.catalogs()。このイディオムを使用しない他のストリームを返すメソッドが追加されました-Scanner.findAll()が思い浮かびます-しかし、複数名詞イディオムはJDKで公正に使用されているようです。
スチュアートマークス

1

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

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

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

私見の最善の解決策は、なぜこれを行うのかをカプセル化し、コレクションを返さないことです。

例えば

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

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

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

2
この回答の問題は、作成者がクライアントが実行したいすべてのアクションを予測する必要があり、クラスのメソッドの数が大幅に増えることです。
dkatzel 2014

@dkatzelエンドユーザーが作成者であるか、それとも共同で作業する人であるかによって異なります。エンドユーザーが認識できない場合は、より一般的なソリューションが必要です。基になるコレクションへのアクセスを制限することもできます。
Peter Lawrey、2014

1

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

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


1

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

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

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

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

ストリームのコンシューマーとして(メソッドの戻りから、またはメソッドのパラメーターとして)、これは危険で混乱を招く状況です。アルゴリズムが正しく動作することを確認するために、ストリームの利用者は、アルゴリズムがストリームの特性について誤った仮定をしないことを確認する必要があります。そしてそれは非常に難しいことです。ユニットテストでは、同じストリームコンテンツで繰り返されるすべてのテストを乗算する必要がありますが、

  • (有限、順序付け、順次)
  • (有限、順序付け、並列)
  • (有限、順不同、順次)...

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

これにより、上記の問題がどれも問題にならない場合にのみ、Streamがメソッドシグネチャの有効な選択肢として残ります。

順序付け、サイズ、または並列性(およびスレッドプールの使用法)について誤った仮定でデータを誤って処理することを不可能にする明示的なコントラクト(および暗黙のスレッドプール処理なし)を持つメソッドシグネチャで他のデータ型を使用する方がはるかに安全です。


2
無限ストリームに関するあなたの懸念は根拠のないものです。問題は、「コレクションまたはストリームを返すべきか」です。コレクションが可能である場合、結果は定義上有限です。したがって、コレクションを返すことができたとしても、呼び出し側が無限の反復を行う危険性があるという心配は根拠がありません。この回答の残りのアドバイスは単に悪いです。ストリームを使いすぎている誰かに出会い、反対方向に回転しすぎているように思えます。理解できるが悪いアドバイス。
Brian Goetz

0

シナリオ次第だと思います。多分、あなたがあなたのTeam道具を作るならばIterable<Player>、それは十分です。

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

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

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

しかし、より完全で流暢なAPIが必要な場合は、ストリームが良い解決策になる可能性があります。


OPが投稿したコードでは、プレーヤーの数は推定値以外はほとんど役に立たないことに注意してください(「1034人のプレーヤーが今プレイしています。ここをクリックして開始してください!」)これは、変更可能なコレクションの不変のビューを返すためです。なので、今取得するカウントは、3マイクロ秒後のカウントとは異なる場合があります。そのため、コレクションを返すと、カウントに「簡単に」アクセスできます(実際、これstream.count()も非常に簡単です)が、その数はデバッグや推定以外にはあまり意味がありません。
Brian Goetz 2014

0

注目度の高い回答者の一部が優れた一般的なアドバイスを提供しましたが、誰も完全に述べていないことに驚きます:

すでに「マテリアライズ」さCollectionれている場合(つまり、呼び出しの前に既に作成されている場合-メンバーフィールドである上記の例のように)、これをに変換しても意味がありませんStream。発信者はそれを自分で簡単に行うことができます。一方、呼び出し元がデータを元の形式で使用したい場合は、データをaに変換してStream、元の構造のコピーを再実体化するための冗長な作業を強制します。


-1

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

また、ドメインクラスのユーザーに、よりモダンなJava 8スタイルでコードを書くことを推奨します。既存のゲッターを維持し、新しいストリームを返すゲッターを追加することで、このスタイルに段階的にリファクタリングすることが可能です。リストまたはセットを返すすべてのゲッターを最終的に削除するまで、時間をかけてレガシーコードを書き直すことができます。この種のリファクタリングは、すべてのレガシーコードをクリアすると本当に気持ちがいいものです。


7
これが完全に引用されている理由はありますか?ソースはありますか?
Xerus

-5

おそらく2つのメソッドがあり、1つはを返し、Collectionもう1つはコレクションをとして返しStreamます。

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

// ...

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

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

}

これは両方の長所です。クライアントは、リストとストリームのどちらを必要とするかを選択でき、ストリームを取得するためだけにリストの不変のコピーを作成する追加のオブジェクト作成を行う必要はありません。

これにより、APIにメソッドが1つだけ追加されるため、メソッドが多すぎない


1
なぜなら、彼はこれら2つのオプションのどちらかを選択することを望み、それぞれの長所と短所を尋ねました。さらに、これらの概念についての理解を深めることができます。
Libert Piou Piou 2014

しないでください。APIを想像してみてください。
フランソワ・ゴーティエ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.