1. ホーム
  2. ジャワ

Java8新機能ラムダ式

2022-02-27 02:10:06
<パス

ラムダカタログ


前書き

Java 8の大きな目玉の1つは、よりクリーンなコードを設計するために使われるラムダ式の導入である。開発者がラムダ式を書くと、それと一緒に関数型インターフェースにコンパイルされます。次の例では、匿名インナークラスの代わりにLambda構文を使っており、コードがきれいなだけでなく、読みやすくなっています。
Lambdaを使用していない古いメソッド。

button.addActionListener(new ActionListener(){
    public void actionPerformed(ActionEvent actionEvent){
        System.out.println("Action detected");
    }
});


Lambdaを使用する。

button.addActionListener( actionEvent -> { 
    System.out.println("Action detected");
});


もっとわかりやすい例を見てみましょう。
Lambdaを使わない昔のやり方。

Runnable runnable1=new Runnable(){
@Override
public void run(){
    System.out.println("Running without Lambda");
}
};


Lambdaを使用する。

Runnable runnable2=()->System.out.println("Running from Lambda");


このように、ラムダ式を使うと、コードがシンプルになるだけでなく、読みやすくなり、そして何より、コード量が格段に減ります。しかし、これらの機能はある程度、ScalaのようなJVM言語ではすでに広く使われているものです。

I. Lambda式の構文とは?

1.1. 構文 I (引数なし、戻り値なし)

() -> System.out.println("Hello Lambda!");



@Test
public void test(){
    Runnable a = new Runnable(){
     @Override
     public void run(){
        System.out.println("Hello World! ")
    }
    };
    //Lambda writing
    Runnable a1 = () -> System.out.println("Hello World!");
    a1.run();
}

// Here the parentheses can be omitted if there is only one argument on the left side ie: x -> System.out.println(x)
(x) -> System.out.println(x)


1.2. 構文 II (パラメータ1つ、戻り値なし)

@Test
public void test(){
    Consumer
 con = (x) -> System.out.println(x);
                         // x -> System.out.println(x);
    con.accept("Hello World!");
}

Comparator 
 com = (x, y) -> {
    System.out.println("functional interface");
    return Integer.compare(x, y);
};


@Test
public void test(){
    //Comparator is annotated with @FunctionalInterface's interface Example abstract method int compare(T o1,T o2);
    Comparator
 com = (x,y) -> {
      System.out.println("hhaha0");
      return (x < y) ? -1 : ((x == y) ? 0 : 1);
    };
    com.compare(1,2);
}

Comparator 
 com = (x, y) -> Integer.compare(x, y);

(Integer x, Integer y) -> Integer.compare(x, y);


1.3. 構文III (引数が2つ以上、戻り値があり、ラムダ本体に複数の文がある場合)

Thread t = new Thread(() -> System.out.println("Hello world"));


public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
}


1.4. 構文IV(ラムダ本体に1つしか文がない場合、returnと中括弧の両方を省略可能)

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}



1.5. 構文 V ( ラムダ式の引数リストのデータ型は、JVMコンパイラがコンテキストからデータ型を推論するため、省略することができる、すなわち "type inference")

Runnable r1 = () -> System.out.println("Hello world")
// Equivalent to
Runnable r2 = new Runnable() {
    public void run(){ 
        System.out.println("Hello world"); 
    } 
}


注:ラムダ式の引数の型は、コンパイラが推論するものです。このプログラムはラムダ式で型を指定せずにコンパイルされますが、これはjavacがプログラムの文脈からバックグラウンドで引数の型を推論しているためです。Lambda式の型はコンテキストに依存し、コンパイラによって推論される。これは、型推論と呼ばれています。

II. ラムダの発現構造?

ラムダ式は、0個、1個、またはそれ以上の引数を持つことができます。
引数の型は明示的に宣言することもできるし、コンパイラが文脈から自動的に推論することもできる。例えば、(int a)はさっきの(a)と同じです。
引数は括弧で囲み、カンマで区切ります。例えば、(a, b) または (int a, int b) または (String a, int b, float c) のようになります。
空白の括弧は、引数の集合が空であることを示すために使用されます。例えば、() -> 42のような感じです。
引数が1つだけの場合、明示的に型を指定しなければ、括弧は不要である。例えば、a -> return a*a のように。
ラムダ式の本体は、0個、1個、またはそれ以上のステートメントを含むことができます。
Lambda 式の本体が 1 つのステートメントのみを含む場合、中括弧は省略可能であり、式の戻り値の型は匿名関数の戻り値の型と同じである必要があります。
ラムダ式の本体が複数のステートメントを持つ場合は、中括弧(コードのブロック)で囲み、式の戻り値の型は無名関数の戻り値の型と同じにする必要があります。

