ラムダ式には、コード行を保存する以外の用途がありますか?


120

ラムダ式には、コード行を保存する以外の用途がありますか?

簡単に解決できない問題を解決するラムダの特別な機能はありますか?私が見た典型的な使用法はこれを書く代わりに:

Comparator<Developer> byName = new Comparator<Developer>() {
  @Override
  public int compare(Developer o1, Developer o2) {
    return o1.getName().compareTo(o2.getName());
  }
};

ラムダ式を使用してコードを短縮できます。

Comparator<Developer> byName =
(Developer o1, Developer o2) -> o1.getName().compareTo(o2.getName());

27
さらに短くすることもできます。 (o1, o2) -> o1.getName().compareTo(o2.getName())
ThorbjørnRavn Andersen 2017

88
またはさらに短い:Comparator.comparing(Developer::getName)
ThomasKläger2017年

31
そのメリットを過小評価しないでください。物事がはるかに簡単な場合、物事を「正しい」方法で行う可能性が高くなります。多くの場合、コードを書くことには、長期的には保守性が高くなるのに対して、短期的には書き留めやすくなるという緊張があります。ラムダはその緊張を和らげるので、よりきれいなコードを書くのに役立ちます。
yshavit 2017年

5
ラムダはクロージャーであり、パラメーター化されたコンストラクター、フィールド、割り当てコードなどを置換クラスに追加する必要がないため、スペースの節約がさらに大きくなります。
CodesInChaos 2017年

回答:


116

ラムダ式は、Javaで一般的に解決できる一連の問題を変更しませんが、特定の問題の解決を確実に容易にします。これは、私たちがアセンブリ言語でプログラミングしていないのと同じ理由からです。プログラマーの作業から冗長なタスクを削除することで、作業が楽になり、(手動で)作成する必要があるコードの量だけのために、他の方法では触れられないことも実行できます。

しかし、ラムダ式はコード行を節約するだけではありません。ラムダ式を使用すると、以前に回避策として匿名の内部クラスを使用できた関数を定義できます。そのため、これらのケースでは匿名の内部クラスを置き換えることができますが、一般的にはできません。

特に、ラムダ式は、変換先の機能インターフェイスとは独立して定義されているため、アクセスできる継承されたメンバーはなく、機能インターフェイスを実装する型のインスタンスにはアクセスできません。ラムダ式の中で、thisそしてsuper周囲の文脈におけるものと同じ意味を持ち、また参照のこの答えを。また、周囲のコンテキストのローカル変数をシャドウする新しいローカル変数を作成することもできません。関数を定義するという意図されたタスクでは、これにより多くのエラーソースが削除されますが、他のユースケースでは、関数型インターフェイスを実装していてもラムダ式に変換できない匿名の内部クラスが存在する可能性があることも意味します。

さらに、構成 new Type() { … }newは、常に新しいインスタンスを生成することを保証します。匿名の内部クラスインスタンスは、非staticコンテキストで作成された場合、常に外部インスタンスへの参照を保持します。これとは対照的に、ラムダ式は唯一の参照キャプチャthis、彼らがアクセスした場合、すなわち、必要なときにthisまたは非staticメンバー。そして、意図的に指定されていないIDのインスタンスを生成します。これにより、実装は既存のインスタンスを再利用するかどうかを実行時に決定できます(「ラムダ式は、実行されるたびにヒープ上にオブジェクトを作成しますか?」も参照してください)。

これらの違いは、例に適用されます。匿名の内部クラスコンストラクトは常に新しいインスタンスを生成しますが、外部インスタンスへの参照をキャプチャすることもありますが、(Developer o1, Developer o2) -> o1.getName().compareTo(o2.getName())が、通常の実装ではシングルトンに評価される非キャプチャラムダ式です。さらに、.classハードドライブにファイルを作成しません。

セマンティックとパフォーマンスの両方の違いを考えると、ラムダ式は、もちろん、新しいAPIが新しい言語機能を利用した関数型プログラミングのアイデアを取り入れているため、将来の特定の問題の解決方法を変える可能性があります。Java 8のラムダ式とファーストクラスの値もご覧ください。


1
基本的に、ラムダ式は関数型プログラミングの一種です。関数型プログラミングまたは命令型プログラミングを使用して任意のタスクを実行できる場合、他の懸念を上回る可能性のある可読性と保守性以外の利点はありません。
Draco18sは、

