Java 8では、メソッド参照式や、関数型インターフェースの実装を返すメソッドを使用するほうが文法的に優れているでしょうか?


11

Java 8では、関数型インターフェイスの概念と、関数型インターフェイスを取るように設計された多数の新しいメソッドが追加されました。これらのインターフェースのインスタンスは、メソッド参照式(例SomeClass::someMethod)とラムダ式(例(x, y) -> x + y)を使用して簡潔に作成できます。

同僚と私は、どちらのフォームを使用するのが最適かについて意見が異なります(この場合、「最適」は、「最も読みやすい」と「標準的な慣行に最も近い」に要約されます。同等)。具体的には、次のすべてが当てはまる場合です。

  • 問題の関数は単一のスコープの外では使用されません
  • インスタンスに名前を付けると、読みやすさが向上します(たとえば、ロジックが何が起こっているかを一目で確認できるほど単純であるのとは対照的)
  • ある形式が他の形式よりも好まれるプログラミング上の理由は他にはありません。

この問題についての私の現在の意見は、プライベートメソッドを追加し、それをメソッド参照で参照することが、より優れたアプローチであるというものです。これはこの機能がどのように使用されるように設計されているかのように感じられ、メソッド名とシグネチャを介して何が起こっているかを伝える方が簡単に思われます(たとえば、「boolean isResultInFuture(Result result)」は明らかにブール値を返すと言っています)。また、クラスの将来の拡張で同じチェックを利用したい場合に、プライベートメソッドを再利用しやすくしますが、機能インターフェイスラッパーは必要ありません。

私の同僚の好みは、インターフェイスのインスタンスを返すメソッドを用意することです(たとえば、 "Predicate resultInFuture()")。私には、この機能の使用方法が完全ではないように感じられ、少し不格好に感じられ、名前を付けることで意図を実際に伝えるのが難しいように思われます。

この例を具体的にするために、異なるスタイルで記述された同じコードを次に示します。

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    results.filter(this::isResultInFuture).forEach({ result ->
      // Do something important with each future result line
    });
  }

  private boolean isResultInFuture(Result result) {
    someOtherService.getResultDateFromDatabase(result).after(new Date());
  }
}

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    results.filter(resultInFuture()).forEach({ result ->
      // Do something important with each future result line
    });
  }

  private Predicate<Result> resultInFuture() {
    return result -> someOtherService.getResultDateFromDatabase(result).after(new Date());
  }
}

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    Predicate<Result> resultInFuture = result -> someOtherService.getResultDateFromDatabase(result).after(new Date());

    results.filter(resultInFuture).forEach({ result ->
      // Do something important with each future result line
    });
  }
}

1つのアプローチの方が望ましいか、言語設計者の意図に沿ったものか、または読みやすいかについて、公式または準公式のドキュメントまたはコメントはありますか?公式の情報源を除いて、1つがより良いアプローチになる理由が明確にありますか?


1
ラムダは理由のために言語に追加されました、そしてそれはJavaを悪化させることではありませんでした。

@スノーマンそれは言うまでもありません。したがって、私はあなたのコメントと私の質問の間には、私が完全には理解していない暗黙の関係があると思います。あなたがしようとしている点は何ですか?
M.ジャスティン

2
彼が言っていることは、ラムダが現状を改善していなければ、彼らがそれらを導入することに煩わされなかったであろうということだと思います。
Robert Harvey

2
@RobertHarveyもちろん。ただし、その時点で、ラムダとメソッド参照の両方が同時に追加されたため、どちらも以前の状態ではありませんでした。この特定のケースがなくても、メソッド参照がより良い場合があります。たとえば、既存のパブリックメソッド(例:)Object::toString。したがって、私の質問は、この特定のタイプのインスタンスの方が、ここでレイアウトしている方が、一方が他方より優れているインスタンスが存在するのか、またはその逆であるのかよりも重要です。
M.ジャスティン

回答:


8

関数型プログラミングの観点から、あなたとあなたの同僚が話し合っているのは、ポイントフリースタイル、より具体的にはeta削減です。次の2つの割り当てを検討してください。

Predicate<Result> pred = result -> this.isResultInFuture(result);
Predicate<Result> pred = this::isResultInFuture;

これらは操作上同等であり、1 つ目はポイントフルスタイルと呼ばれ、2つ目はポイントフリースタイルと呼ばれます。「ポイント」という用語は、後者の場合には欠落している名前付き関数の引数(これはトポロジーに由来する)を指します。

より一般的には、すべての関数Fについて、与えられたすべての引数を取り、それらを変更せずにFに渡し、Fの結果を返すラッパーラムダはF自体と同じです。ラムダを削除してFを直接使用する(上記のポイントフルスタイルからポイントフリースタイルに移行する)は、η縮約と呼ばれ、ラムダ計算に由来する用語です。

eta削減によって作成されないポイントフリーの式があり、その最も典型的な例は関数構成です。タイプAからタイプBへの関数とタイプBからタイプCへの関数が与えられると、それらをタイプAからタイプCへの関数にまとめることができます。

public static Function<A, C> compose(Function<A, B> first, Function<B, C> second) {
  return (value) -> second(first(value));
}

次に、メソッドがあるとしますResult deriveResult(Foo foo)。述語は関数なので、最初に呼び出しderiveResult、次に派生結果をテストする新しい述語を作成できます。

