CodeGym /Java Blog /ランダム /Javaのラムダ式について解説します。例とタスク付き。パート1
John Squirrels
レベル 41
San Francisco

Javaのラムダ式について解説します。例とタスク付き。パート1

ランダム グループに公開済み
この記事は誰に向けたものですか?
  • これは、Java Core についてはすでによく知っているが、Java のラムダ式についてはまったく知らない人を対象としています。あるいは、ラムダ式について何か聞いたことがあるかもしれませんが、詳細は不明です
  • これは、ラムダ式をある程度理解しているものの、ラムダ式にまだ気が遠く、使用に慣れていない人を対象としています。
Javaのラムダ式について解説します。 例とタスク付き。 パート 1 - 1これらのカテゴリのいずれにも当てはまらない場合は、この記事が退屈、欠陥がある、または一般的に興味のない記事であると感じるかもしれません。この場合、ご自由に他のことに移っていただいても構いません。あるいは、このテーマに精通している場合は、記事を改善または補足する方法についてコメント欄で提案してください。この資料には、新規性はおろか、学術的価値があるとは主張されていません。全く逆です。私は、(一部の人にとっては) 複雑なことをできるだけ簡単に説明しようとします。Stream API について説明してほしいというリクエストが、これを書くきっかけになりました。私はそれについて考え、ラムダ式を理解していないとストリームの例の一部は理解できないと判断しました。それでは、ラムダ式から始めます。 この記事を理解するには何を知っておく必要がありますか?
  1. オブジェクト指向プログラミング (OOP)、つまり次のことを理解している必要があります。

    • クラス、オブジェクト、およびそれらの違い。
    • インターフェイス、クラスとの違い、インターフェイスとクラスの関係。
    • メソッド、その呼び出し方法、抽象メソッド (つまり、実装のないメソッド)、メソッド パラメータ、メソッド引数、およびそれらを渡す方法。
    • アクセス修飾子、静的メソッド/変数、最終メソッド/変数。
    • クラスとインターフェイスの継承、インターフェイスの多重継承。
  2. Java コアの知識: ジェネリック型 (ジェネリック)、コレクション (リスト)、スレッド。
さて、それでは始めましょう。

ちょっとした歴史

ラムダ式は関数型プログラミングから Java に導入され、さらに数学から Java に導入されました。20 世紀半ばのアメリカでは、数学とあらゆる種類の抽象化が大好きだったアロンゾ チャーチがプリンストン大学で働いていました。ラムダ計算を発明したのはアロンゾ チャーチでしたが、ラムダ計算は当初、プログラミングとはまったく無関係の一連の抽象的なアイデアでした。アラン・チューリングやジョン・フォン・ノイマンなどの数学者も同じ時期にプリンストン大学で働いていました。すべてが結びつき、チャーチはラムダ計算を思いつきました。チューリングは、現在「チューリング マシン」として知られる抽象コンピューティング マシンを開発しました。そしてフォン・ノイマンは、現代のコンピュータの基礎となったコンピュータ・アーキテクチャ(現在は「フォン・ノイマン・アーキテクチャ」と呼ばれています)を提案しました。その時、アロンゾ教会は」彼のアイデアは、(純粋数学の分野を除いて) 彼の同僚の研究ほどには有名ではありませんでした。しかし、少し後、ジョン・マッカーシー(同じくプリンストン大学の卒業生で、この話の時点ではマサチューセッツ工科大学の職員)がチャーチの考えに興味を持ち始めました。1958 年に、彼はこれらのアイデアに基づいて最初の関数型プログラミング言語 LISP を作成しました。そして 58 年後、関数型プログラミングのアイデアが Java 8 に浸透しました。まだ 70 年も経っていません...正直に言うと、これは数学的なアイデアが実際に適用されるまでにかかった最長時間ではありません。マサチューセッツ工科大学の職員)は、チャーチの考えに興味を持ちました。1958 年に、彼はこれらのアイデアに基づいて最初の関数型プログラミング言語 LISP を作成しました。そして 58 年後、関数型プログラミングのアイデアが Java 8 に浸透しました。まだ 70 年も経っていません...正直に言うと、これは数学的なアイデアが実際に適用されるまでにかかった最長時間ではありません。マサチューセッツ工科大学の職員)は、チャーチの考えに興味を持ちました。1958 年に、彼はこれらのアイデアに基づいて最初の関数型プログラミング言語 LISP を作成しました。そして 58 年後、関数型プログラミングのアイデアが Java 8 に浸透しました。まだ 70 年も経っていません...正直に言うと、これは数学的なアイデアが実際に適用されるまでにかかった最長時間ではありません。