1
@Trilarion:機能の定義で本全体を埋めることができます。また、目標に焦点を合わせるか、Javaのラムダ式によってサポートされる側面に焦点を合わせるかを決定する必要があります。私の回答の最後のリンク(これ)は、Javaのコンテキストでこれらの側面のいくつかについてすでに説明しています。しかし、私の答えを理解するには、関数はクラスとは異なる概念であることを理解するだけで十分です。詳細については、既存のリソースとのQ&Aは...ある
ホルガー

1
@Trilarion:両方とも、他のソリューションが必要とするコードサイズ/複雑さまたは回避策の量を(最初の段落で述べたように)許容できないものと見なし、関数を定義する方法がすでにない限り、関数はこれ以上の問題を解決できません言ったように、内部クラスは、関数を定義するためのより良い方法がないための回避策として使用できます(ただし、内部クラスは、はるかに多くの目的に役立つため、関数ではありません)。これは、OOPと同じです。アセンブリではできなかったことができませんが、それでも、アセンブリで記述されたEclipseなどのアプリケーションをイメージ化できますか?
Holger

3
@EricDuminil:私にとって、チューリング完全性はあまりにも理論的です。たとえば、Javaが完全であるかどうかに関係なく、JavaでWindowsデバイスドライバーを作成することはできません。(または、チューリング完全でもあるPostScriptで)Javaにそれを可能にする機能を追加すると、それで解決できる一連の問題が変更されますが、それは起こりません。ですから、私はアセンブリポイントを好みます。実行可能なすべてのことは実行可能なCPU命令に実装されているため、すべてのCPU命令をサポートするアセンブリで実行できないことはありません(チューリングマシンの形式よりも理解しやすい)。
Holger、

2
@abelitoは、すべての高レベルのプログラミング構造に共通しており、「実際に起こっていることに対する可視性」を提供することが少ないのです。これを使用して、コードを改善することも悪化させることもできます。それは単なるツールです。投票機能のように; これは、投稿の実際の品質について根拠のある判断を下すために使用できるツールです。または、ラムダを嫌うからといって、精巧で合理的な回答に反対票を投じることもできます。
ホルガー

52

プログラミング言語は、マシンが実行するためのものではありません。

彼らはプログラマが考えるためのものですです。

言語は、私たちの考えをマシンが実行できるものに変えるためのコンパイラとの会話です。他の言語から、それに来る(または人からのJavaについての主訴の一つ残して、それをするために他の言語では)それということに使用されるプログラマに一定のメンタルモデルを(つまり、すべてがクラスです)。

それが良いのか悪いのかを検討するつもりはありません。すべてがトレードオフです。しかし、Java 8ラムダにより、プログラマーは関数の観点から考えることができますことができます。これは、以前はJavaでは不可能でした。

手続き型プログラマが考えることを学ぶのと同じことですがJavaに関してクラスの観点です。構造化された構造体であるクラスから徐々に移動し、一連の静的メソッドを持つ「ヘルパー」クラスがあり、より合理的なOO設計に似ています(平均culpa)。

匿名の内部クラスを表現するためのより短い方法としてそれらを単に考えれば、おそらく上記の手続き型プログラマーがクラスが大きな改善であるとは考えなかったのと同じように、それらを非常に印象的に見つけることはできないでしょう。


5
それでも注意してください、私はOOPがすべてであると思っていたので、エンティティコンポーネントシステムアーキテクチャとひどく混乱しました(ワダヤは、ゲームオブジェクトの種類ごとにクラスがないことを意味します!!と各ゲームオブジェクトOOPオブジェクトではありませんか?!)
user253751 2017年

3
言い換えれば、* o f *クラスを別のツールとして考えるのではなく OOPで考えることは、私の考えを悪い方法で制限しました。
user253751 2017年

2
@immibis:「OOPで考える」にはさまざまな方法があるので、「OOPで考える」と「クラスで考える」を混同するというよくある間違いに加えて、生産的でない方法で考える方法がたくさんあります。残念ながら、本やチュートリアルでさえ劣ったものを教え、例に反パターンを示し、その考えを広めるので、それは最初からあまりにも頻繁に起こります。「あらゆる種類のクラスを持つ」という想定される必要性は、単なる一例であり、抽象化の概念と矛盾します。同様に、「OOPは可能な限り定型文を書くことを意味する」という神話などがあるようです
Holger

