クラスとメソッドとラムダ式
Javaのプログラムはクラスとメソッドでできています。
メソッドを実行するときは、必ずクラスを定義して、newでオブジェクトを生成します。
インタフェースの場合は、それをimplementsした実体のあるクラスを定義します。
ラムダ式は、このようなインタフェースを扱う処理に関する記法です。
// クラスの元になるインタフェース
public interface AnyClass {
public String getToday(boolean isAdvent);
}
// クラスを定義する
public class AnyParentClass implements AnyClass {
public String getToday(boolean isAdvent) {
return isAdvent ? "AdventCalendar 17日目" : "2020年12月17日";
}
}
public void execSample1() {
// メソッドを呼ぶときは、必ず定義したクラスをnewする
AnyClass obj = new AnyParentClass();
System.out.println(obj.getToday(true));
}
定義したクラスAnyParentClassをここでしか使用しない場合、匿名クラスを使用すれば、少し定義が楽になります。
AnyParentClassという、implementsしたクラス名が不要になります。
public void execSample2() {
// 匿名クラスなら、少し定義が楽に
AnyClass obj = new AnyClass() {
public String getToday(boolean isAdvent) {
return isAdvent ? "AdventCalendar 17日目" : "2020年12月17日";
}
};
System.out.println(obj.getToday(true));
}
でも、もう少し記述を楽にできないでしょうか?
- 変数objはAnyClassを使用することは宣言でわかります。
- インタフェースAnyClassはメソッドを1つしか定義していません。
とすると
- new AnyClass()を記述しなくても大丈夫じゃなかろうか。
- 使用するメソッドは1つしかないのだから、getToday()を記述しなくても大丈夫じゃなかろうか。
ということで記述を簡略化したのがラムダ式になります。
ただしラムダ式であることを示す「->」という語句を使用する必要があります。
public void execSample3() {
// ラムダ式なら、もっと簡単に
AnyClass obj = (a) -> {return a ? "AdventCalendar 17日目" : "2020年12月17日";};
System.out.println(obj.getToday(true));
}
まとめると、ラムダ式は
メソッドが1つだけのインタフェースを匿名クラスとして使用する場合に、定義を簡略化する記法。
になります。
ラムダ式の簡略化
このようにラムダ式は「記述しなくても大丈夫じゃなかろうか」の箇所を省略した記法です。
上記のコード、もう少し「記述しなくても大丈夫じゃなかろうか」がある気がします。
- 引数が1つだから、引数の()は記述しなくても大丈夫じゃなかろうか?
- 処理が1行だから、{}や;は記述しなくても大丈夫じゃなかろうか?
- 処理が1行だけで戻り値があるメソッドだから、returnは記述しなくても大丈夫じゃなかろうか
ということで、処理内容次第でここまで省略できます。
public void execSample4() {
// ラムダ式を、もっと簡単に
AnyClass obj = a -> a ? "AdventCalendar 17日目" : "2020年12月17日";
System.out.println(obj.getToday(true));
}
ラムダ式を使用すると
定義が楽になる以外にも、以下のような特徴があります。
関数を代入するような記法
先述の通り、Javaのプログラムはクラスとメソッドでできています。
従って通常の記法では、変数へクラスをnewしたオブジェクトを格納しますが、ラムダ式は処理を直接格納しているように見えるので、変数の内容をイメージしやすくなります。
public void execSample5() {
// 変数へオブジェクトを代入、これが通常
AnyClass obj1 = new AnyParentClass();
// ラムダ式は、変数へ直接処理(関数)を代入しているように見える
AnyClass obj2 = (a) -> {return a ? "AdventCalendar 17日目" : "2020年12月17日";};
}
ラムダ式を使う前提の汎用的なインタフェース
今までのサンプルコードは、AnyClassインタフェースを定義していました。
AnyClassはメソッド1つ、引数1つ、戻り値ありのインタフェースで、よくありがちなものですね。
なのでこれも「定義しなくても大丈夫じゃなかろうか」となるわけです。
はい、すでにFunctionというインタフェースがJavaで定義されおり、これを使用することができます。
public void execSample6() {
// AnyClassは不要、既存インタフェースのFunctionを使用
Function<Boolean, String> obj = a -> a ? "AdventCalendar 17日目" : "2020年12月17日";
System.out.println(obj.apply(true));
}
すでに定義されているインタフェースはjava.util.function内にあり、例として以下のようなものがあります。
- Function<T, R> 引数1つあり、戻り値あり
- Consumer<T> 引数1つあり、戻り値なし
- Supplier<T> 引数なし、戻り値あり
- Predicate<T> 引数1つあり、戻り値boolean
Stream API
Stream APIは難しく考えず、StringBuilder#append()と同じようなものと考えればよいと思います。
append()はStringBuilderを戻り値としているため
StringBuilder.append(xxx).append(xxx).append(xxx) ...
と連結することができます。
同じようにStream APIは、Streamクラス(正確にはインタフェース)がStreamを戻り値とするメソッドを持っています。
なのでStringBuilder#append()のようにメソッドを連結できますが、Streamのメソッドは引数にラムダ式を指定できるようにしてあり、処理込みで連結できます。
public void execSample7() {
List<String> list = Arrays.asList("A","D","V","E","N","T"," ","C","A","L","E","N","D","A","R");
// 実行結果「A! C! D! E! 」
list.stream().filter((s) -> { return "ABCDE".indexOf(s) != -1; }) //→Streamを返す
.sorted((s1, s2) -> { return s1.compareTo(s2); }) //→Streamを返す
.map((s) -> { return s + "!"; }) //→Streamを返す
.distinct() //→Streamを返す
.forEach((s) -> { System.out.print(s + " "); });
}
(※)本記事は入門者がまず初めにラムダ式を把握する目的で書かせて頂きました。私の勉強不足で本質的な思想から離れた表現があるかもしれませんが、ご勘弁願います。