私はJavaストリームについて読んでいて、新しいものを発見しています。私が見つけた新しいものの1つはpeek()
関数でした。ピークで読んだほぼすべてのものは、Streamsのデバッグに使用すべきだと言っています。
各アカウントにユーザー名、パスワードフィールド、login()およびloggingIn()メソッドがあるStreamがある場合はどうなりますか。
私も持っています
Consumer<Account> login = account -> account.login();
そして
Predicate<Account> loggedIn = account -> account.loggedIn();
なぜこれがそんなに悪いのでしょうか?
List<Account> accounts; //assume it's been setup
List<Account> loggedInAccount =
accounts.stream()
.peek(login)
.filter(loggedIn)
.collect(Collectors.toList());
今、私が知る限り、これは意図したとおりのことをしています。それ;
このようなことをすることの欠点は何ですか?続行しない理由は何ですか?最後に、このソリューションではない場合はどうしますか?
これの元のバージョンは、次のように.filter()メソッドを使用しました。
.filter(account -> {
account.login();
return account.loggedIn();
})
これからの重要なポイント:
当面の目標を達成したとしても、意図しない方法でAPIを使用しないでください。このアプローチは将来中断する可能性があり、将来のメンテナーにとっても不明確です。
これらは別個の操作であるため、これを複数の操作に分割しても害はありません。そこにisAPIを不明確で意図しない方法で使用すると害があります。これは、この特定の動作がJavaの将来のバージョンで変更された場合に影響を与える可能性があります。
この操作でforEach
を使用すると、メンテナーはaccounts
の各要素にintended副作用があることが明確になります。 、それを変更できる操作を実行していること。
また、peek
は中間操作であり、端末操作が実行されるまでコレクション全体では操作されませんが、forEach
は実際には端末操作です。この方法で、peek
がこのコンテキストでforEach
と同じように振る舞うかどうかについて質問するのではなく、動作とコードのフローについて強力な議論をすることができます。
accounts.forEach(a -> a.login());
List<Account> loggedInAccounts = accounts.stream()
.filter(Account::loggedIn)
.collect(Collectors.toList());
理解しなければならない重要なことは、ストリームは端末操作によって駆動されることです。端末操作は、すべての要素を処理する必要があるか、まったく処理する必要があるかを決定します。したがって、collect
は各アイテムを処理する操作ですが、findAny
は一致する要素に遭遇するとアイテムの処理を停止する場合があります。
また、count()
は、アイテムを処理せずにストリームのサイズを決定できる場合、要素をまったく処理しない場合があります。これはJava 8で行われるのではなく、Java 9で行われる最適化であるため、Java and9に切り替えて、すべてのアイテムを処理するcount()
に依存するコードを作成すると、驚くかもしれません。これは、他の実装依存の詳細にも接続されます。 Java 9でも、リファレンス実装はlimit
と組み合わせた無限ストリームソースのサイズを予測できませんが、そのような予測を妨げる基本的な制限はありません。
peek
は、「各要素で提供されたアクションを実行する要素が結果のストリームから消費されるので」を許可するため、強制されません要素を処理しますが、端末操作に必要なものに応じてアクションを実行します。これは、特定の処理が必要な場合、細心の注意を払って使用する必要があることを意味します。すべての要素にアクションを適用したい。端末操作がすべてのアイテムを処理することが保証されている場合は機能しますが、それでも次の開発者が端末操作を変更しないように注意する必要があります(またはその微妙な側面を忘れます)。
さらに、ストリームは、並列ストリームであっても特定の操作の組み合わせに対して遭遇順序を維持することを保証しますが、これらの保証はpeek
には適用されません。リストに収集する場合、結果のリストは順序付けられた並列ストリームの正しい順序になりますが、peek
アクションは任意の順序で同時に呼び出されます。
したがって、peek
を使用してできる最も便利なことは、ストリーム要素が処理されたかどうかを確認することです。これは、まさにAPIドキュメントに記載されているとおりです。
このメソッドは、主にデバッグをサポートするために存在します。ここでは、パイプラインの特定のポイントを通過する要素を表示します。
おそらく、「デバッグ」シナリオの外でピークを使用する場合は、終了および中間のフィルター条件が何であるかが確実な場合にのみ使用することをお勧めします。例えば:
return list.stream().map(foo->foo.getBar())
.peek(bar->bar.publish("HELLO"))
.collect(Collectors.toList());
1つの操作で、すべてのFoosをBarsに変換し、すべての人に挨拶するという、有効なケースのようです。
次のようなものよりも効率的でエレガントなようです:
List<Bar> bars = list.stream().map(foo->foo.getBar()).collect(Collectors.toList());
bars.forEach(bar->bar.publish("HELLO"));
return bars;
また、コレクションを2回繰り返すことはありません。
peek
は、すべてを詰め込む代わりに、ストリームオブジェクトを変更したり、グローバル状態を変更したりすることができるコードを分散する分散機能を提供します。シンプルまたは合成関数ターミナルメソッドに渡されます。
質問は次のようになります:機能スタイルJava programmingの関数内からストリームオブジェクトを変更するか、グローバル状態を変更する必要がありますか?
上記の2つの質問のいずれかに対する答えが「はい」(または、場合によっては「はい」)の場合、peek()
は、デバッグ目的だけでなくdefinitely、forEach()
がデバッグ目的だけではないのと同じ理由で。
forEach()
とpeek()
の間で選択するとき、次を選択していますか?ストリームオブジェクトを変更するコードの一部をコンポーザブルにアタッチするのか、それともストリームに直接アタッチするのか?
peek()
は、Java9のメソッドと組み合わせた方が良いと思います。例えばtakeWhile()
は、既に変更されたオブジェクトに基づいて反復を停止するタイミングを決定する必要がある場合があるため、forEach()
と比較しても同じ効果はありません。
P.S.新しいオブジェクトを生成するのではなく、オブジェクト(またはグローバル状態)を変更したい場合、map()
とまったく同じように機能するため、peek()
を参照していません。
上記のほとんどの答えには同意しますが、ピークを使用することが実際に最もクリーンな方法のように見える場合があります。
ユースケースと同様に、アクティブなアカウントでのみフィルタリングしてから、これらのアカウントでログインを実行するとします。
accounts.stream()
.filter(Account::isActive)
.peek(login)
.collect(Collectors.toList());
ピークは、コレクションを2回繰り返す必要がなく、冗長な呼び出しを回避するのに役立ちます。
accounts.stream()
.filter(Account::isActive)
.map(account -> {
account.login();
return account;
})
.collect(Collectors.toList());
機能的な解決策は、アカウントオブジェクトを不変にすることです。したがって、account.login()は新しいアカウントオブジェクトを返す必要があります。これは、マップ操作をピークの代わりにログインに使用できることを意味します。