JavaRush /Java Blog /Random-JA /Java のラムダ式について人気があります。例とタスク付き。パート1
Стас Пасинков
レベル 26
Киев

Java のラムダ式について人気があります。例とタスク付き。パート1

Random-JA グループに公開済み
この記事は誰に向けたものですか?
  • Java Core についてはすでによく知っているが、Java のラムダ式についてはまったく知らない人向けです。あるいは、ラムダについてはすでに聞いたことがあるかもしれませんが、詳細は知りません。
  • ラムダ式をある程度理解しているが、まだ使用するのが怖くて慣れていない人向けです。
これらのカテゴリのいずれにも当てはまらない場合は、この記事が退屈で間違っており、一般的に「クールではない」と感じるかもしれません。この場合は、ご自由にスルーしていただくか、このトピックに精通している場合は、コメントで記事を改善または補足できる方法を提案してください。この資料は学術的価値を主張するものではなく、ましてや新規性を主張するものではありません。むしろその逆です。その中で、(一部の人にとっては)複雑なことをできるだけ簡単に説明しようとします。私がこの記事を書くきっかけとなったのは、ストリーム API について説明してほしいというリクエストでした。私はそれについて考え、ラムダ式を理解していなければ、「ストリーム」に関する例のいくつかは理解できないだろうと判断しました。それではラムダから始めましょう。 Java のラムダ式について人気があります。 例とタスク付き。 パート 1 - 1この記事を理解するには次の知識が必要です。
  1. オブジェクト指向プログラミング (以下、OOP と呼びます) の理解:
    • クラスとオブジェクトとは何か、それらの違いは何なのかについての知識。
    • インターフェイスとは何か、クラスとどのように異なるのか、それらの間 (インターフェイスとクラス) の関係は何なのかについての知識。
    • メソッドとは何か、それを呼び出す方法、抽象メソッド (または実装のないメソッド) とは何か、メソッドのパラメータ/引数とは何か、それらをそこに渡す方法についての知識。
    • アクセス修飾子、静的メソッド/変数、最終メソッド/変数。
    • 継承 (クラス、インターフェース、インターフェースの多重継承)。
  2. Java コアの知識: ジェネリックス、コレクション (リスト)、スレッド。
さて、始めましょう。

ちょっとした歴史

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

本質

ラムダ式はそのような関数です。これは Java の通常のメソッドと考えることができます。唯一の違いは、引数として他のメソッドに渡すことができることです。そう、数値、文字列、cat だけでなく、他のメソッドもメソッドに渡すことができるようになりました。これはいつ必要になるでしょうか? たとえば、コールバックを渡したい場合です。呼び出すメソッドが、それに渡す他のメソッドを呼び出せるようにする必要があります。つまり、場合によっては 1 つのコールバックを送信し、別の場合には別のコールバックを送信する機会が得られます。そして、コールバックを受け入れるメソッドがコールバックを呼び出します。簡単な例は並べ替えです。次のような、ある種のトリッキーな並べ替えを作成するとします。
public void mySuperSort() {
    // ... do something here
    if(compare(obj1, obj2) > 0)
    // ... and here we do something
}
ここで、ifメソッドを呼び出しcompare()、比較する 2 つのオブジェクトを渡し、これらのオブジェクトのどちらが「大きい」かを調べます。「大きい」ものを「小さい」ものの前に置きます。「more」を引用符で囲んで書いたのは、昇順だけでなく降順でもソートできる汎用メソッドを作成しているためです (この場合、「more」は本質的に小さいオブジェクトになり、その逆も同様です)。 。ソート方法を正確に設定するには、何らかの方法でルールを に渡す必要がありますmySuperSort()。この場合、メソッドの呼び出し中にメソッドを何らかの方法で「制御」することができます。もちろん、昇順と降順で並べ替えるmySuperSortAsc()ための2 つの別々のメソッドを作成することもできます。mySuperSortDesc()または、メソッド内でパラメーターを渡します (たとえば、booleaniftrueは昇順でソートし、 if はfalse降順でソートします)。しかし、単純な構造ではなく、たとえば文字列配列のリストを並べ替えたい場合はどうすればよいでしょうか? mySuperSort()私たちのメソッドはどのようにしてこれらの文字列配列をソートする方法を知るのでしょうか? サイズ的には?単語の合計の長さによって? おそらく配列の最初の行に応じてアルファベット順でしょうか? しかし、場合によっては配列のサイズによって配列のリストを並べ替える必要があり、別の場合には配列内の単語の合計の長さによって並べ替える必要がある場合はどうなるでしょうか? コンパレータについてはすでに聞いたことがあると思います。そのような場合は、ソート メソッドにコンパレータ オブジェクトを渡すだけで、ソートに使用するルールを記述します。標準メソッドはsort()と同じ原理で実装されているため、mySuperSort()例では標準メソッドを使用しますsort()
String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};

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[]> sortByWordsLength = 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. お母さんがフレームを洗った
  2. 労働党は平和を実現できる
  3. 私は本当にジャワが大好きです
