1ヶ月ほど前、初めてAndroidアプリを開発しました。Android Studioを使って開発したのですが、Android Studioが結構なLambda押しで、ことあるごとに「Lambda式にできますよ!これもLambda式にできますよ!!」って、warningを上げてきました。
ワンクリックで自動的にLambda式にしてくれて便利なのですが、同時に、Lambda式変換後のソースコードを見ると、Lambda式がわからないと何が書かれているのかわからないなと感じました。以前OCJP-Gold SE8 の資格の勉強をした時にLambda式は勉強したのですが、1年以上昔の話で忘れている部分もあるので整理しようと思います。
Lambda式って?
Javaで関数型プログラミングを行うために、Java SE 8から導入された記法です。条件を満たした場合にメソッド名や引数などを省略できるようになるもので、具体的には以下のようなものです。
<通常の記法>
Comparator<Integer> comp = new Comparator<Integer>() {
@Override
public int compare(Integer i1, Integer i2) {
// 自然順序付けの逆
return i1.compareTo(i2) * -1;
}
};
<Lambda式>
// 自然順序付けの逆
Comparator<Integer> comp = (i1, i2) -> i1.compareTo(i2) * -1;
匿名クラス(無名クラス)
上記、<通常の記法>は「匿名クラス」ないしは「無名クラス」と呼ばれているものです。Comparatorはインタフェースなので通常、インスタンス化出来ません。Comparatorをimplementsしたクラスを作成し、それをインスタンス化するのがオーソドックスなコーディングですが、匿名クラスはそういったクラスを作成することなく、インタフェースの実装クラスを作成する記法です。クラス名がないため匿名クラスというわけですね。
Lambda式が書ける条件
<通常の記法>を<Lambda式>で書けるようになるには以下の条件が揃っている必要があります。
①対象のインタフェースは、単一のabstract メソッドを保有すること
②メソッドの実装は1行で記載出来ること
①について、Comparatorが保有するabstractメソッドは"compare(T o1, T o2)"のみです。
②について、メソッドの実装は「return i1.compareTo(i2);」という1行です。
上記2つの条件を満たしているため、ComparatorはLambda式を使用することができます。
Lambda式で省略できること
Lambda式で省略できることを以下に記載します。
・new
・右辺のインスタンス化するクラス名
・クラスを括る{}
・可視性(publicとか)
・戻り値の型
・メソッド名
・メソッドを括る{}
・メソッド引数の型
・return
・左辺とイコール(=)(後述1)
・引数の数が1つの場合は引数を括る()(後述2)
abstractメソッドを1つだけ保有するということは、具象クラスで実装するメソッドが1つということです。このため、実装する際、メソッド名や引数の型などは推論できる(1つしかないので)ため、省略可能になります。その他の省略可能項目は関数型っぽくするためのものですね。
どうやって使うのか?
上述のLambda.javaの書き方を踏襲すると、Lambda式の使い方はLambda2.javaのようになります。
List<Integer> list = new ArrayList<>();
list.add(3);
list.add(1);
list.add(4);
list.add(5);
Comparator<Integer> comp = (i1, i2) -> i1.compareTo(i2) * -1;
list.sort(comp);
Lambda2.javaはさらにLambda3.javaのように実装することができます。
List<Integer> list = new ArrayList<>();
list.add(3);
list.add(1);
list.add(4);
list.add(5);
list.sort((i1, i2) -> i1.compareTo(i2) * -1);
これは、「Lambda式が書ける条件」で後述1とした部分、「左辺とイコール(=)」を省略したものです。Lambda3.javaの1行目でジェネリクスを使用して、Listの要素をIntegerに限定しています。これにより、sortメソッドの引数がComparator<Integer>
になります。メソッドの引数にLambda式を書くことで左辺がComparator<Integer>
であると推論されます。このため、左辺は省略可能となり、その結果、Lambda3.javaのような書き方が出来るようになります。上記は型パラメータをIntegerで書きましたが、下記の通り、Stringなど、他の型で書く事もできます。
List<String> list = new ArrayList<>();
list.add("a");
list.add("B");
list.add("c");
list.add("D");
// Stringのメソッド(compareToIgnoreCase)を使用した順序付け
list.sort((s1, s2) -> s1.compareToIgnoreCase(s2));
引数の()の省略
これまでの例では示せていませんが、単一のabstractメソッドの引数の数が1つの場合、引数の()を省略することができます(Lambda式で省略できることの後述2)。具体的には以下のようなものです。
// @FunctionalInterfaceは関数型インタフェース(単一abstractメソッドを持つインタフェース)
// であることを示すアノテーション
@FunctionalInterface
public interface OneArg {
public void doSomething(String args);
}
public class UseOneArg {
public static void main(String[] args) {
OneArg oneArg = s -> System.out.println(s);
oneArg.doSomething("val");
}
}
"->"の左のsは()で括られていませんが、問題なくコンパイル・実行できます(コンソールに"val"が出力されます)。引数が1つの場合のみですが、覚えておくとよいでしょう。
なぜLambda式は生まれたのか?
この書き方は何が嬉しいのでしょうか?1行で書けるからコード量が少なくて済むことでしょうか?それも確かにそうですが、一番の理由はそこではありません。Lambda式は、関数型プログラミングの流れを汲んだ結果生まれたものです。
関数型プログラミング
関数型プログラミングで言うところの「関数」とは、「入力が同じであれば出力も必ず同じもので、入力以外には影響を与えないもの」です。入力以外には影響を与えないということを”副作用がない”という言い方をします。副作用の具体例をNotFuncSample.javaに示します。
public class NotFuncSample {
private static int x;
public static int fukusayou1(int param) {
return x *= param; // ここでxの値が変わる⇒副作用がある
}
public static int fukusayou2(int param) {
x += param; // ここでxの値が変わる⇒副作用がある
return param;
}
}
副作用がないと何が嬉しいのでしょうか?マルチスレッド環境下で不具合を起こしにくくなります。副作用があるメソッドをマルチスレッドで動かすと、入力以外のものに影響を与えます。それをマルチスレッドで動かすと、何が起こるか予測不能になります(1回目に動かしたときと2回目に動かしたときで結果が異なるということが平気で起こります)。副作用がなければそのような状況になることはないため、マルチスレッドで動かしやすいですね。
最近のCPUはマルチコアなので、その性能を最大限に引き出すにはマルチスレッドでプログラムを動かす必要があります。つまりマルチコアCPUの性能を最大限発揮するには関数型プログラミングが適しており、JavaのLambda式はJavaで関数型プログラミングを行う為に生まれたものです。
終わりに
いかがでしたでしょうか。Lambda式が難しくてよくわからないという声は私の周辺ではちらほら聞こえます。当記事が少しでも理解の助けになれば幸いです。
Java 8 ではLambda式と共に、Stream API という機能が追加されていて、Lambda式とは切っても切れない機能になります。次回、Stream API について整理していこうと思います。