ラムダ式とは
ラムダ式とはjava8から使用できるようになったコーディングの文法のことである。
関数型インターフェイスを実装したクラスのインスタンスを短い記述で作成することが可能になっている。
関数型インターフェイスとは
関数型インターフェイスとは抽象メソッドを1つだけ持つインターフェイスのことである。
抽象メソッドを1つだけ持つという特徴がラムダ式での記述を可能にしている。(型推論)
関数型インターフェイスは標準APIでいくつか用意されているが自作することもできる。
詳しくは後半で記載する。
ラムダ式の構文
まずはラムダ式で書いたコードと書いていないコードを見て、どのような構文か確認していく。
public interface InterfaceSample {
public String method(String str);
}
public class Sample {
public static void main(String[] args) {
InterfaceSample ifs = new InterfaceSample() {
public String method(String str) {
return str;
}
};
System.out.println(ifs.method("HelloWorld"));
}
}
上記は匿名クラスによる記述である。
InterfaceSampleのmethod()を引数の文字列を戻り値として返す処理として匿名クラスで実装している。
そして戻り値を出力しているだけのシンプルなプログラムである。
また、InterfaceSampleは抽象メソッドのmethod()を1つだけ持っているので関数型インターフェイスと言える。
public interface InterfaceSample {
public String method(String str);
}
public class Sample {
public static void main(String[] args) {
InterfaceSample ifs = str -> str;
System.out.println(ifs.method("HelloWorld"));
}
}
こちらがラムダ式で書いたプログラム。処理内容は先ほどと一緒で、引数に入れた文字列を戻り値として出力している。
同じ処理だが、2つを比較すると、ラムダ式で書いた方がコードの記述量が少なくすることができ、読みやすい。
しかし、ぱっと見ただけではわからないのでどのような仕組みなのかを以降で説明していく。
ラムダ式の成り立ち
ラムダ式がどのようにして成り立っているのかを匿名クラスをラムダ式に変化させていきながら説明していく。
以下は匿名クラスで書いた場合。先ほどのラムダじゃないよ.javaと一緒。
public interface InterfaceSample {
public String method(String str);
}
public class Sample {
public static void main(String[] args) {
InterfaceSample ifs = new InterfaceSample() {
public String method(String str) {
return str;
}
};
System.out.println(ifs.method("HelloWorld"));
}
}
ここからいろんな部分を削っていったのがラムダ式の構文になる。
記述を省略できるのは、関数型インターフェイスには抽象メソッドが1つだけなので、戻り値や引数の型と順番を関数型インターフェイスの型からJavaコンパイラが推測できるからである。
では、実際に匿名クラスをラムダ式にしていくため1つずつ不要な部分を削除していく。
①インターフェイス名を削除
代入演算子の右辺にあるインターフェイス名を削除
public class Sample {
public static void main(String[] args) {
InterfaceSample ifs = new {
public String method(String str) {
return str;
}
};
System.out.println(ifs.method("HelloWorld"));
}
}
代入演算子左辺の変数の型と右辺のインターフェイスを指定した部分が同じInterfaceSampleなので同じことを2回書く必要なない。
②抽象メソッドのアクセス修飾子を削除
メソッドのpublicを削除
public class Sample {
public static void main(String[] args) {
InterfaceSample ifs = new {
String method(String str) {
return str;
}
};
System.out.println(ifs.method("HelloWorld"));
}
}
インターフェイスの抽象メソッドは全てpublicなので、省略しても意味は変わらない。
③抽象メソッドの戻り値の型を削除
メソッドの戻り値の型を削除
public class Sample {
public static void main(String[] args) {
InterfaceSample ifs = new {
method(String str) {
return str;
}
};
System.out.println(ifs.method("HelloWorld"));
}
}
抽象メソッドは1つしかないので、戻り値の型は抽象メソッドの型以外ありえない。
そのため省略が可能になる。
④抽象メソッドのメソッド名を削除
メソッド名を削除
public class Sample {
public static void main(String[] args) {
InterfaceSample ifs = new {
(String str) {
return str;
}
};
System.out.println(ifs.method("HelloWorld"));
}
}
InterfaceSampleの抽象メソッドは1つしかなく、書かなくても推測可能であるため不要になる。
⑤抽象メソッドの引数の型を削除
メソッドの引数の型を削除
public class Sample {
public static void main(String[] args) {
InterfaceSample ifs = new {
(str) {
return str;
}
};
System.out.println(ifs.method("HelloWorld"));
}
}
こちらも抽象メソッドの型から推測できるので不要になる。
⑥抽象メソッドのreturnを削除
メソッドの中にあるreturnを削除
public class Sample {
public static void main(String[] args) {
InterfaceSample ifs = new {
(str) {
str;
}
};
System.out.println(ifs.method("HelloWorld"));
}
}
今回の抽象メソッドの戻り値はStringであるため、最終的にStringの戻り値を返すことが推測できるので省略可能。
⑦抽象メソッドの{}と;を削除
public class Sample {
public static void main(String[] args) {
InterfaceSample ifs = new {
(str)
str;
};
System.out.println(ifs.method("HelloWorld"));
}
}
抽象メソッドの処理は1行しかないので、複数行書くために必要な{}や;は不要になる。複数行だったら必要。
⑧匿名クラスの{}を削除
public class Sample {
public static void main(String[] args) {
InterfaceSample ifs = new
(str)
str;
;
System.out.println(ifs.method("HelloWorld"));
}
}
現在あるのは抽象メソッドの実装だけであり、フィールドやほかのメソッドはないため。{}は不要。
⑨newを削除
public class Sample {
public static void main(String[] args) {
InterfaceSample ifs =
(str)
str;
System.out.println(ifs.method("HelloWorld"));
}
}
匿名クラスの構文自体が、クラスから作成されたインスタンスを戻す構文なので、不要になる。
⑩ラムダ式の完成
public class Sample {
public static void main(String[] args) {
InterfaceSample ifs = (str) -> str;
System.out.println(ifs.method("HelloWorld"));
}
}
不要な部分を削除して最後に残るのは抽象メソッドの引数名と処理の中身の部分だけである。そこに「->」を追記してあげればラムダ式の完成である。引数の()は省略可能。
既に書いているがラムダ式が成り立つのは関数型インターフェイスが決まり事を守っているおかげで戻り値や引数の型や順番をJavaコンパイラが推測できるからである。
引数として使用する
関数型インターフェイスは変数の宣言以外だと引数にも使用することができる。
public class Sample {
public static void main(String[] args) {
lamdaMethod(str -> str);
}
static void lamdaMethod(InterfaceSample ifs) {
System.out.println(ifs.method("HelloWorld"));
}
}
lamdaMethod()の引数がInterfaceSample型になっており、このメソッドをmainメソッドで呼び出している。
変数の宣言をして引数に入れることももちろんできるが、関数型インターフェイスは抽象メソッドを1つだけ持つため、上記のように処理を直接引数に書くことができる。
JavaScriptとかでは普通にあるが、関数を引数に入れているようなイメージ。
Javaコンパイラのラムダ式の見方
Javaコンパイラがラムダ式を見つけたら、ラムダ式が書かれているところで扱われる「型」を最初に判断する。型の参照先は、変数の型、メソッドの引数の型、メソッドの戻り値の型。
Javaコンパイラが見つけた型が関数型インターフェイスなら、その抽象メソッドの引数の型や数、戻り値の型などを認識して、ラムダ式で書かれていることとの整合性をチェックしていく。
ラムダ式のさまざまな書き方
ラムダ式の基本的な書き方は「引数 -> 処理」であるが処理が複数行ある場合など、パターンによってさまざまな書き方ができる。
以下でいくつかのパターンをさまざまな書き方で表しているが全て同じ処理を行うものである。
// 引数なし、戻り値なしのパターン
Runnable runnable0 = new Runnable() {
public void run() {
System.out.println("テスト");
}
};
Runnable runnable1 = () -> System.out.println("テスト");
Runnable runnable2 = () -> {System.out.println("テスト");};
// 引数なし、戻り値ありのパターン
public static void main(String[] args) {
InterfaceSample ifs1= new InterfaceSample() {
public String method() {
return "テスト";
}
};
InterfaceSample ifs2 = () -> "テスト";
InterfaceSample ifs3 = () -> {return "テスト";};
}
// 引数1つ、戻り値ありのパターン
public static void main(String[] args) {
InterfaceSample ifs1= new InterfaceSample() {
public String method(String str) {
return str;
}
};
InterfaceSample ifs2 = (String str) -> str;
InterfaceSample ifs3 = (str) -> str;
InterfaceSample ifs4 = str -> str;
}
// 引数2つ、戻り値ありのパターン
public static void main(String[] args) {
InterfaceSample ifs1= new InterfaceSample() {
public String method(String str1, String str2) {
return str1 + str2;
}
};
InterfaceSample ifs2 = (String str1, String str2) -> {return str1 + str2;};
InterfaceSample ifs3 = (str1, str2) -> str1 + str2;
// 型を書く場合は全ての引数に書かなければならないのでコンパイルエラーが発生する。
InterfaceSample ifs4 = (String str1, str2) -> {return str1 + str2;};
}
// メソッドの処理が複数行のパターン
public static void main(String[] args) {
InterfaceSample ifs1= new InterfaceSample() {
public String method(String str) {
if(str.equals("aaaaa")) {
return "イコール(^O^)";
} else {
return "ノットイコール(T_T)";
}
}
};
InterfaceSample ifs2 = (String str) -> {
if(str.equals("aaaaa")) {
return "イコール(^O^)";
} else {
return "ノットイコール(T_T)";
}
};
InterfaceSample ifs3 = (str) -> {
if(str.equals("aaaaa")) {
return "イコール(^O^)";
} else {
return "ノットイコール(T_T)";
}
};
}
ラムダ式で使用できる変数
ラムダ式で使用できる変数や引数は以下のものがある。
・メソッド内で宣言したローカル変数
・メソッドの引数
・クラスで宣言されているフィールド
・ローカル変数の内、ラムダ式前で宣言されていて、finalまたは実質的finalな変数
実質的にfinal変数とは簡単に言うと、finalの変数ではないけれど、宣言した以降で値が変化していないのでfinalと同じと言える変数のことである。
実質的finalのコードの例としては以下の通り
public static void main(String[] args) {
String aaa = "aaa";
InterfaceSample ifs = str -> str;
System.out.println(ifs.method(aaa));
}
ローカル変数aaaを宣言後、値が変化していないためfinalの変数と実態が変わらないと言える。
この状態が実質的finalな状態である。
以下のコードでは変数iを宣言した後、iの値をラムダ式の中で変化させようとしているため、実質的finalとは言えなくなる。
finalまたは実質的finalでなければラムダ式で使用できる変数に当てはまらなくなるのでコンパイルエラーが起きる。
public static void main(String[] args) {
String aaa = "aaa";
InterfaceSample ifs = str -> {
// 実質的finalではなくなり、コンパイルエラーが起きる
aaa = "change";
return str;
};
}
以下のコードでもコンパイルエラーが起きる。
public static void main(String[] args) {
String aaa = "aaa";
InterfaceSample ifs = str -> aaa + "bbb";
System.out.println(ifs.method(aaa));
aaa = "change";
}
ラムダ式の中で変数aaaの値を変化させてはいないが、ラムダ式の処理の後に値を"change"変化させている。
ラムダ式の外であろうと変数の値が変化すれば実質的finalではなくなる。
参照型変数のfinal
参照型変数のfinalは変数が指すインスタンスを変えられないということを指す。
そのため、以下のようにコードを書くと配列の内容を書き換えることが可能となる。
public interface InterfaceSample {
public void method();
}
public class Sample {
public static void main(String[] args) {
final int[] i = {0, 1, 2};
System.out.println("変化前→" + i[1]);
InterfaceSample ifs = () -> {
i[1] = 10;
};
ifs.method();
System.out.println("変化後→" + i[1]);
}
}
変化前→1
変化後→10
このコードがコンパイルエラーにならないのは参照型変数iが指しているインスタンスを変更しないから。
iは同じ配列インスタンスを示しており、配列の中身を変えることはfinalの違反にはならない。
同様に参照型変数にゲッターとセッターがある場合、セッターを使用して値を変更することはfinalの違反にはならない。
また、ラムダ式で使用できる変数にクラスで宣言されているフィールド(インスタンス変数、static変数)があると説明したが、これらは値の変更が自由に行える。
関数型インターフェイスの一覧
関数型インターフェイスは自作もできるがよく使用されるものは標準として用意されている。
ここでは一部紹介する。(もっとたくさん確認したい人はこちら)
Runnable
引数なしと戻り値なしのrunメソッドを持つインターフェイス。
簡単に言うと、ただ処理を垂れ流す感じ。
入力と出力がないので個人的には使いどころがあまりわからない。
Runnable runnable1 = () -> System.out.println("abcde");
Runnable runnable2 = () -> {System.out.println("あいおえお");};
runnable1.run(); // 出力結果→abcde
runnable2.run(); // 出力結果→あいうえお
Consumer
消費するという意味。
引数1つ、戻り値なしのacceptメソッドを持つ。
引数を渡すのに戻り値を返さず、消費するというイメージ。
Consumer<String> consumer = str -> System.out.println(str);
consumer.accept("test"); // 出力結果→test
Supplier
供給するという意味。
引数なし、戻り値ありのgetメソッドを持つ。
言葉の通り、戻り値を供給するイメージ。
Supplier<Integer> supllier = () -> 100;
System.out.println(supllier.get()); // 出力結果→100
Function
代表的な関数型インターフェイス。
引数1つ、戻り値ありのapplyメソッドを持つ。
Function<String, String> function = str -> "戻り値" + str;
System.out.println(function.apply("test")); // 出力結果→戻り値test
ちなみにFunctionの二つのStringは左が引数、右が戻り値である。
BiFunction
引数2つ、戻り値ありのapplyメソッドを持つ。
Functionの引数2つ版のようなイメージ。
BiFunction<String, String, String> biFunction = (str1, str2) -> str1 + str2;
System.out.println(biFunction.apply("Hello", "World")); //出力結果→HelloWorld
BiConsumer
引数2、戻り値なしのacceptメソッドを持つ。
Consumerの引数2つ版のようなイメージ。
BiConsumer<Integer, Integer> biConsumer = (i, j) -> System.out.println(i + j);
biConsumer.accept(2, 8); // 出力結果→10
Predicate
引数1つ、戻り値でboolean型を返すtestメソッドを持つ。
Predicate<Integer> predicate = i -> i > 10;
System.out.println(predicate.test(5)); // 出力結果→false
Functionでも代用できるがPredicateの方が処理が速いらしいので真偽値が欲しいときはPredicateを使った方が良い。
StreamAPI
StreamAPIとはリストや配列などデータの集合体を操作することができるAPIである。
例えば、リストのデータをStreamAPIで操作したいとしたらlist.stream()と書くことでStreamが生成され、以降中間操作と終端操作と呼ばれるメソッドでデータの操作することができる。
中間操作ではデータの絞り込みや変換などデータを加工する処理が行われ、終端操作では中間操作で操作したデータを使用して最終的な操作を行う。
以下は数値を持つリストをStreamAPIを使用して、5より大きい値のみ出力するコードである。
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(8);
list.add(4);
list.add(10);
list.add(6);
list.add(3);
list.stream().filter(v -> v > 5).forEach(v -> System.out.println(v));
出力結果↓
8
10
6
list.stream()でリストをStreamを生成して、filterメソッドで5より値が大きいかどうかの絞り込みを行い、forEachメソッドで絞り込んだデータを出力している。
ここでいうfilterメソッドが中間操作、forEachメソッドが終端操作に当たる。
メソッドの引数にはラムダ式が使用されているのがわかる。(引数vはlistの要素1つ1つで要素の数だけ中間操作と終端操作を行う)
このようにStreamAPIを使用することで可読性が高いコードを書くことが可能になる。
中間操作と終端操作にはたくさんのメソッドが用意されている。
中間操作でよく使うのは「filter」「map」、終端操作でよく使うのは「forEach」「collect」「anyMatch」などがある。
これらのメソッドを駆使していくことで配列とリストの変換やデータの変換などを短い記述で実装できるので必要に応じて調べて使用していくと良い。
終わりに
ラムダ式やStreamAPIは知らない状態で見ると何をやっているのかわからないことが多いですが、私も現場で教えてもらい何度も使っていくことで少しずつ書いたり読めたりできるようになってきたので、苦手意識がある人はたくさん触れてみると良いと思う。