III. 機能的なインターフェイス

Lambdaといえば、関数型インターフェースを理解することが重要です。Lambdaを使うためには、関数型インターフェースで使う必要があるからです。
関数型インターフェースとは、抽象メソッドを1つだけ持つが、デフォルトメソッドを複数持つことができ、暗黙のうちにLambda式に変換できるインターフェースである。関数型インターフェースには通常 @FunctionalInterface アノテーションがあり、@Override と同様にコンパイラに関数型インターフェースであることを伝え、コンパイル時にインターフェースが抽象メソッドをひとつだけ持っているか、もし複数持っていればコンパイルされないかをチェックするために使用されます。

3.1. 関数型インターフェイスでのラムダ式の使用

関数型インターフェースは、暗黙のうちにラムダ式に変換することができます。
次のような例です。

java.util.function

Threadの構造を見ることができます。

IntUnaryOperator addOneShort = (x) -> (x + 1);
IntUnaryOperator addOneLong = (x) -> { return (x + 1); }


入力が Runnable 型のインタフェースである場合、Runnable インタフェースを続行します。

IntUnaryOperator makeAdder(int amount) {
    return (x) -> (x + amount); // Legal even though amount will go out of scope
                                // because amount is not modified
}

IntUnaryOperator makeAccumulator(int value) {
    return (x) -> { value += x; return value; }; // Will not compile
}


jdk1.8 を見ればわかるように、Runnable は機能的なインターフェイスです。

Read more in Closures with lambda expressions.

run() メソッドのシグネチャ:引数リストが空、void を返す;ラムダのシグネチャ。() -> void 引数リストが空、voidを返す Runnableのrunメソッドのシグネチャがラムダのシグネチャと一致していることがわかります。このメソッドの抽象記述を関数記述子と呼びます。

3.2. java 8 では、様々な Lambda 式のシグネチャを記述するために使用できる多くの関数インターフェイスが提供されています。