@immibisその例ではそれはあなたを助けていませんでしたが、その逸話は実際に要点をサポートしています:言語とパラダイムはあなたの思考を構造化し、それらをデザインに変えるためのフレームワークを提供します。あなたのケースでは、フレームワークの端にぶつかりましたが、別のコンテキストでは、それが大きな泥の玉に迷い込んで醜いデザインを生み出すのに役立つかもしれません。したがって、「すべてがトレードオフである」と思います。前者の問題を回避しながら後者の利点を維持することは、言語をどの程度「大きく」するかを決定する際の重要な問題です。
Leushenko

@immibisだからこそ、たくさんのパラダイムをよく学ぶことが重要ですです。それぞれが他の不備についてあなたに届きます。
Jared Smith

36

コード行の保存は、他の人が読んで理解するのにかかる時間がかからない、より短時間で明確な方法で大量のロジックを記述できる場合、新機能と見なすことができます。

ラムダ式(またはメソッド参照、あるいはその両方)Streamがなければ、パイプラインははるかに読みにくくなります。

たとえば、Stream各ラムダ式を匿名クラスインスタンスに置き換えた場合、次のパイプラインがどのように見えるかを考えてください。

List<String> names =
    people.stream()
          .filter(p -> p.getAge() > 21)
          .map(p -> p.getName())
          .sorted((n1,n2) -> n1.compareToIgnoreCase(n2))
          .collect(Collectors.toList());

それはそのようになります:

List<String> names =
    people.stream()
          .filter(new Predicate<Person>() {
              @Override
              public boolean test(Person p) {
                  return p.getAge() > 21;
              }
          })
          .map(new Function<Person,String>() {
              @Override
              public String apply(Person p) {
                  return p.getName();
              }
          })
          .sorted(new Comparator<String>() {
              @Override
              public int compare(String n1, String n2) {
                  return n1.compareToIgnoreCase(n2);
              }
          })
          .collect(Collectors.toList());

これはラムダ式のバージョンよりも書くのがはるかに難しく、エラーが発生しやすくなります。理解するのも難しいです。

そして、これは比較的短いパイプラインです。

ラムダ式やメソッド参照なしでこれを読みやすくするには、ここで使用されているさまざまな機能インターフェイスインスタンスを保持する変数を定義する必要があり、パイプラインのロジックが分割され、理解が難しくなります。


17
これは重要な洞察だと思います。構文上および認識上のオーバーヘッドが大きすぎて役に立たない場合、他のアプローチの方が優れている場合、プログラミング言語で何かを行うのは事実上不可能であると主張できます。したがって、(ラムダは匿名クラスと比較してそうであるように)構文と認識のオーバーヘッドを減らすことにより、上記のようなパイプラインが可能になります。もっとひたすら、コードの一部を書くことで仕事から解雇されるとしたら、それは不可能だと言えるでしょう。
Sebastian Redl 2017年

Nitpick:とにかく自然な順序で比較するComparatorためsorted、実際にはfor は必要ありません。例をさらに良くするために、文字列の長さを比較するなどに変更することはできますか?
tobias_k 2017年

@tobias_k問題ありません。compareToIgnoreCaseと動作が異なるように変更しましたsorted()
エラン

1
compareToIgnoreCase既存のString.CASE_INSENSITIVE_ORDERコンパレータをそのまま使用できるので、それでも悪い例です。@tobias_kによる長さによる比較(または組み込みのコンパレータを持たないその他のプロパティ)の提案は、より適切です。SO Q&Aコードの例から判断すると、ラムダは多くの不要なコンパレータ実装を引き起こしました…
Holger

2番目の例の方が理解しやすいと思うのは私だけですか?
クラークバトル

8

内部反復

Javaコレクションを反復するとき、ほとんどの開発者は要素を取得し処理する傾向があります。これは、その項目を取り出してから使用するか、再挿入するなどです。Javaの8より前のバージョンでは、内部クラスを実装して次のようなことができます。

numbers.forEach(new Consumer<Integer>() {
    public void accept(Integer value) {
        System.out.println(value);
    }
});

Java 8を使用すると、次のことを行うことで、より優れた冗長な操作を実行できます。

numbers.forEach((Integer value) -> System.out.println(value));

以上

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

引数としての動作

次のケースを推測してください:

public int sumAllEven(List<Integer> numbers) {
    int total = 0;

    for (int number : numbers) {
        if (number % 2 == 0) {
            total += number;
        }
    } 
    return total;
}

Java 8 Predicateインターフェースを使用すると、次のようにすることができます。

public int sumAll(List<Integer> numbers, Predicate<Integer> p) {
    int total = 0;

    for (int number : numbers) {
        if (p.test(number)) {
            total += number;
        }
    }
    return total;
}

次のように呼び出す:

