カウントで評価されない中間ストリーム操作


33

Javaがストリーム操作をストリームパイプラインに構成する方法を理解するのに苦労しているようです。

次のコードを実行すると

public
 static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

コンソールはのみを印刷し4ます。StringBuilderオブジェクトは、まだ価値があります""

フィルター操作を追加すると: filter(s -> true)

public static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .filter(s -> true)
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

出力は次のように変わります。

4
1234

この一見冗長なフィルター操作は、合成ストリームパイプラインの動作をどのように変更しますか?


2
面白い !!!
uneq95

3
これは実装固有の動作だと思います。おそらく、最初のストリームは既知のサイズを持っていますが、2番目のストリームはわかっていないためです。
アンディターナー

興味のないところで、フィルターとマップを逆にするとどうなりますか?
アンディターナー

Haskellで少しプログラミングしたので、ここで行われている遅延評価のように少しにおいがします。返されたグーグル検索は、確かにそのストリームにいくらかの遅延がある。そうかもしれませんか?フィルターがなければ、Javaが十分に賢い場合は、実際にマッピングを実行する必要はありません。
フレデリク

@AndyTurner逆転しても同じ結果になります
uneq95

回答:


39

count()端末操作は、JDKの私のバージョンでは、次のコードを実行して終わります。

if (StreamOpFlag.SIZED.isKnown(helper.getStreamAndOpFlags()))
    return spliterator.getExactSizeIfKnown();
return super.evaluateSequential(helper, spliterator);

存在する場合filter()の動作のパイプラインの動作は(以降、最初に知られているストリームのサイズは、もはや知ることができないfilterストリームのいくつかの要素を拒絶することができます)。したがって、ifブロックは実行されず、中間操作が実行され、StringBuilderが変更されます。

一方、map()パイプラインにのみ存在する場合、ストリーム内の要素の数は、要素の初期数と同じであることが保証されています。したがって、ifブロックが実行され、中間操作を評価せずにサイズが直接返されます。

渡されたラムダmap()は、ドキュメントで定義されている規約に違反していることに注意してください。これは、非干渉のステートレス操作であると想定されていますが、ステートレスではありません。したがって、両方のケースで結果が異なることはバグと見なすことはできません。


flatMap()要素の数を変更できる可能性があるので、それが最初に熱心だった(今は怠惰な)理由はそれでしたか?したがって、別の方法としては、現在の形式で契約に違反しているforEach()場合map()は、個別に使用してカウントすることでしょう。
フレデリク

3
flatMapについては、そうは思いません。それは、それを熱心にするための最初の方が簡単だったので、AFAIKでした。はい、map()でストリームを使用して副作用を生成することは悪い考えです。
JBニゼット

4 1234追加のフィルターを使用したり、map()操作で副作用を発生させたりせずに完全な出力を実現する方法についての提案はあります か?
atalantus

1
int count = array.length; String result = String.join("", array);
JBニゼット

1
またはあなたが本当にのStringBuilderを使用したい場合は、のforEachを使用することができ、またはあなたが使用することができますCollectors.joining("")
njzk2

19

ではJDK-9 、それは明らかにJavaのドキュメントに記載されていました

副作用を排除することも驚くべきことかもしれません。ターミナルの操作forEachおよびforEachOrderedを除いて、ストリームの実装が計算の結果に影響を与えずに動作パラメーターの実行を最適化できる場合、動作パラメーターの副作用が常に実行されるとは限りません。(特定の例については、countオペレーションに記載されているAPIノートを参照してください。)

APIノート:

実装は、ストリームソースから直接カウントを計算できる場合、ストリームパイプラインを(順次または並列に)実行しないことを選択できます。そのような場合、ソース要素はトラバースされず、中間操作は評価されません。副作用のある動作パラメーターは、デバッグなどの無害な場合を除いて強く非推奨ですが、影響を受ける可能性があります。たとえば、次のストリームを考えてみます。

 List<String> l = Arrays.asList("A", "B", "C", "D");
 long count = l.stream().peek(System.out::println).count();

ストリームソースによってカバーされる要素の数であるリストは既知であり、中間操作であるピークは、ストリームに要素を挿入したり、ストリームから要素を削除したりしません(flatMapまたはフィルタ操作の場合のように)。したがって、カウントはリストのサイズであり、パイプラインを実行して副作用としてリスト要素を出力する必要はありません。


0

これは.mapの目的ではありません。「Something」のストリームを「Something Else」のストリームに変換するために使用されることになっています。この場合、mapを使用して外部のStringbuilderに文字列を追加します。その後、「Stringbuilder」のストリームがあり、それぞれが元のStringbuilderに1つの数値を追加するmap操作によって作成されました。

ストリームは実際にはストリーム内のマッピングされた結果に対して何もしないので、ストリームプロセッサによってステップをスキップできると想定することは完全に合理的です。あなたは、マップの機能モデルを破壊する作業を行うための副作用に頼っています。これを行うためにforEachを使用することで、より良いサービスが提供されます。完全に別のストリームとしてカウントするか、forEachでAtomicIntを使用してカウンターを配置します。

フィルターは、各ストリーム要素で概念的に意味のある何かを実行する必要があるため、ストリームコンテンツを実行するように強制します。

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