ここでは、配列は各配列内の単語の数によって並べ替えられます。ワード数が少ない配列は「小さい」とみなされます。だからこそ最初に来るのです。単語数が多い方が「多い」とみなされ、最後に終わります。sort()別のコンパレータをメソッドに渡すと(sortByWordsLength)結果は異なります。
  1. 労働党は平和を実現できる
  2. お母さんがフレームを洗った
  3. 私は本当にジャワが大好きです
ここで、配列は、そのような配列の単語に含まれる文字の合計数によって並べ替えられます。最初のケースには 10 文字、2 番目には 12 文字、3 番目には 15 文字があります。コンパレータを 1 つだけ使用する場合、そのコンパレータに対して別個の変数を作成することはできませんが、単に匿名クラスのオブジェクトを直接作成するだけです。メソッドを呼び出す時間sort()。そのように:
String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};

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;
    }
});
ここでは、リストを取得し、arraysそのメソッドを呼び出します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 つのメソッドを備えたインターフェイスです。上で述べたように、ラムダ式はオブジェクトにラップされたメソッドです。そして、そのようなオブジェクトをどこかに渡すとき、実際にはこの 1 つのメソッドを渡します。このメソッドが何と呼ばれるかは私たちにとって重要ではないことがわかりました。私たちにとって重要なのは、このメソッドが受け取るパラメーター、そして実際にはメソッド コード自体です。ラムダ式は本質的には次のとおりです。関数インターフェイスの実装。1 つのメソッドを持つインターフェイスが表示される場合、ラムダを使用してそのような匿名クラスを書き換えることができることを意味します。インターフェイスに複数のメソッドがある場合、または複数のメソッドがある場合、ラムダ式は適さないため、匿名クラス、または通常のクラスを使用することになります。ラムダを詳しく見てみましょう。:)

構文

一般的な構文は次のようなものです。
(параметры) -> {тело метода}
つまり、括弧内にはメソッド パラメーターである「矢印」 (マイナスとそれ以上の 2 文字が連続しています) があり、その後はいつものようにメソッドの本体が中括弧で囲まれています。パラメーターは、メソッドを説明するときにインターフェイスで指定されたパラメーターに対応します。変数の型がコンパイラによって明確に定義できる場合 (この場合、文字列の配列によってList正確に型指定されるため、文字列の配列を操作していることが確実にわかっています)、変数の型を定義するString[]必要はありません。書かれる。
不明な場合は、タイプを指定すると、IDEA が不要な場合はそのタイプを灰色で強調表示します。
たとえば、Oracle チュートリアルで詳細を読むことができます。これを「ターゲットタイピング」と呼びます。変数には任意の名前を付けることができますが、インターフェイスで指定した名前である必要はありません。パラメータがない場合は、括弧だけを使用します。パラメータが 1 つだけの場合は、括弧なしの変数名のみです。パラメーターを整理しました。次に、ラムダ式自体の本体について説明します。中括弧内に通常のメソッドと同様にコードを記述します。コード全体が 1 行だけで構成されている場合は、(if やループのように) 中かっこを記述する必要はまったくありません。ラムダが何かを返しても、その本体が 1 行で構成されている場合は、return記述する必要はまったくありません。ただし、中括弧がある場合は、通常の方法と同様に、明示的に を記述する必要がありますreturn

