経緯
Javaでラムダ式をなんとなく使用してきた(Stream()等)が、詳しく中身がどうなっているのかを理解せずに使用していた。
ラムダ式と検索すると「匿名関数」とか「メソッドを変数のように使用できる」とかなんだか小難しそうな説明ばかりでよくわからん。
なので今回はラムダ式を分解してみます。
ラムダ式とは
まずはラムダ式とはを検索してみた。
ラムダ式とは、プログラミング言語に用意された記法の一つで、名前の無い無名関数(匿名関数)を簡潔に定義するもの。
いやわからん。
とりあえず一番わかりやすかったのがこれ。ひとまずこの程度の理解で進んでいくことにした。
ラムダ式とはクラス定義とインスタンス生成をお手軽にやる記法。
関数型インターフェイスを簡潔に実装するための記法。
関数型インターフェイスとは
関数型インターフェイスとは抽象メソッドを一つだけ持つインターフェイスのこと。
この一つだけがめちゃくちゃ重要になってきます。
一旦完成形
以下の関数型インターフェイスを用いてラムダ式を使用するVerと使用しないVerで比べてみる。
public interface SampleSum {
public abstract int sum(int x, int y);
}
ラムダ式を使用しない場合
関数型インターフェイスを実装したクラスを作成
public class SampleSumImpl implements SampleSum {
@Override
public int sum(int x, int y) {
return x + y;
}
}
↑で作成したクラスを使用して計算。
public class Main {
public static void main(String[] args) {
SampleSumImpl sampleSumImpl = new SampleSumImpl();
System.out.println(sampleSumImpl.sum(10,5));
}
}
// 15
ラムダ式を使用した場合
なんとこの2行。
関数型インターフェイスを実装したクラスをnewする必要もクラスを定義する必要もありません。
冒頭で説明した、クラス定義とインスタンス生成を同時にやってくれます。
public class Main {
public static void main(String[] args) {
SampleSum sampleSum = (x, y) -> x + y;
System.out.println(sampleSum.sum(10,5));
}
}
// 15
ラムダ式が出来上がるまで
ラムダ式は名無しのクラスを定義しそのクラスのインスタンスを生成するものです。
どうしてこのようなことが可能なのかを順を追って解体していきたいと思います。
冒頭でも説明した関数型インターフェイスは抽象メソッドを1つだけ持つがとっても重要になってくるので注意して見てみてください。
ラムダ式になるまでには大きく2段階の手順を踏みます。
1. 普通クラス→匿名クラス
2. 匿名クラス→ラムダ式
普通クラス→匿名クラスになるまで
普通クラス→内部クラス→ローカルクラス→匿名クラス
冒頭でも登場したインターフェイスをもとに解説していきます。
public interface SampleSum {
public abstract int sum(int x, int y);
}
普通クラス
public class SampleSumImpl implements SampleSum {
@Override
public int sum(int x, int y) {
return x + y;
}
}
public class Main {
public static void main(String[] args) {
SampleSumImpl sampleSumImpl = new SampleSumImpl();
System.out.println(sampleSumImpl.sum(10,5));
}
}
内部クラス
普通クラスでは別のjavaファイルだったのを一つのファイルにまとめます。
public class Main {
public static void main(String[] args) {
SampleSumImpl sampleSumImpl = new SampleSumImpl();
System.out.println(sampleSumImpl.sum(10,5));
}
class SampleSumImpl implements SampleSum {
@Override
public int sum(int x, int y) {
return x + y;
}
}
}
ローカルクラス
あまり使ったことはありませんがメソッドの中にクラスを定義します。
public class Main {
public static void main(String[] args) {
class SampleSumImpl implements SampleSum {
@Override
public int sum(int x, int y) {
return x + y;
}
}
SampleSumImpl sampleSumImpl = new SampleSumImpl();
System.out.println(sampleSumImpl.sum(10,5));
}
}
匿名クラス
ついに最終形態匿名クラスになります。
クラスの宣言やnewが一行で記述できるようになりました。
クラス名はJavaのコンパイラが勝手に命名してくれるそう。
public class Main {
public static void main(String[] args) {
SampleSum sampleSum = new SampleSum() {
@Override
public int sum(int x, int y) {
return x + y;
}
};
System.out.println(sampleSum.sum(10,5));
}
}
匿名クラス→ラムダ式になるまで
さて普通クラスは無事匿名クラスとなりました。
この時点で結構簡潔に書けてはいますがラムダ式はまだまだ削ぎ落とします。
最終形態になるまでコンパイルは通りませんのでご注意ください。
SampleSum sampleSum = new SampleSum() {
public int sum(int x, int y) {
return x + y;
}
};
まずはインターフェイス名を除去。
SampleSum sampleSum = new {
public int sum(int x, int y) {
return x + y;
}
};
代入演算子の左辺と右辺で同じ型を記述する必要は無いからです。
次に抽象メソッドのアクセス修飾子を削除
SampleSum sampleSum = new {
int sum(int x, int y) {
return x + y;
}
};
javaではインターフェイスの抽象メソッドは例外なくpublicらしいです。
次に抽象メソッドの戻り値の型を削除
SampleSum sampleSum = new {
sum(int x, int y) {
return x + y;
}
};
SampleSumインターフェイスには抽象メソッドはint sum(int x,int y)の一つしかないため、戻り値は必ずintと判定できるからです。
この辺りから関数型インターフェイスの抽象メソッドが一つだけというのが効いてきます。
次に抽象メソッドのメソッド名を削除
SampleSum sampleSum = new {
(int x, int y) {
return x + y;
}
};
SampleSumの抽象メソッドはsumの一つしか無いためです。
次に抽象メソッドの引数の型を削除
SampleSum sampleSum = new {
(x, y) {
return x + y;
}
};
これもまたSampleSumの抽象メソッドはsumの一つしか無いためです。
次に抽象メソッドのreturnを削除
SampleSum sampleSum = new {
(x, y) {
x + y;
}
};
この抽象メソッドの戻り値はintです。returnしているだけなので消しちゃいましょ。
次に抽象メソッドの{}を削除
SampleSum sampleSum = new {
(x, y)
x + y;
};
どうせ中身はreturnする1行だけなので消してしまいます。
次に匿名クラスの{}を削除
SampleSum sampleSum = new
(x, y)
x + y;
;
このクラスは抽象メソッドの一つだけを記述しているだけでフィールド等もありませんのでクラスの{}も消してしまいましょう。
次にnewを削除
SampleSum sampleSum =
(x, y)
x + y;
;
もうここまで来るとnewが邪魔です。
いよいよラムダ式の完成
public class Main {
public static void main(String[] args) {
SampleSum sampleSum = (x, y) -> x + y;
System.out.println(sampleSum.sum(10,5));
}
}
最後に残ったのは、抽象メソッドの引数名と抽象メソッドの中身だけです。
最後にラムダ式だとわかるように「->」記号を間に入れて上げれば完成です。
関数型インターフェイスの重要性
関数型インターフェイスは抽象メソッドが一つだけなので戻り値,引数などをJavaコンパイラが関数型インターフェイスの方から推測することが可能です。
この仕組みを型推論といいます。
型推論と関数型インターフェイスのおかげでラムダ式が成り立っているということです。
少しだけ応用
関数型インターフェイスの型がわかればラムダ式の記述が可能ということはもうおわかりだと思います。
すなわち、メソッドの引数や戻り値にも関数型インターフェイスを活用することが可能です。
public class Main {
public static void main(String[] args) {
System.out.println(lambdaSample((x,y) -> x + y));
}
private static int lambdaSample(SampleSum sampleSum) {
return sampleSum.sum(10,5);
}
}
public class Main {
public static void main(String[] args) {
SampleSum sampleSum = getSampleSum();
System.out.println(sampleSum.sum(10,5));
}
private static SampleSum getSampleSum() {
return (x,y) -> x + y;
}
}
さいごに
今回はラムダ式を分解して詳細を見てみました。
一年前にはわからなかったが、結構スラスラ理解できたのが印象的でした。
成長しているのだな〜〜〜。。多分
ではまた!!