ラムダ式とは
Java8で導入された新しい文法。
ローカルクラスと無名クラスを利用して記述する内容を省略して処理記述を簡潔にする。
代表的な例としてCollections.sortやStreamAPIのメソッドが恩恵を受けた。
ラムダ式について分解
ラムダ式はローカルクラスと無名クラスという仕組みを利用して記述を省略しています。
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!
}
次に無名クラスを見てみましょう。
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!
}
あたかもRannableインターフェースのインスタンスを生成しているように見えると思いますが、実際にはRannableインターフェースを実装した無名クラスのインスタンスを生成しています。
最後にラムダ式を見てみましょう。
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 void run」を省略すると、複数メソッドが定義されているインターフェースの場合、どのメソッドをオーバーライドするのかわかりません。
そのため、ラムダ式で使用できるのは抽象メソッドが一つのインターフェースのみとなります。
Rannableインターフェースだけでは引数なし、戻り値なしのラムダ式しか作れません。
他の形で作成したい場合は、関数型インターフェースというものが追加されたのでそちらを利用します。
関数型インターフェース
関数型インターフェースは、ラムダ式やメソッド参照の代入先になれるインターフェースのこと。
関数型インターフェースの条件は、大雑把に言って、定義されている抽象メソッドが1つだけあるインターフェース。
staticメソッドやデフォルトメソッドは含まれていても構わない(関数型インターフェースの条件としては無視される)。
また、Objectクラスにあるpublicメソッドがインターフェース内に抽象メソッドとして定義されている場合、そのメソッドも無視される。
(この条件を満たすインターフェースを、JDK1.8で「関数型インターフェース」と呼ぶようになった)
SE8から新しくjava.util.functionパッケージ配下にインターフェースがたくさん追加されました。
https://docs.oracle.com/javase/jp/8/docs/api/java/util/function/package-summary.html
今回はその中でもよく使うインターフェースを紹介します。
2-1. Function<T, R>
Functionは、値を変換する為の関数型インターフェース。
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-2. Consumer<T>
Consumerは、引数を受け取り、それを使って処理を行う為の関数型インターフェース。
Consumer<T>のTはメソッドの引数の型を指定します。
値を返さないので、基本的に副作用を起こす目的で使用する。
メソッドは void accept(T) です。
Consumer<String> buyer = (goods) -> { System.out.println(goods + "を購入しました"); };
buyer.accept("おにぎり"); // おにぎりを購入しました。
2-3. Predicate<T>
Predicateは判定を行う為の関数型インターフェース。
Predicate<T>のTはメソッドの引数の型を指定します。
引数を受け取り、判定を行い、真偽値(判定結果)を返す。
メソッドは boolean test(T) です。
Predicate<String> checker = (s)-> { return s.equals("Java"); };
boolean result = checker.test("Java");
System.out.println(result); //true
ラムダ式を利用する意図
次のコードは、Stream APIをラムダ式を用いて記述したものです。
List<Integer> numbers = List.of(3, 1, -4, 1, -5, 9, -2, 6, 5, 3, 5);
numbers.stream()
.filter(number -> Math.abs(number) >= 5)
.forEach(System.out::println);
出力結果は次のようになります。
-5
9
6
5
5
次にラムダ式を利用しないで記述したコードを見てみます。
List<Integer> numbers = List.of(3, 1, -4, 1, -5, 9, -2, 6, 5, 3, 5);
numbers.stream()
.filter(new Predicate<Integer>() {
@Override
public boolean test(Integer number) {
return Math.abs(number) >= 5;
}
})
.forEach(new Consumer<Integer>() {
@Override
public void accept(Integer number) {
System.out.println(number);
}
});
ラムダ式の分解で紹介したようにインターフェースの生成、実行するメソッド記述が増えているため、
コードの記述量が多くなり処理の見通しがとても悪くなりました。
このように処理記述を簡潔でわかりやすくするために利用します。
ラムダ式の文法
ラムダ式の文法を説明します。以下がラムダ式の基本文法です。
(引数) -> { 処理; }
この文法に従って書いたのが以下になります。
// (1) 引数と戻り値がある場合
(Integer number) -> {
return Math.abs(number) >= 5;
}
// (2) 戻り値がない場合
(Integer number) -> {
System.out.println(number);
}
// (3) 引数も戻り値もない場合
() -> {
System.out.println("Hello!");
}
(1)は、Predicateのように引数と戻り値がある例です。引数で指定されたnumberを用いて処理を行い、戻り値をreturnしています。
(2)は、Consumerのように戻り値がない例です。その場合はreturn文を書く必要はありません。
(3)のように、引数がない処理は引数部分を( )で記載します。java.lang.Runnableなどがこれに該当します。
また、ラムダ式では引数の型を省略することができます。
そして、引数が1つしかない場合に限り、引数を囲む小括弧( )を省略することができます。引数がない場合や、2つ以上ある場合は省略できません。このルールを(1)と(3)に当てはめると、次のようになります。
// (1) 引数が1つなので ( ) を省略できる
number -> {
return Math.abs(number) >= 5;
}
// (3) 引数がないため ( ) を省略できない
() -> {
System.out.println("Hello!");
}
さらに、処理が1行しかない場合は、中括弧{ }と、returnと、文末のセミコロンを省略することもできます。(1)~(3)について省略した形で記述すると次のようになります。
// (1) 引数と戻り値がある場合
number -> Math.abs(number) >= 5
// (2) 戻り値がない場合
number -> System.out.println(number)
// (3) 引数も戻り値もない場合
() -> System.out.println("Hello!")
最後に、処理内容がメソッド呼び出し1つの場合、かつ、引数が一意に決まる場合に限り、メソッド参照を利用して、引数そのものを省略することができます。メソッド参照は次のような文法になります。
クラス名::メソッド名
このメソッド参照を適用できるのは(2)だけとなります。(2)をメソッド参照を用いて記載すると次のようになります。
System.out::println
System.out.printlnメソッドは引数を1つだけ取るメソッドであり、引数であるIntegerの値が渡されることが明らかであるため、メソッド参照が利用できるのです。一方、(1)はメソッド呼び出しの後に>= 5という大小判定があるため、メソッド参照が使えません。また、(3)は引数に"Hello!"という値を指定しているため引数が一意には決まるとは言えず、これもメソッド参照は使えません。
これを踏まえた上で先ほどのstream処理を見てみましょう
List<Integer> numbers = List.of(3, 1, -4, 1, -5, 9, -2, 6, 5, 3, 5);
numbers.stream()
.filter(number -> Math.abs(number) >= 5)
.forEach(System.out::println);
streamAPIの中に(1),(2)で学習したラムダ式が組み込まれていることがわかります。
※streamAPIについての記事は以下です。
StreamAPIについてまとめてみた
参考記事
Java8のラムダ式を理解する
ラムダ式とStream APIで学ぶモダンJava ― 関数型を取り入れて変化するJava言語の現在