例1.
() -> {}
最も単純なオプション。そして最も無意味なもの:)それは何もしないからです。 例2。
() -> ""
これも興味深いオプションです。何も受け入れず、空の文字列を返します(return不要なので省略)。同じですが、次のようになりますreturn
() -> {
    return "";
}
例 3. ラムダを使用した Hello world
() -> System.out.println("Hello world!")
何も受け取らず、何も返しません (メソッドの戻り値の型は単に画面に碑文を表示するだけなので、return呼び出しの前に置くことはできません。インターフェイスの実装に最適です。同じ例はより完全です: System.out.println()println() — void)Runnable
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();
    }
}
または、ラムダ式を type のオブジェクトとして保存し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)。したがって、このように書くと、何も受け取らない、または何も返さないメソッドでオブジェクトが作成されることになります。run()これはインターフェースのメソッドと完全に一致していますRunnable。このため、このラムダ式を のような変数に入れることができましたRunnable例 4
() -> 42
繰り返しますが、何も受け入れませんが、数値 42 を返します。このインターフェイスは次Callableのようなメソッドを 1 つだけ定義しているため、このラムダ式は type の変数に配置できます。
V call(),
ここで、Vは戻り値の型です (この場合はint)。したがって、そのようなラムダ式は次のように格納できます。
Callable<Integer> c = () -> 42;
例 5. 数行の Lambda
() -> {
    String[] helloWorld = {"Hello", "world!"};
    System.out.println(helloWorld[0]);
    System.out.println(helloWorld[1]);
}
繰り返しますが、これはパラメータとその戻り値のないラムダ式ですvoid( がないためreturn)。 例6
x -> x
ここでは、変数に何かを取り込んхで返します。パラメータが 1 つだけ受け入れられる場合は、パラメータを囲む括弧を記述する必要がないことに注意してください。同じですが、括弧が付いています:
(x) -> x
そして、ここに明示的なオプションがありますreturn:
x -> {
    return x;
}
または、次のように括弧と を使用しますreturn
(x) -> {
    return x;
}
または、型を明示的に指定します (それに応じて括弧も使用します)。
(int x) -> x
例 7
x -> ++x
私たちはそれを受け入れてх返品しますが、それ1以上のものがあります。次のように書き換えることもできます。
x -> x + 1
どちらの場合も、パラメータ、メソッド本体、および word を括弧で囲むreturn必要はないため、括弧は示しません。大括弧と改行を含むオプションについては、例 6 で説明します。 例 8
(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 オブジェクト、名前と整数の年齢を含む文字列を受け入れます。メソッド自体では、渡された名前と年齢を Cat に設定します。catこの変数は参照型であるため、ラムダ式の外側にある Cat オブジェクトは変更されます (内側に渡された名前と年齢を受け取ります)。同様のラムダを使用する、もう少し複雑なバージョン:
public class Main {
    public static void main(String[] args) {
        // create a cat and print to the screen to make sure it's "blank"
        Cat myCat = new Cat();
        System.out.println(myCat);

        // create lambda
        Settable<Cat> s = (obj, name, age) -> {
            obj.setName(name);
            obj.setAge(age);
        };

        // call the method, to which we pass the cat and the lambda
        changeEntity(myCat, s);
        // display on the screen and see that the state of the cat has changed (has a name and age)
        System.out.println(myCat);
    }

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

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

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

class Cat implements WithNameAndAge {
    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='Murzik', age=3} ご覧の とおり、最初は Cat オブジェクトには 1 つの状態がありましたが、ラムダ式を使用した後は状態が変化しました。 。ラムダ式はジェネリックとうまく連携します。Dogまた、たとえば、 も実装するクラスを作成する必要がある場合WithNameAndAge、ラムダ式自体をまったく変更せずに、メソッド内main()で Dog を使用して同じ操作を実行できます。 タスク 3。数値を受け取りブール値を返すメソッドを使用して関数インターフェイスを作成します。true渡された数値が剰余なしで 13 で割り切れるかどうかを 返すラムダ式の形式で、このようなインターフェイスの実装を作成します。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