ラムダ式とはこんなやつです。
Runnable runner = () -> { System.out.println("Hello Lambda!"); };
見慣れてないとギョッとするかもしれませんが、実は既存のJavaの機能を利用して構成されています。
ここではラムダ式を分解して成り立ちから理解してみましょう。
ラムダ式の省略についてはこちら。
【Java】ラムダ式の省略まとめ
1. ラムダ式を分解してみる
ラムダ式はローカルクラスと無名クラスという仕組みを利用しています。
1-1. ローカルクラス
ローカルクラスとはメソッドの処理中でクラスを宣言して利用できる仕組みです。
public static void main(String[] args) {
class Local {
public void sayHello() {
System.out.println("Hello!");
}
}
Local local = new Local();
local.sayHello(); // Hello!
}
インターフェースを実装したローカルクラスを定義することもできます。
public static void main(String[] args) {
class Local implements Runnable {
@Override
public void run() {
System.out.println("Hello Lambda!");
}
}
Runnable runner = new Local();
runner.run(); // Hello Lambda!
}
次に無名クラスを見てみましょう。
1-2. 無名クラス
無名クラスとはインターフェースを実装したローカルクラスの宣言を省略する仕組みです。
Runnableインターフェースを実装した無名クラスの例を記述します。
public static void main(String[] args) {
Runnable runner = new Runnable() {
@Override
public void run() {
System.out.println("Hello Lambda!");
}
};
runner.run(); //Hello Lambda!
}
あたかもRunnableインターフェースのインスタンスを生成しているように見えると思いますが、実際にはRunnableインターフェースを実装した無名クラスのインスタンスを生成しています。
最後にラムダ式を見てみましょう。
1-3. ラムダ式
無名クラスから更に「new Runnable(){}」と「public void run」を省略してラムダ式となります。
public static void main(String[] args) {
Runnable runner = () -> { System.out.println("Hello Lambda!"); };
runner.run(); //Hello Lambda!
}
最初の()はrunメソッドの引数を表し、->{}の中身はrunメソッドの実装内容になります。
runner変数にはRunnableを実装した無名クラスのインスタンスが代入されます。
つまり、ラムダ式とはインターフェースを実装したインスタンスを生成する式といえます。
ところで「new Runnable(){}」を省略したら、何型のインスタンスを生成するのかわかりません。
Javaでは代入される変数の型によって自動的に推論する仕組みになっています。
この仕組みを型推論と呼びます。
引数にインターフェースを指定して、ラムダ式を受け取ることができるメソッドを定義することができます。
public static void main(String[] args) {
method(()->{System.out.println("Hello Lambda!");});
}
public static void method(Runnable r) {
r.run();
}
// Hello Lambda!
これも引数の型から型推論して、自動的にRunnable型のインスタンスが生成されています。
また、「public void run」を省略すると、複数メソッドが定義されているインターフェースの場合、どのメソッドをオーバーライドするのかわかりません。
そのため、ラムダ式で使用できるのは抽象メソッドが一つのインターフェースのみとなります。
Runnableインターフェースだけでは引数なし、戻り値なしのラムダ式しか作れません。
他の形で作成したい場合は、関数型インターフェースというものが追加されたのでそちらを利用します。
2. 関数型インターフェース
SE8から新しくjava.util.functionパッケージ以下にインターフェースがたくさん追加されました。
これらは関数型インターフェースと呼ばれています。
https://docs.oracle.com/javase/jp/8/docs/api/java/util/function/package-summary.html
関数型インターフェースは単一のメソッドをもつインターフェース群で、ラムダ式で利用するのに非常に都合がいいです。
この中から使用頻度の高いFunction、Consumer、Predicateを紹介します。
2-1. Function<T, R>
Function<T, R>のTはメソッドの引数の型、Rは戻り値の型を指定します。
メソッドは R apply(T) です。
Function<Integer, String> asterisker = (i) -> { return "*"+ i; };
String result = asterisker.apply(10);
System.out.println(result); // *10
2つの引数を受け付けるBiFunctionというインターフェースもあります。
BiFunction<Integer, Integer, Integer> adder = (a, b) -> { return a + b; };
int result = adder.apply(1, 2);
System.out.println(result); // 3
2-2. Consumer<T>
Consumer<T>のTはメソッドの引数の型を指定します。
メソッドは void accept(T) です。
Consumer<String> buyer = (goods) -> { System.out.println(goods + "を購入しました"); };
buyer.accept("おにぎり"); // おにぎりを購入しました。
紹介は割愛しますが、引数2つ、戻り値なしのBiConsumerインターフェースというのもあります。
2-3. Predicate<T>
Predicate<T>のTはメソッドの引数の型を指定します。
メソッドは boolean test(T) です。
Predicate<String> checker = (s)-> { return s.equals("Java"); };
boolean result = checker.test("Java");
System.out.println(result); //true
3. ラムダ式の使い方
そもそもなぜラムダ式が生まれたのでしょうか。
それは値ではなく処理を引数に渡すことで、動的に処理を変更したかったためです。
代表的な例としてCollections.sortやStreamAPIのメソッド群があります。
3-1. Collections.sort(List<T>, Comparator super T>)
ソートの方法はオブジェクトの種類や状況によって異なります。
例えば数値を並べ替える場合でも、単純な昇順だったり、絶対値の昇順だったりします。
どちらのソートも実現するためには数値の比較処理を動的に切り替える必要があります。
そのため、Collections.sortは比較の処理自体を受け取る仕組みになっています。
int[] numbers = {-1, 2, 0, -3, 8};
List<Integer> numbersList = new ArrayList<>();
for(int n : numbers) {
numbersList.add(n);
}
Collections.sort(numbersList, 【ソート方法】);
【ソート方法】にはComparatorインターフェースのcompare(s1, s2)メソッドを実装したインスタンスを指定します。
compareメソッドはint型を返し、大小を以下のように判断します。
戻り値 | 大小 |
---|---|
0より大きい | s1 > s2 |
0 | s1 = s2 |
0より小さい | s1 < s2 |
実際にラムダ式を用いてソートしてみましょう。
Collections.sort(numbersList, (a, b) -> { return a - b; });
for(Integer n : numbersList) {
System.out.print(n + " ");
}
// -3 -1 0 2 8
ラムダ式の内容を変えることでソート方法を変更することができます。
降順にしてみましょう。
Collections.sort(numbersList, (a, b) -> { return b - a; });
for(Integer n : numbersList) {
System.out.print(n + " ");
}
// 8 2 0 -1 -3
絶対値の順にしてみましょう。
Collections.sort(numbersList, (a, b) -> { return a*a - b*b; });
for(Integer n : numbersList) {
System.out.print(n + " ");
}
// 0 -1 2 -3 8
もちろん、Comparatorを実装した通常クラスのインスタンスを渡しても正常に動作します。
しかし、ラムダ式を使うことで処理そのものを渡しているように見えるので、より簡潔に直感的な記述ができるようになります。
3-2.StreamAPI
Collectionインターフェースにstreamというメソッドが追加されました。
streamメソッドは自分自身のStreamインスタンスを返します。
Streamには関数型インターフェースを引数にとる便利なメソッドがたくさん定義されています。
void forEach(Consumer<T>)
forEachメソッドはConsumerを引数に取り、要素の数だけ処理を繰り返します。
int[] numbers = {-1, 2, 0, -3, 8};
List<Integer> numbersList = new ArrayList<>();
for(int n : numbers) {
numbersList.add(n);
}
numbersList.stream().forEach((i) -> { System.out.print(i + " "); });
// -1 2 0 -3 8
Stream filter(Predicate<T>)
filterメソッドはPredicateを引数に取り、条件に合致しないものを除いたStreamを返します。
返ってくるのもStreamなのでそのままForEachメソッドを呼び出すことができます。
numbersList.stream().filter((i) -> { return i > 0; })
.forEach((i) -> { System.out.print(i + " "); });
// 2 8
filterメソッドは戻り値がStream型なので続けてforEachを呼び出すことができます。
そのため、filterのようなメソッドは中間操作と呼ばれています。
対してforEachのようなメソッドを終端操作と呼びます。
Stream map(Function<T, R>)
mapメソッドはFunctionを引数に取り、処理後の結果をStreamにして返します。
filterと同じく中間操作です。
numbersList.stream().filter((i) -> { return i >= 0; })
.map((i) -> { return "*" + i + "*"; })
.forEach((s) -> { System.out.print(s + " "); });
// *2* *0* *8*
Stream sorted(Comparator<T>)
java.utilパッケージですが、Comparatorを引数に取るsortedメソッドも用意されています。
filterと同じく中間操作です。
numbersList.stream().filter((i) -> { return i >= 0; })
.sorted((i1, i2) -> { return i1 - i2; })
.map((i) -> { return "*" + i + "*"; })
.forEach((s) -> { System.out.print(s + " "); });
// *0* *2* *8*
以上です。
自分が見たラムダ式と違うという場合は省略パターンについて検索してみてください。