問題の核心

ラムダ式は関数の一種です。これは通常の Java メソッドであると考えることができますが、引数として他のメソッドに渡すことができる独特の機能を備えています。それは正しい。メソッドに数値や文字列、猫だけでなく他のメソッドも渡すことができるようになりました!これはいつ必要になるでしょうか? たとえば、コールバック メソッドを渡したい場合などに役立ちます。つまり、呼び出すメソッドが必要な場合、そのメソッドに渡す他のメソッドを呼び出すことができます。言い換えれば、特定の状況では 1 つのコールバックを渡し、別の状況では別のコールバックを渡すことができるということです。そして、コールバックを受け取るメソッドがコールバックを呼び出すようにします。並べ替えは簡単な例です。次のような賢い並べ替えアルゴリズムを作成しているとします。

public void mySuperSort() { 
    // We do something here 
    if(compare(obj1, obj2) > 0) 
    // And then we do something here 
}
このステートメントでは、比較する 2 つのオブジェクトを渡してメソッドを呼び出し、これらのオブジェクトのどちらが「大きい」かを知りたいと考えていますifcompare()「大きい」ものが「小さい」ものより前に来ると仮定します。「大きい」を引用符で囲んだのは、昇順だけでなく降順でも並べ替える方法を認識する汎用メソッドを作成しているためです (この場合、「大きい」オブジェクトは実際には「小さい」オブジェクトになります) 、 およびその逆)。ソートに特定のアルゴリズムを設定するには、それをメソッドに渡す何らかのメカニズムが必要ですmySuperSort()。こうすることで、メソッドが呼び出されたときにメソッドを「制御」できるようになります。もちろん、2 つの別々のメソッドを作成することもできますmySuperSortAscend()mySuperSortDescend()— 昇順と降順で並べ替えます。または、メソッドに何らかの引数を渡すこともできます (たとえば、ブール変数。true の場合は昇順で並べ替え、false の場合は降順で並べ替えます)。しかし、文字列配列のリストなどの複雑なものをソートしたい場合はどうすればよいでしょうか? 私たちのメソッドはどのようにしてmySuperSort()これらの文字列配列をソートする方法を知るのでしょうか? サイズ的には?すべての単語の合計の長さによって? おそらく配列の最初の文字列に基づいてアルファベット順でしょうか? そして、場合によっては配列のサイズによって、また別の場合には各配列内のすべての単語の累積長によって配列のリストを並べ替える必要がある場合はどうなるでしょうか? コンパレータについてはすでに聞いたことがあると思いますが、この場合は、目的の並べ替えアルゴリズムを記述するコンパレータ オブジェクトを並べ替えメソッドに渡すだけです。スタンダードなので、sort()メソッドは と同じ原理に基づいて実装されており、例で mySuperSort()使用します。sort()

String[] array1 = {"Dota", "GTA5", "Halo"}; 
String[] array2 = {"I", "really", "love", "Java"}; 
String[] array3 = {"if", "then", "else"}; 

List<String[]> arrays = new ArrayList<>(); 
arrays.add(array1); 
arrays.add(array2); 
arrays.add(array3); 

Comparator<;String[]> sortByLength = new Comparator<String[]>() { 
    @Override 
    public int compare(String[] o1, String[] o2) { 
        return o1.length - o2.length; 
    } 
}; 

Comparator<String[]> sortByCumulativeWordLength = new Comparator<String[]>() { 

    @Override 
    public int compare(String[] o1, String[] o2) { 
        int length1 = 0; 
        int length2 = 0; 
        for (String s : o1) { 
            length1 += s.length(); 
        } 

        for (String s : o2) { 
            length2 += s.length(); 
        } 

        return length1 - length2; 
    } 
};

arrays.sort(sortByLength);
結果:

  1. Dota GTA5 Halo
  2. if then else
  3. I really love Java