<テーブル 機能的なインターフェイス 関数記述子 述語 T->ブーリアン 消費者 T->void(ボイド 関数<T,R> T->R サプライヤー ()->T UnaryOperator T->T BiPredicate<L,R> (L,R)->ブール値 BiConsumer<T,U> (T,U)->ボイド BiFunction<T,U,R> (T,U)->R

これらはより一般的に使われる機能インターフェイスであり、他にも多くの機能インターフェイスが public void passMeALambda(Foo1 f) { f.bar(); } passMeALambda(() -> System.out.println("Lambda called")); パッケージに含まれているので、興味のある方はご自身で調べてみてください。

3.3. 暗黙の戻り値

ラムダに置かれるコードが宣言ではなくjava式である場合、その式の値を返すメソッドとして扱われるので、以下の二つは等価です。

java.util.

3.4. ローカル変数へのアクセス (バリュークロージャ)

ラムダは匿名内部クラスの簡略化された書き方なので、閉領域の局所変数にアクセスするのと同じ規則に従います。変数はfinalとして扱われなければならず、ラムダ式で修正することはできません。

Java SE 1.2
Collections.sort(
    personList,
    new Comparator
() {
        public int compare(Person p1, Person p2){
            return p1.getFirstName().compareTo(p2.getFirstName());
        }
    }
);


このようにミュータブル変数を含める必要がある場合、この変数のコピーを含むリーガルオブジェクトを使用する必要があります。

Collections.sort(
    personList, 
    (p1, p2) -> p1.getFirstName().compareTo(p2.getFirstName())
);

3.5. ラムダを受け取る

ラムダはインターフェースの実装なので、メソッドにラムダを受け取らせるために特別なことをする必要はありません:関数インターフェースであれば、どんな関数でもラムダを受け取ることができます。

Comparator.comparing




3.6. ラムダ式によるコレクションのソート

java8以前では、コレクションをソートする場合、匿名(または名前付き)クラスを実装し、そのクラスで method references インタフェースが必要である。

Collections.sort(
    personList,
    Comparator.comparing(Person::getFirstName)
);


java 8以降、無名内部クラスはラムダ式に置き換えることができます。なお、p1とp2の引数はコンパイラが自動的に推論するため、無視できます。

import static java.util.Collections.sort;
import static java.util.Comparator.comparing;
//...
sort(personList, comparing(Person::getFirstName));


この例を簡略化すると

sort(personList, comparing(Person::getFirstName).thenComparing(Person::getLastName));

そして class Person { private final String name; private final String surname; public Person(String name, String surname){ this.name = name; this.surname = surname; } public String getName(){ return name; } public String getSurname(){ return surname; } } List people = getSomePeople(); people.stream().map(Person::getName) (メソッド参照)で、:.で表されます。(ダブルコロン)記法で表現されます。

people.stream().map(person -> person.getName())


静的インポートにより、より簡潔に表現できるようになったが、全体の可読性が向上するかどうかは議論がある。

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


コンパレータは、連鎖的に呼び出せるように作られている。例えば、名前で比較した後、同じ名前の人がいれば、Comparingメソッドは性別に基づいて比較を続けます。

people.forEach(person -> System.out.println(person));


IV. メソッドリファレンス

メソッド参照により、定義済みの静的メソッドやインスタンスメソッドを適切な機能インターフェイスにバインドし、無名のラムダ式を使用せずに引数として渡せるようになります。
参照型は

静的メソッドの参照。クラス名::メソッド名
インスタンスのインスタンスメソッド参照::instanceReference::methodName
スーパークラスでのインスタンスメソッド参照:super::methodName
型のインスタンスメソッド参照。クラス名::メソッド名
メソッドのリファレンスを構築します。クラス::new
配列のコンストラクタのリファレンスです。TypeName[]::new

モデルがあるとします。

List
 numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
numbers.stream().map(String::valueOf)


4.1. インスタンスメソッドの参照(任意のインスタンスに対して)

numbers.stream().map(num -> String.valueOf(num))

List
 strings = Arrays.asList("1", "2", "3");
strings.stream().map(Integer::new)


等価なラムダ。

Collect Elements of a Stream into a Collection

この例では、Person クラスのインスタンスの getName() メソッドに対するメソッドリファレンスが渡されています。コレクション型として扱われるため、インスタンス上のメソッド(後で気付きます)が呼び出されます。

4.2. インスタンスメソッドの参照 (特定のタイプ用)

Collect Elements of a Stream into a Collection

// Old way
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello world");
    }
}).start();

// New way
new Thread(
    () -> System.out.println("Hello world")
).start();


System.outはPrintStreamのインスタンスなので、この特定のインスタンスへのメソッド参照が引数として渡されます。等価なラムダ式

ActionListener

4.3. 静的メソッドの参照

変換ストリームでは、静的メソッド参照を使用できます。

// Old way
button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("Hello world");
    }
});

// New way
button.addActionListener( (e) -> {
        System.out.println("Hello world");
});


この例では、String型のvalueOf()に静的メソッドの参照を渡しているので、valueOf()
は、コレクション内のインスタンスオブジェクトのパラメータとして渡されます。等価なラムダです。

// old way
List
 list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
for (Integer n : list) {
    System.out.println(n);
}

// Lambda expressions using ->
list.forEach(n -> System.out.println(n));

// Lambda expressions that use ::
list.forEach(System.out::println);

package com.wuxianjiezh.demo.lambda;

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class Main {

    public static void main(String[] args) {
        List
 list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);

        System.out.print("Output all numbers: ");
        evaluate(list, (n) -> true);

        System.out.print("No output: ");
        evaluate(list, (n) -> false);

        System.out.print("output even: ");
        evaluate(list, (n) -> n % 2 == 0);

        System.out.print("Output odd: ");
        evaluate(list, (n) -> n % 2 == 1);

        System.out.print("Output numbers greater than 5: ");
        evaluate(list, (n) -> n > 5);
    }

    public static void evaluate(List
 list, Predicate
 predicate) {
        for (Integer n : list) {
            if (predicate.test(n)) {
                System.out.print(n + " ");
            }
        }
        System.out.println();
    }
}



4.4. コンストラクタのリファレンス