sumAll(numbers, n -> n % 2 == 0);

出典:DZone-Javaでラムダ式が必要な理由


おそらく最初のケースはコードのいくつかの行を保存する方法ですが、コードを読みやすくします
別に、

4
内部反復はどのようにラムダ機能ですか?実際の単純な問題は、技術的にはラムダでは、java-8以前はできなかったことは何もできないということです。コードを渡すためのより簡潔な方法です。
Ousmane D.17年

@Aominèこの場合numbers.forEach((Integer value) -> System.out.println(value));、ラムダ式は2つの部分で構成され、1つはパラメーターをリストする矢印記号(->)の左側、もう1つは本体を含む右側の部分です。コンパイラーは、ラムダ式に、コンシューマーインターフェイスの実装されていない唯一のメソッドと同じシグネチャがあることを自動的に判断します。
それ以外の場合は、

5
私はラムダ式が何であるかを尋ねませんでした、私は単に内部反復がラムダ式とは何の関係もないと言っているだけです。
Ousmane D.17年

1
@Aominèおっしゃるとおりですが、ラムダ自体には何もありませんが、ご指摘のとおり、よりすっきりとした書き方のようです。私は、効率の観点から考えるには、前の8バージョンとして良いようです
alseether

4

次のように、内部クラスの代わりにラムダを使用することには多くの利点があります。

  • 言語構文のセマンティクスを追加せずに、コードをよりコンパクトで表現力のあるものにします。あなたはすでにあなたの質問に例を挙げました。

  • ラムダを使用することで、コレクションのマップリデュース変換など、要素のストリームに対する関数スタイルの操作を使用してプログラミングできます。java.util.functionおよびjava.util.streamパッケージのドキュメントを参照してください。

  • コンパイラーによってラムダ用に生成される物理クラスファイルはありません。したがって、提供されるアプリケーションが小さくなります。メモリはどのようにラムダに割り当てますか?

  • ラムダがスコープ外の変数にアクセスしない場合、コンパイラはラムダの作成を最適化します。つまり、ラムダインスタンスはJVMによって一度だけ作成されます。詳細については、質問に対する@Holgerの回答をご覧ください。Java8 では、メソッド参照キャッシュは良いアイデアですか?

  • Lambdasは、関数型インターフェースに加えてマルチマーカーインターフェースを実装できますが、匿名の内部クラスはそれ以上のインターフェースを実装できません。次に例を示します。

    //                 v--- create the lambda locally.
    Consumer<Integer> action = (Consumer<Integer> & Serializable) it -> {/*TODO*/};

2

ラムダは、匿名クラスの構文糖衣です。

ラムダの前は、匿名クラスを使用して同じことを実現できます。すべてのラムダ式は匿名クラスに変換できます。

IntelliJ IDEAを使用している場合は、変換を実行できます。

  • ラムダにカーソルを置きます
  • Alt / Option + Enterを押します

ここに画像の説明を入力してください


3
... @StuartMarksはすでにラムダの構文糖度(sp?gr?)を明確にしていると思いましたか?(stackoverflow.com/a/15241404/3475600softwareengineering.stackexchange.com/a/181743
srborlongan

2

あなたの質問に答えるために、実際の問題は、ラムダで java-8より前にできなかったことは何もできないようにすることです。むしろ、より簡潔なコードを書くことができます。これの利点は、コードがより明確でより柔軟になることです。


明らかに、@ Holgerはラムダ効率の点で疑問を解消しています。それだけでなく、コードクリーナーを作るための方法ですが、ラムダは、任意の値をキャプチャしていない場合、それはシングルトンクラス(再利用可能)になります
alseether

深く掘り下げると、すべての言語機能に違いがあります。手元の質問は高いレベルにあるので、私は自分の答えを高いレベルで書きました。
Ousmane D.17年

2

まだ言及していませんが、ラムダを使用すると、使用する機能を定義できます。

したがって、いくつかの単純な選択関数がある場合は、ボイラープレートの束を使用して別の場所に配置する必要はなく、簡潔でローカルに関連するラムダを記述するだけです。


1

はい、多くの利点があります。

  • クラス全体を定義する必要はなく、参照として関数の実装自体を渡すことができます。
    • 内部でクラスを作成すると.classファイルが作成されますが、ラムダを使用する場合、ラムダではクラスではなく関数の実装を渡すため、クラスの作成はコンパイラーによって回避されます。
  • コードの再利用性は以前よりも高い
  • そしてあなたが言ったように、コードは通常の実装よりも短いです。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.