ここでは、配列は各配列内の単語の数によって並べ替えられます。ワード数が少ない配列は「より少ない」とみなされます。だからこそ、それが第一です。より多くの単語を含む配列は「より大きい」とみなされ、最後に配置されます。sort()メソッドに別のコンパレータ ( など)を渡すとsortByCumulativeWordLength、別の結果が得られます。

  1. if then else
  2. Dota GTA5 Halo
  3. I really love Java
ここで、配列は、配列の単語内の文字の合計数によって並べ替えられます。最初の配列には 10 文字、2 番目には 12 文字、3 番目には 15 文字があります。コンパレータが 1 つしかない場合は、それに対して別の変数を宣言する必要はありません。代わりに、メソッドの呼び出し時に匿名クラスを作成するだけですsort()。このようなもの:

String[] array1 = {"Dota", "GTA5", "Halo"}; 
String[] array2 = {"I", "really", "love", "Java"}; 
String[] array3 = {"if", "then", "else"}; 

List<String[]> arrays = new ArrayList<>(); 

arrays.add(array1); 
arrays.add(array2); 
arrays.add(array3); 

arrays.sort(new Comparator<String[]>() { 
    @Override 
    public int compare(String[] o1, String[] o2) { 
        return o1.length - o2.length; 
    } 
}); 
最初のケースと同じ結果が得られます。タスク 1.この例を書き換えて、配列を各配列内のワード数の昇順ではなく降順に並べ替えます。私たちはこれらすべてをすでに知っています。私たちはオブジェクトをメソッドに渡す方法を知っています。その時点で必要なものに応じて、さまざまなオブジェクトをメソッドに渡すことができ、メソッドは実装したメソッドを呼び出します。ここで疑問が生じます。一体なぜここでラムダ式が必要なのでしょうか?  ラムダ式はメソッドを 1 つだけ持つオブジェクトであるためです。「メソッドオブジェクト」のようなもの。オブジェクトにパッケージ化されたメソッド。ちょっと見慣れない構文を持っているだけです (ただし、これについては後ほど詳しく説明します)。 このコードをもう一度見てみましょう。

arrays.sort(new Comparator<String[]>() { 
    @Override 
    public int compare(String[] o1, String[] o2) { 
        return o1.length - o2.length; 
    } 
});
ここでは、配列リストを取得してそのsort()メソッドを呼び出し、そこに 1 つのメソッドを持つコンパレータ オブジェクトを渡しますcompare()(名前は私たちにとって重要ではありません。結局のところ、これがこのオブジェクトの唯一のメソッドなので、間違えることはありません)。このメソッドには 2 つのパラメーターがあり、これらを使用します。IntelliJ IDEA で作業している場合は、次のようにコードを大幅に圧縮することが提案されているのを見たことがあるでしょう。

arrays.sort((o1, o2) -> o1.length - o2.length);
これにより、6 行が 1 つの短い行に減ります。6 行が 1 つの短い行として書き換えられます。何かが消えましたが、それが重要なものではなかったことは保証します。このコードは、匿名クラスの場合とまったく同じように機能します。 タスク 2.ラムダ式を使用してタスク 1 のソリューションを書き直すことを推測します (少なくとも、IntelliJ IDEA に匿名クラスをラムダ式に変換するよう依頼してください)。

インターフェースについて話しましょう

原則として、インターフェイスは単なる抽象メソッドのリストです。何らかのインターフェースを実装するクラスを作成する場合、そのクラスはインターフェースに含まれるメソッドを実装する必要があります (またはクラスを抽象化する必要があります)。多数の異なるメソッドを備えたインターフェイス (たとえば、  List) もあれば、1 つのメソッドのみを備えたインターフェイス (たとえば、ComparatorまたはRunnable) もあります。単一のメソッドを持たないインターフェイス ( などのいわゆるマーカー インターフェイスSerializable) があります。メソッドが 1 つだけあるインターフェイスは、関数インターフェイスとも呼ばれます。Java 8 では、これらは特別なアノテーションでマークされています。@FunctionalInterface。ラムダ式のターゲット タイプとして適しているのは、これらの単一メソッド インターフェイスです。上で述べたように、ラムダ式はオブジェクトにラップされたメソッドです。そして、そのようなオブジェクトを渡すとき、本質的にはこの 1 つのメソッドを渡していることになります。メソッドの名前は気にしないことがわかりました。私たちにとって重要なのは、メソッドのパラメーターと、もちろんメソッドの本体だけです。本質的に、ラムダ式は関数インターフェイスの実装です。単一のメソッドを持つインターフェイスが表示される場合はどこでも、匿名クラスをラムダとして書き換えることができます。インターフェイスに複数のメソッドがある場合、または複数のメソッドがある場合、ラムダ式は機能せず、代わりに匿名クラス、または通常のクラスのインスタンスを使用します。ここで、ラムダについて少し掘り下げてみましょう。:)