List
 strings = Arrays.asList("1", "2", "3");
strings.stream().map(Integer::new)


読む Collect Elements of a Stream into a Collection 要素をコレクションに集める方法を参照してください。Integerの唯一のString引数コンストラクタは、ここでは、文字列が数字を表す限り、引数として与えられたStringから整数を構成し、ストリームは整数に変換されます。等価なラムダです。

Collect Elements of a Stream into a Collection


V. 事例紹介

5.1. スレッドの初期化

// Old way
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello world");
    }
}).start();

// New way
new Thread(
    () -> System.out.println("Hello world")
).start();


5.2. イベントハンドリング

Java 8 では、Lambda 式を使用してイベントハンドリングを行うことができます。以下のコードでは ActionListener を新旧の方法でUIコンポーネントに追加しました。

// Old way
button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("Hello world");
    }
});

// New way
button.addActionListener( (e) -> {
        System.out.println("Hello world");
});


5.3. 反復出力(メソッドリファレンス)

与えられた配列の全要素を出力するシンプルなコードです。なお、Lambda式を利用する方法もあります。

// old way
List
 list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
for (Integer n : list) {
    System.out.println(n);
}

// Lambda expressions using ->
list.forEach(n -> System.out.println(n));

// Lambda expressions that use ::
list.forEach(System.out::println);


5.4. 論理演算

package com.wuxianjiezh.demo.lambda;

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class Main {

    public static void main(String[] args) {
        List
 list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);

        System.out.print("Output all numbers: ");
        evaluate(list, (n) -> true);

        System.out.print("No output: ");
        evaluate(list, (n) -> false);

        System.out.print("output even: ");
        evaluate(list, (n) -> n % 2 == 0);

        System.out.print("Output odd: ");
        evaluate(list, (n) -> n % 2 == 1);

        System.out.print("Output numbers greater than 5: ");
        evaluate(list, (n) -> n > 5);
    }

    public static void evaluate(List
 list, Predicate
 predicate) {
        for (Integer n : list) {
            if (predicate.test(n)) {
                System.out.print(n + " ");
            }
        }
        System.out.println();
    }
}



結果を実行します。

<ブロッククオート

すべての数字を出力する。1 2 3 4 5 6 7
出力しません。
偶数を出力する。2 4 6
奇数を出力する。1 3 5 7
5より大きい数字を出力:6 7

VI. 概要

6.1. ラムダ式と匿名クラスの違い

thisキーワードです。匿名クラスの場合、this キーワードは匿名クラスに解決されますが、 ラムダ式の場合、this キーワードは記述されたラムダを含むクラスに解決されます。
コンパイルされる方法 JavaコンパイラはLambda式をコンパイルする際、クラスのプライベートメソッドに変換し、動的にバインドします。

6.2. 理論的なまとめ

<ブロッククオート

1. ラムダは、引数のリスト、矢印、本体から構成される。
2. 機能的インターフェースは、1つの抽象的メソッド、複数のデフォルトメソッド、複数の静的メソッドのみを持つことができます。
3. メソッド参照は、実はLambdaへのショートカットです。
4. ストリームは一度しかトラバースできない。トラバースした後、そのストリームは消費されたと言います。元のデータソースから新しいストリームを取得すれば、再び反復することができます。
5. 並列ストリームは ForkJoin を使って実装されています。
6. 6. 並列ストリームでは、peek,mapで外部データを変更しない。
7.並列ストリームは注意して使用する必要があります、推測に頼らず、もっとテストしてください。
8. インターフェイスのデフォルトメソッド、最も低い優先度、サブクラスはデフォルトメソッドを継承し、デフォルトメソッドをオーバーライドすることができます。多重継承の問題(サブクラスが2つのインターフェースを実装し、両方のインターフェースが同じメソッド名、同じ関数記述子を持つ)で競合が発生した場合、そのメソッドをオーバーライドする必要があります。もし、あるインターフェースのデフォルトメソッドが呼ばれることが予想される場合は、X.super.m(...)を使って、どのインターフェースのデフォルトメソッドが呼ばれるかを表示することができます。
9. サブクラスに継承されず、オーバーライドもできないインターフェースの静的メソッドは、同じ名前と同じ戻り値の共通メソッドまたは静的メソッドを定義することができます。