Javaストリームでは、デバッグのみを目的としていますか?


137

私はJavaストリームについて読んでいて、進むにつれて新しいものを発見しています。私が見つけた新しいものの1つはpeek()機能でした。私がピークで読んだほとんどすべては、それをストリームのデバッグに使用する必要があると述べています。

各アカウントにユーザー名、パスワードフィールド、login()メソッドとlogsIn()メソッドが含まれるストリームがある場合はどうなりますか。

私も持っています

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();
    })

38
複数行のラムダが必要なときはいつでも、行をプライベートメソッドに移動して、ラムダの代わりにメソッド参照を渡します。
VGR 2015年

1
目的は何ですか-あなたはすべてのアカウントでログインしようとしている彼らは、ログインしている場合に基づいてフィルタにそれらを(自明真であるかもしれませんか)?それとも、あなたがそれらをログに記録したいです、そして、彼らはログインしてきたかどうかに基づいて、それらをフィルタリングしますか?これはこの順序でお願いforEachpeekます。なぜなら、あなたが望む操作ではないかもしれないからです。それがAPIにあるからといって、それが悪用に対して開かれていないという意味ではありません(などOptional.of)。
マコト

8
また、あなたのコードは単に.peek(Account::login)andである可能性があることに注意してください.filter(Account::loggedIn)。そのような別のメソッドを呼び出すだけのコンシューマーと述語を記述する理由はありません。
Joshua Taylor

2
また、ストリームAPI は、動作パラメータの副作用を明示的に阻止していることに注意してください
Didier L

6
有用な消費者には常に副作用がありますが、もちろんそれは推奨されません。これは実際には同じセクションに記載されている:「のようなストリーム操作の数が少ない、forEach()およびpeek()、副作用を介してのみ動作することができます。これらは注意して使用する必要があります。」。私の発言は、peek(デバッグの目的で設計された)操作を、map()またはのような別の操作内で同じことを実行することで置き換えるべきではないことを思い出させることfilter()です。
Didier L

回答:


77

これからの重要なポイント:

たとえあなたの当面の目標を達成したとしても、意図しない方法でAPIを使用しないでください。このアプローチは将来破綻する可能性があり、将来のメンテナにとっても不明確です。


これらは別個の操作であるため、これを複数の操作に分割しても害はありません。そこ、この特定の動作は、Javaの将来のバージョンで変更された場合波及効果を有することが不明確と意図しない方法でAPIを使用して害は、。

forEachこの操作で使用すると、の各要素に意図した副作用があり、accountsそれを変更できる操作を実行していることがメンテナに明らかになります。

またpeek、ターミナル操作が実行されるまでコレクション全体を操作しない中間操作であるという意味で、より一般的ですが、forEach実際にはターミナル操作です。この方法では、このコンテキストでのpeek動作と同じように動作するかどうかについて質問するのではなく、コードの動作とフローに関して強力な議論をすることができforEachます。

accounts.forEach(a -> a.login());
List<Account> loggedInAccounts = accounts.stream()
                                         .filter(Account::loggedIn)
                                         .collect(Collectors.toList());

3
前処理ステップでログインを実行する場合、ストリームはまったく必要ありません。あなたは実行できるforEachソースコレクションで右:accounts.forEach(a -> a.login());
ホルガー

1
@ホルガー:素晴らしい点。私はそれを答えに組み込んだ。
Makoto

2
@ Adam.J:そうです、私の答えは、タイトルに含まれている一般的な質問に焦点を合わせたものです。つまり、この方法は、デバッグの目的のみであり、その方法の側面を説明することです。この答えは、実際のユースケースと、代わりにそれを行う方法にさらに融合しています。ですから、一緒に全体像を提供することができます。1つ目は、これが意図された用途ではない理由、2つ目は、意図しない使用法に固執しないで、代わりに何をすべきかという結論です。後者の方が実用的です。
Holger

2
もちろん、login()メソッドbooleanが成功ステータスを示す値を返した方がはるかに簡単でした…
Holger

