はじめに
本記事では Java8 から追加された機能、ラムダ式について解説していきます。
概要
ラムダ式とは
Javaのラムダ式(lambda expression)とは、関数型インターフェイスを実装したクラスのインスタンスを、ごく短いコーディング量で簡単に作れる文法のことです。
ラムダ式はクラス定義とインスタンス生成をお手軽に実現する文法
Javaのラムダ式は、 関数型インターフェイス
を実装したクラスのインスタンスを簡単に作るための文法です。
言い換えればクラスの宣言とインスタンスの生成を同時に行う文法です。
以下のような、何かのインターフェイスを実装したクラスがあり、そのクラスのインスタンスを生成して使いたいとします。
※ここの例では インターフェイス( java.util.Comparator
)を使います。
import java.util.Comparator;
class ComparatorImpl implements Comparator<String> {
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
}
import java.util.Comparator;
class Sample1 {
public static void main(String[] args) {
Comparator<String> comparator = new ComparatorImpl();
System.out.println(comparator.compare("ABC", "DEF")); // => -3
}
}
これを以下のように1行で書けます。ラムダ式は「 (s1, s2) -> s1.compareTo(s2)
」の部分です。
最初のプログラムと同じようにインターフェイスが持つメソッド compare
が呼べていますね。
import java.util.Comparator;
class Sample1 {
public static void main(String[] args) {
Comparator<String> comparator = (s1, s2) -> s1.compareTo(s2);
System.out.println(comparator.compare("ABC", "DEF")); // ⇒ -3
}
}
Javaのラムダ式は、本質的にはこれだけです。
匿名クラスを定義し、そのクラスのインスタンスを生成するものです。
書き方のお作法や制限事項が少々ありますが、それは二の次です。
本来は複数行で行うクラスの定義が、なぜたった1行にできるのか。その理由はぱっと見では分からないです。
以降はその仕組みをしっかりと説明していきます。
関数型インターフェイスは、抽象メソッドを1つだけ持つ
前の説明では 関数型インターフェイス
という用語をいきなり使いました。
関数型インターフェイス(functional interface)
とは 抽象メソッドを1つだけ持つインターフェイス
のことです。
ラムダ式を学ぶ上で絶対に忘れてはならないのは、この「1つだけ」というところ。この特徴がラムダ式の理解には必須、かつラムダ式とは不可分です。その理由は後述します。
関数型インターフェイス
としては、例えば標準APIでは以下のものがあります。
また、抽象メソッドが1つだけあるインターフェイスに過ぎませんから、自分で作ることも可能です。
java.lang.Comparable
java.lang.Runnable
java.util.Comparator
java.util.concurrent.Callable
java.util.function.Consumer
java.util.function.Function
java.util.function.Predicate
java.util.function.Supplier
...など
関数型インターフェイスの例
それぞれ抽象メソッドは一つだけです。( run
と compare
)
package java.lang;
@FunctionalInterface
public interface Runnable {
public abstract void run(); // ←これがRunnableの抽象メソッド
}
package java.util;
※import文は省略
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2); // ←これがComparatorの抽象メソッド
※以下、default/staticメソッドの宣言がずっと続く…
}
クラスがラムダ式になるまで
普通のクラスがラムダ式になる過程を一つずつ追っていきます。
通常クラスの場合
まずインターフェイスを実装した通常クラスから始めます。
Comparator
インターフェイスを実装した通常クラス( ComparatorImpl
)と
実行クラス( Sample1
)を作成します。
import java.util.Comparator;
class ComparatorImpl implements Comparator<String> {
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
}
import java.util.Comparator;
class Sample1 {
public static void main(String[] args) {
Comparator<String> comparator = new ComparatorImpl();
System.out.println(comparator.compare("ABC", "DEF")); // => -3
}
}
解説
インターフェイスである Comparator
、その実装クラスである ComparatorImpl
、そして実行クラスの Sample1
が登場します。
特段難しい点はありませんが、ここで実装と呼び出しを行っている compare
メソッドが他で再利用する予定がなく、その場限りのメソッドであるとします。
そのような場合にわざわざ Comparator
インターフェイスの実装クラスを定義し、利用するという2段階を行うことは非常に手間です。
問題点
処理自体は大した処理でもないのでコード自体は短く済みますが、似たような実装を行うメソッドを呼び出したいという場合には、実装したいメソッドの数だけ実装クラスを定義する必要があります。
これが同じクラスファイル内であれば問題は小さいように感じますが、別のクラスファイルなどに分かれている場合などには管理することも手間になります。
匿名クラスの場合
このような手間を解決する時に利用できるのが匿名クラスを用いた実装です。
import java.util.Comparator;
class Sample1 {
public static void main(String[] args) {
Comparator<String> comparator = new Comparator<String>() {
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
};
System.out.println(comparator.compare("ABC", "DEF")); // ⇒ -3
}
}
クラスの定義とインスタンス生成(new)が1行になり、クラス名も消えました。
この匿名クラスのクラス名はJavaコンパイラが勝手に命名します。
これがラムダ式のベースになるものです。つまりラムダ式は特殊な書き方で匿名クラス(のようなもの)を作る文法だとも言えます。
ラムダ式の場合
最後にラムダ式を用いた実装です。
匿名クラスから更に new Comparator<String>() {}
と public int compare
を省略してラムダ式となります。
import java.util.Comparator;
class Sample1 {
public static void main(String[] args) {
Comparator<String> comparator = (String s1, String s2) -> { return s1.compareTo(s2); };
System.out.println(comparator.compare("ABC", "DEF")); // ⇒ -3
}
}
comparator
変数には Comparator<String>
を実装した匿名クラスのインスタンスが代入されます。
つまり、ラムダ式とはインターフェースを実装したインスタンスを生成する式 といえます。
さらに省略したラムダ式
引数の型や処理が1文なら return
や {}
も省略可能です。
import java.util.Comparator;
class Sample1 {
public static void main(String[] args) {
Comparator<String> comparator = (s1, s2) -> s1.compareTo(s2);
System.out.println(comparator.compare("ABC", "DEF")); // ⇒ -3
}
}
ラムダ式は関数型インターフェイスがあってこそ
この例で重要な役割を果たしたのは、代入演算子の左辺の変数の型 Comparator<String>
です。
この型が関数型インターフェイスの決まりごとを守っているのでラムダ式が成り立ちます。
関数型インターフェイスには1つしか抽象メソッドがないので、戻り値・引数の型と順番を関数型インターフェイスの型からJavaコンパイラが推測できます。この仕組みを 型推論
といいます。
ラムダ式がコンパイルエラーになる理由
ラムダ式の裏側では、クラスの変形手順の一番最初に出てきた、関数型インターフェイスを実装した匿名クラスをJavaが自動で作り、そのクラスのインスタンスを新しく生成して戻しています。
ですから、今なら以下のラムダ式がコンパイルエラーになる理由が分かるでしょう。
なぜなら、ラムダ式の対象となる関数型インターフェイスが何か、Javaコンパイラが分からないからですね。
class ComparatorImplTest {
public static void main(String[] args) {
(s1, s2) -> s1.compareTo(s2);
}
}
ラムダ式の約束事
ここまでの内容で、ラムダ式とは「関数型インターフェイスを実装したクラスのインスタンスを簡単に作るための文法」だということがお分かりいただけたでしょうか。
ここからは、ラムダ式を使う上で覚えておくべきいろいろな仕様について、ざっと説明します。
ラムダ式の基本形
ラムダ式の基本形は「①引数の宣言部 -> ②抽象メソッドの本体部」です。ここで注目すべきは ->
(アロー演算子)です。Javaで ->
が出たなら、それはラムダ式です。
①引数の宣言部と②抽象メソッドの本体部には、以下のパターンがあります。Javaのラムダ式は、この①と②のどれかの組み合わせで作られているのです。
引数の宣言部
パターン | 引数部の書き方 |
---|---|
抽象メソッドに引数がない場合 | () |
抽象メソッドの引数が1つだけの場合 | (引数の型 引数の変数名) |
(引数の変数名) | |
引数の変数名 | |
抽象メソッドの引数が2つ以上の場合 | (引数の型1 引数の変数名1, 引数の型2 引数の変数名2, …) |
(引数の変数名1, 引数の変数名2, …) |
そして、引数の型が省略されても、その変数の型が何かはJavaコンパイラは分かっています。ですから、メソッド本体部ではその変数の型が持つフィールドやメソッドを全部使えます。
ちなみに、引数の型が書かれているなら、finalなどの修飾子や注釈(アノテーション)をつけることもできます。逆に、変数名のみだと修飾子・アノテーションは書けません。
抽象メソッドの本体部
抽象メソッドの戻り値の型 | 抽象メソッドの処理内容 | 本体部の書き方 |
---|---|---|
void | 処理が1行 | 処理 |
{処理;} | ||
処理が2行以上 | {処理1行目; 処理2行目; …} | |
void以外 | 処理が1行 | 戻り値を戻す処理 |
{return 戻り値を戻す処理;} | ||
処理が2行以上 | {処理1行目; 処理2行目; …; return 処理結果;} |
ラムダ式ならではのことは、{}
や return
を省略できる場合があることです。先述した、抽象メソッドを実装する上で必要最小限なことは何だろう…の視点があれば、納得できるかと思います。
{}
があるならメソッドの中身は複数行で書けますし、1行だけでもOKです。普通のメソッドと同じく、改行やインデントは自由です。
{}
がないなら1行しか書けませんが、最後の ;
はいりません。
抽象メソッドの戻り値の型が void
ではなく、さらにメソッド本体部に {}
がないなら、その文や式の評価結果が戻り値の型と合っていればOKです。これはプログラムでの「評価」の仕組みが分かっていないと、ピンと来ないかもしれませんね。
return
については、戻り値の型が void
でも途中で return
はできますし、void
ではないならどこかで必ず戻り値の型を return
しなければなりません。これらも普通のメソッドと同じルールです。
引数とメソッド本体の組み合わせ例
以下の例では、すべて同じことをしています。
特に注目してもらいたいのは、引数部がどう省略できるかと、メソッド本体の {}
や return
がどういう場合に省略できるかです。
引数なし、戻り値の型がvoid
Runnable runnable0 = new Runnable() {
public void run() {
System.out.println("ABC");
}
};
Runnable runnable1 = () -> System.out.println("ABC");
Runnable runnable2 = () -> {System.out.println("ABC");};
引数なし、戻り値の型がString
Callable<String> callable0 = new Callable<String>() {
public String call() {
return "ABC";
}
};
Callable<String> callable1 = () -> "ABC";
Callable<String> callable2 = () -> {return "ABC";};
引数1つ、戻り値の型がboolean
Predicate<String> predicate0 = new Predicate<String>() {
public boolean test(String s) {
return "ABC".equals(s);
}
};
Predicate<String> predicate1 = (String s) -> "ABC".equals(s);
Predicate<String> predicate2 = (s) -> "ABC".equals(s);
Predicate<String> predicate3 = s -> "ABC".equals(s);
引数2つ、戻り値の型がInteger
Comparator<Integer> comparator0 = new Comparator<Integer>() {
public int compare(Integer i1, Integer i2) {
return i1.compareTo(i2);
}
};
Comparator<Integer> comparator1 = (Integer i1, Integer i2) -> {return i1.compareTo(i2);};
Comparator<Integer> comparator2 = (i1, i2) -> i1.compareTo(i2);
// ↓コンパイルエラー、型を書くなら、すべての引数に書かなければならない
Comparator<Integer> comparator3 = (Ingeter i1, i2) -> i1.compareTo(i2);
引数1つ、戻り値の型がInteger、メソッド本体が複数行
Function<String, Integer> function0 = new Function<String, Integer>() {
public Integer apply(String s) {
if ("ABC".equals(s)) {
return 1;
} else {
return 0;
}
}
};
Function<String, Integer> function1 = (String s) -> {
if ("ABC".equals(s)) {
return 1;
} else {
return 0;
}
};
Function<String, Integer> function2 = (s) -> {
if ("ABC".equals(s)) {
return 1;
} else {
return 0;
}
};
Function<String, Integer> function3 = s -> {
if ("ABC".equals(s)) {
return 1;
} else {
return 0;
}
};
まとめ
Javaのラムダ式とは、関数型インターフェイスを実装したクラスのインスタンスを、ごく短いコーディング量で簡単に作れる文法