構文

一般的な構文は次のようなものです。

(parameters) -> {method body}
つまり、メソッド パラメーターを括弧で囲み、「矢印」 (ハイフンと不等号で形成)、そしていつものように中括弧で囲んだメソッド本体です。パラメータは、インターフェイス メソッドで指定されたパラメータに対応します。変数の型がコンパイラーによって明確に決定できる場合 (この例では、オブジェクトが]Listを使用して型指定されているため、文字列配列を操作していることが認識さString[れています)、変数の型を指定する必要はありません。
あいまいな場合は、その種類を示します。IDEA が必要ない場合は灰色に着色されます。
詳細については、このOracle チュートリアルや他の場所で読むことができます。これを「ターゲットタイピング」と呼びます。変数には任意の名前を付けることができます。インターフェイスで指定されているのと同じ名前を使用する必要はありません。パラメータがない場合は、空の括弧を指定するだけです。パラメータが 1 つだけの場合は、括弧を付けずに変数名を単純に指定します。パラメータを理解したところで、ラムダ式の本体について説明します。中括弧内には、通常のメソッドと同じようにコードを記述します。コードが 1 行で構成されている場合は、中括弧を完全に省略できます (if ステートメントや for ループと同様)。単一行のラムダが何かを返す場合は、return声明。ただし、中括弧を使用する場合は、return通常のメソッドの場合と同様に、ステートメントを明示的に含める必要があります。

例1.

() -> {}
最も単純な例。そして最も無意味なこと:)、それは何もしないからです。 例2。

() -> ""
もう一つの興味深い例。何も取らず、空の文字列を返します(return不要なため省略されています)。これは同じことですが、次のようになりますreturn

() -> { 
    return ""; 
}
例 3.「ハロー、ワールド!」ラムダを使用する

() -> System.out.println("Hello, World!")
これは何も取らず、何も返しません (メソッドの戻り値の型が であるため、returnの呼び出しの前に 置くことはできません)。単に挨拶を表示するだけです。これはインターフェイスの実装に最適です。次の例はより完全です。 System.out.println()println()voidRunnable

public class Main { 
    public static void main(String[] args) { 
        new Thread(() -> System.out.println("Hello, World!")).start(); 
    } 
}
または次のようにします。

public class Main { 
    public static void main(String[] args) { 
        Thread t = new Thread(() -> System.out.println("Hello, World!")); 
        t.start();
    } 
}
または、ラムダ式をオブジェクトとして保存してRunnable、コンストラクターに渡すこともできますThread

public class Main { 
    public static void main(String[] args) { 
        Runnable runnable = () -> System.out.println("Hello, World!"); 
        Thread t = new Thread(runnable); 
        t.start(); 
    } 
}
ラムダ式が変数に保存される瞬間を詳しく見てみましょう。インターフェイスRunnableは、そのオブジェクトにメソッドが必要であることを示していますpublic void run()。インターフェイスによれば、このrunメソッドはパラメータを取りません。そして、何も返しません。つまり、戻り値の型は ですvoid。したがって、このコードは何も受け取らない、または何も返さないメソッドでオブジェクトを作成します。これはRunnableインターフェースのrun()メソッドと完全に一致します。このため、このラムダ式をRunnable変数に入れることができました。  例4.

() -> 42
繰り返しますが、何もかかりませんが、数値 42 が返されます。Callableこのインターフェイスには次のようなメソッドが 1 つだけあるため、このようなラムダ式は変数に入れることができます。

V call(),
ここで、 V は戻り値の型 (この場合は int) です。したがって、次のようにラムダ式を保存できます。

Callable<Integer> c = () -> 42;
例 5.複数の行を含むラムダ式