3
それが私が目指していたものです。がをlogin()返す場合booleanは、最もクリーンなソリューションである述語として使用できます。それはまだ副作用があり、それは、それが非干渉であると大丈夫だ、つまりlogin1のprocess`はAccount別のlogin`のprocess`に影響を与えませんAccount
Holger

111

理解しておくべき重要なことは、ストリームは端末操作によって駆動されるということです。端末操作は、すべての要素を処理する必要があるか、それともまったく処理する必要があるかを決定します。そうcollect各項目を処理する動作は、一方でfindAnyそれは整合素子に遭遇後の項目の処理を停止してもよいです。

またcount()、アイテムを処理せずにストリームのサイズを決定できる場合は、要素をまったく処理しない可能性があります。これはJava 8では行われなかった最適化ですが、Java 9で行われるため、Java 9に切り替えて、count()すべてのアイテムの処理に依存するコードを作成すると、驚くかもしれません。これは、他の実装に依存する詳細にも関連付けられます。たとえば、Java 9の場合でも、リファレンス実装は、無限ストリームソースのサイズを予測できませんが、limitそのような予測を妨げる基本的な制限はありません。

peekは「要素が結果のストリームから消費されるときに各要素に提供されたアクションを実行する」ことを許可するので、要素の処理を強制するのではなく、端末操作の必要に応じてアクションを実行します。これは、特定の処理が必要な場合(たとえば、すべての要素にアクションを適用したい場合)は、慎重に使用する必要があることを意味します。端末操作がすべてのアイテムを処理することが保証されている場合に機能しますが、それでも、次の開発者が端末操作を変更しないようにする必要があります(またはその微妙な側面を忘れます)。

さらに、ストリームは、並列ストリームの場合でも、特定の組み合わせの操作の遭遇順序を維持することを保証しますが、これらの保証はには適用されませんpeek。リストに収集するとき、結果のリストは順序付けられた並列ストリームの正しい順序にpeekなりますが、アクションは任意の順序で同時に呼び出される可能性があります。

したがって、あなたができる最も有用なことはpeek、ストリーム要素が処理されているかどうかを調べることです。これは、APIドキュメントが正確に言っているとおりです。

このメソッドは、主にデバッグをサポートするために存在し、パイプラインの特定のポイントを通過するときに要素を表示する必要があります。


OPのユースケースで、将来または現在の問題はありますか?彼のコードは常に彼が望むことをしますか?
ZhongYu、2015年

9
@ bayou.io:私が見る限り、この正確な形式で問題はありません。しかし、私はこの方法は...あなたはコードに«機能要求9876を»組み込むために1年または2年後のコードに戻ってきた場合でも、あなたは、この点を覚えている意味、それを使用して、説明しようとしたとして
ホルガー

1
「ピークアクションは任意の順序で同時に呼び出される可能性があります」。このステートメントは、「要素が消費されるとき」などのピークの動作に関するルールに反しませんか?
Jose Martinez

5
@Jose Martinez:「結果のストリームから要素が消費されるとき」と表示されます。これは最終アクションではなく処理ですが、最終アクションが最終結果に一貫性がある限り、順序どおりに要素を消費することはできません。しかし、私は、API パイプラインの要素がパイプラインの特定のポイントを過ぎて流れるのを見る」というフレーズも、それを説明するのに優れていると思います。
Holger 2017

23

おそらく、「経験則」は、「デバッグ」シナリオの外でピークを使用する場合、終了および中間のフィルタリング条件が何であるかを確信している場合にのみ使用するべきであるということです。例えば:

return list.stream().map(foo->foo.getBar())
                    .peek(bar->bar.publish("HELLO"))
                    .collect(Collectors.toList());

すべてのFoosをBarsに変換し、すべてのHelloに伝える1つの操作で、あなたが望む有効なケースのようです。

次のようなものよりも効率的でエレガントなようです:

List<Bar> bars = list.stream().map(foo->foo.getBar()).collect(Collectors.toList());
bars.forEach(bar->bar.publish("HELLO"));
return bars;

そして、コレクションを2回繰り返すことにはなりません。


4

端末メソッドに渡される単純な関数または構成された関数にすべてを詰め込む代わりに、ストリームオブジェクトを変更したり、グローバル状態を(それらに基づいて)変更したりできるコードpeek分散する機能を提供すると思います。

質問は次のようになるでしょう:関数型のJavaプログラミングの関数内からストリームオブジェクトを変更するか、グローバル状態を変更する必要がありますか?

2上記の質問のいずれかに対する答えはイエス(または:いくつかのケースでYES)であれば、peek()あるデバッグ目的だけではなく、間違いなく同じ理由でforEach()デバッグ目的だけではありません

forEach()との間で選択するとき、私peek()は次を選択しています:ストリームオブジェクトを変更するコードをコンポーザブルにアタッチするか、ストリームに直接アタッチするか?

私が考えるpeek()java9方法でペアをより良くします。たとえばtakeWhile()、すでに変更されたオブジェクトに基づいて反復を停止するタイミングを決定する必要がある場合があるため、それとの比較でforEach()は同じ効果はありません。

PSmap()新しいオブジェクトを生成するのではなく、オブジェクト(またはグローバル状態)を変更したい場合は、まったく同じように動作するため、どこにも参照していませんpeek()


3

上記のほとんどの回答に同意しますが、peekを使用することが実際に最もクリーンな方法のように思われる1つのケースがあります。

ユースケースと同様に、アクティブなアカウントのみをフィルタリングして、これらのアカウントでログインを実行するとします。

accounts.stream()
    .filter(Account::isActive)
    .peek(login)
    .collect(Collectors.toList());

Peekは、コレクションを2回繰り返す必要がない一方で、冗長な呼び出しを回避するのに役立ちます。

accounts.stream()
    .filter(Account::isActive)
    .map(account -> {
        account.login();
        return account;
    })
    .collect(Collectors.toList());

3
あなたがしなければならないすべてはそのログイン方法を正しくすることです。ピークが最もクリーンな方法であることは本当にわかりません。コードを読んでいる誰かが、実際にAPIを誤用していることをどのように知る必要がありますか。適切でクリーンなコードは、読者にコードについての仮定を強制するものではありません。
kaba713 2018

1

機能的な解決策は、アカウントオブジェクトを不変にすることです。そのため、account.login()は新しいアカウントオブジェクトを返す必要があります。これは、プレビューの代わりにマップ操作をログインに使用できることを意味します。

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