Predicate<Foo> isFoosResultInFuture =
    compose(this::deriveResult, this::isResultInFuture);

が、実装composeラムダと用途好い加減なスタイル、使用を定義するコンのは、isFoosResultInFuture引数が言及する必要はないので、ポイントは無料です。

ポイントフリープログラミングは暗黙的プログラミングとも呼ばれ、関数定義から無意味な(意図されていない)詳細を削除することにより、読みやすさを向上させる強力なツールになります。Java 8は、Haskellのような関数型言語ほど完全にポイントフリープログラミングをサポートしていませんが、経験則として、常にeta-reductionを実行することをお勧めします。ラップする関数と動作が異なるラムダを使用する必要はありません。


1
:私は何を私の同僚と私は議論されているより多くのこれらの二つの割り当てだと思いますPredicate<Result> pred = this::isResultInFuture;し、Predicate<Result> pred = resultInFuture();どこresultInFuture()が返されますPredicate<Result>。したがって、これはメソッド参照とラムダ式をいつ使用するかについての優れた説明ですが、この3番目のケースは完全にはカバーしていません。
M.ジャスティン

4
@ M.Justin:resultInFuture();割り当ては、私の場合はresultInFuture()メソッドがインライン化されていることを除いて、私が提示したラムダ割り当てと同じです。したがって、そのアプローチには、2つの間接層(どちらも何もしない)があります。ラムダ構築メソッドとラムダ自体です。さらに悪い!
ジャック

5

現在の回答はどちらも、クラスがprivate boolean isResultInFuture(Result result)メソッドを持っているべきか、メソッドを持っているべきかという質問の核心を実際に扱っていませんprivate Predicate<Result> resultInFuture()。それらはほぼ同等ですが、それぞれに長所と短所があります。

最初のアプローチは、メソッド参照を作成することに加えて、メソッドを直接呼び出す場合により自然です。たとえば、このメソッドを単体テストする場合、最初のアプローチを使用する方が簡単かもしれません。最初のアプローチのもう1つの利点は、メソッドがその使用方法を気にする必要がなく、呼び出しサイトから切り離されることです。後でクラスを変更してメソッドを直接呼び出す場合、この分離は非常に小さな効果をもたらします。

最初のアプローチは、このメソッドをさまざまな機能インターフェイスまたはインターフェイスのさまざまなユニオンに適応させたい場合にも優れています。たとえば、メソッド参照をタイプPredicate<Result> & Serializableにしたい場合、2番目のアプローチでは柔軟性を実現できません。

一方、述語でより高次の演算を実行する場合は、2番目の方法がより自然です。述語には、メソッド参照で直接呼び出すことができないいくつかのメソッドがあります。これらの方法を使用する場合は、2番目の方法が優れています。

最終的に、決定は主観的です。ただし、Predicateでメソッドを呼び出すつもりがない場合は、最初のアプローチを優先する傾向があります。


述語の高次の操作は、メソッドの参照をキャストして、まだ用意されています((Predicate<Resutl>) this::isResultInFuture).whatever(...)
ジャック

4

私はJavaコードをもう書いていませんが、生活のために関数型言語で書き、関数型プログラミングを学んでいる他のチームもサポートしています。ラムダには読みやすさの微妙なスイートスポットがあります。それらの一般的に受け入れられているスタイルは、インライン化できるときに使用することですが、後で使用するために変数に割り当てる必要があると感じた場合は、おそらくメソッドを定義するだけです。

はい、人々は常にチュートリアルで変数にラムダを割り当てます。これらは、それらがどのように機能するかを完全に理解するのに役立つチュートリアルにすぎません。比較的まれな状況(if式を使用して2つのラムダから選択するなど)を除いて、実際にはそのようには使用されません。

つまり、@ JimmyJamesのコメントの例のように、インライン化した後に簡単に読みやすいようにラムダが短い場合は、次のようにします。

people = sort(people, (a,b) -> b.age() - a.age());

これを、ラムダの例をインライン化しようとしたときの読みやすさと比較してください。

results.filter(result -> someOtherService.getResultDateFromDatabase(result).after(new Date()))...

あなたの特定の例では、プルリクエストで個人的にフラグを立て、その長さのために名前付きメソッドにプルするようにお願いします。Javaは厄介で冗長な言語であるため、おそらく独自の言語固有のプラクティスは他の言語とは異なるでしょうが、それまではラムダを短くインラインにしてください。


2
再:冗長性-新しい機能の追加はそうではありませんが、それ自体は多くの冗長性を減らしますが、そうする方法を可能にします。たとえば、次のように定義できます。public static final <T> List<T> sort(List<T> list, Comparator<T> comparator)明らかな実装を使用して、次のようなコードを記述できます。 people = sort(people, (a,b) -> b.age() - a.age());これは、IMOの大きな改善点です。
JimmyJames

これは、私が話していた@JimmyJamesの良い例です。ラムダが短く、インライン化できる場合、それらは大きな改善です。それらが長くなりすぎてそれらを切り離して名前を付けたい場合は、メソッドを定義するだけのほうがよいでしょう。私の答えがどのように誤解されたかを理解してくれてありがとう。
Karl Bielefeldt

あなたの答えは大丈夫だったと思います。なぜそれが否決されるのかわからない。
JimmyJames
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.