() -> { 
    String[] helloWorld = {"Hello", "World!"}; 
    System.out.println(helloWorld[0]); 
    System.out.println(helloWorld[1]); 
}
繰り返しますが、これはパラメーターと戻り値の型を持たないラムダ式ですvoid(ステートメントがないためreturn)。  例6

x -> x
ここでは変数を取得しxて返します。パラメータが 1 つだけの場合は、そのパラメータを囲む括弧を省略できることに注意してください。以下は同じことですが、括弧が付いています:

(x) -> x
次に、明示的な return ステートメントを使用した例を示します。

x -> { 
    return x;
}
または、括弧と return ステートメントを使用して次のようにします。

(x) -> { 
    return x;
}
または、型を明示的に指定します (したがって括弧を使用します)。

(int x) -> x
例 7

x -> ++x
それを受け取ってx返しますが、それは 1 を追加した後でのみです。そのラムダを次のように書き換えることができます。

x -> x + 1
どちらの場合も、パラメーターとメソッド本体、およびステートメントを囲む括弧はreturnオプションであるため、省略します。括弧と return ステートメントを含むバージョンは、例 6 に示されています。 例 8

(x, y) -> x % y
xと を 取り、によるy除算の余りを返します。ここではパラメータを囲む括弧が必要です。これらは、パラメータが 1 つしかない場合にのみオプションです。ここでは型を明示的に示しています。 xy

(double x, int y) -> x % y
例9

(Cat cat, String name, int age) -> {
    cat.setName(name); 
    cat.setAge(age); 
}
Catオブジェクト、String名前、整数の年齢を 取得します。メソッド自体では、渡された名前と年齢を使用して猫に変数を設定します。このcatオブジェクトは参照型であるため、ラムダ式の外側で変更されます (渡された名前と年齢が取得されます)。以下は、同様のラムダを使用するもう少し複雑なバージョンです。

public class Main { 

    public static void main(String[] args) { 
        // Create a cat and display it to confirm that it is "empty" 
        Cat myCat = new Cat(); 
        System.out.println(myCat);
 
        // Create a lambda 
        Settable<Cat> s = (obj, name, age) -> { 
            obj.setName(name); 
            obj.setAge(age); 

        }; 

        // Call a method to which we pass the cat and lambda 
        changeEntity(myCat, s); 

        // Display the cat on the screen and see that its state has changed (it has a name and age) 
        System.out.println(myCat); 

    } 

    private static <T extends HasNameAndAge>  void changeEntity(T entity, Settable<T> s) { 
        s.set(entity, "Smokey", 3); 
    }
}

interface HasNameAndAge { 
    void setName(String name); 
    void setAge(int age); 
}

interface Settable<C extends HasNameAndAge> { 
    void set(C entity, String name, int age); 
}

class Cat implements HasNameAndAge { 
    private String name; 
    private int age; 

    @Override 
    public void setName(String name) { 
        this.name = name;
    }

    @Override
    public void setAge(int age) {
        this.age = age; 
    } 

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' + 
                ", age=" + age + 
                '}';
    }
}
結果:

Cat{name='null', age=0}
Cat{name='Smokey', age=3}
ご覧のとおり、Catオブジェクトには 1 つの状態があり、ラムダ式を使用した後に状態が変化しました。ラムダ式はジェネリックと完全に結合します。Dogまた、 も実装するクラスを作成する必要がある場合は、 ラムダ式を変更せずにメソッド 内でHasNameAndAge同じ操作を実行できます。タスク 3.数値を受け取りブール値を返すメソッドを含む関数インターフェイスを作成します。このようなインターフェイスの実装を、渡された数値が 13 で割り切れる場合に true を返すラムダ式として作成します 。 タスク 4.Dogmain()2 つの文字列を受け取り、文字列も返すメソッドを使用して関数インターフェイスを作成します。このようなインターフェイスの実装は、より長い文字列を返すラムダ式として作成します。 タスク 5. 3 つの浮動小数点数 a、b、c を受け取り、浮動小数点数を返すメソッドを含む関数インターフェイスを作成します。このようなインターフェイスの実装は、判別式を返すラムダ式として作成します。忘れた場合に備えて、それは ですD = b^2 — 4acタスク 6.タスク 5 の関数インターフェイスを使用して、 の結果を返すラムダ式を作成しますa * b^cJavaのラムダ式について解説します。例とタスク付き。パート2
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION