Java : ラムダ式の基本
Java 8 でラムダ式が言語仕様に追加されました。
そして、Java 8 で追加されたAPIには、ラムダ式で使うことを想定しているものがあります。(Stream や Optional など)
ぜひラムダ式を理解して、Java 8 の API を使いこなしていきましょう。
本記事では、そんなラムダ式の基本的な使い方を解説していきます。
概要
さっそくコードを見てみましょう。
public void main() {
final Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Run!");
}
};
runnable.run(); // Run!
}
メソッド内で Runnable インタフェースの 匿名クラス(無名クラス) を作成して、インスタンスも生成しています。
Runnable は、スレッドを使うときによくお世話になるインタフェースですね。
スレッドに渡す Runnable を、このように匿名クラスで作成することはよくありそうです。
これが ラムダ式 だと、
public void main() {
final Runnable runnable = () -> {
System.out.println("Run!");
};
runnable.run(); // Run!
}
このように書けます。
だいぶすっきりとしますね!
関数型インタフェース
ラムダ式の前に、まずは関数型インタフェースを見ていきましょう。
関数型インタフェースはラムダ式の基本になります。
標準APIの関数型インタフェースである Runnable の定義を見てみましょう。
@FunctionalInterface
public interface Runnable {
void run();
}
このように、1つの抽象メソッド を持つインタフェースを、関数型インタフェースといいます。
関数型インタフェースでは、2つ以上の抽象メソッドは持てません。
上記の Runnable の例では、run メソッドが抽象メソッドですね。
厳密には必須ではないのですが、関数型インタフェースであることを明示するのに @FunctionalInterface アノテーションが使えます。
@FunctionalInterface をつけて、2つ以上の抽象メソッドを定義するとコンパイルエラーになります。
コンパイルエラーで早期に間違いに気づけるのは大事ですね。
問題は早期発見、早期解決が鉄則です。
// コンパイルエラーが発生します。
@FunctionalInterface
public interface NgFuncSample {
void run1();
void run2();
}
抽象メソッドでなければ、複数定義しても問題ありません。
具体的には staticメソッド や defaultメソッド です。
@FunctionalInterface
public interface FuncSample {
// 抽象メソッド
void run();
default void d1() {
System.out.println("d1");
}
default void d2() {
System.out.println("d2");
}
static void s1() {
System.out.println("s1");
}
static void s2() {
System.out.println("s2");
}
}
final FuncSample func = () -> {
System.out.println("Run!");
};
func.run(); // Run!
// defaultメソッド
func.d1(); // d1
func.d2(); // d2
// staticメソッド
FuncSample.s1(); // s1
FuncSample.s2(); // s2
ラムダ式
ラムダ式とは、簡単にいうと
- 関数型インタフェースの インスタンスを生成
するための簡易的な書きかたです。
次のように書くのが基本となります。
() -> {
}
ラムダ式はインスタンスを生成するので、結果を変数に代入できます。
final Runnable runnable = () -> {
System.out.println("Run!");
};
runnable.run();
// 結果
// ↓
//Run!
もしくは、メソッドのパラメータに直接渡すことが多いかもしれません。
public void main() {
func(() -> {
System.out.println("abcd");
});
// 結果
// ↓
//abcd
}
public void func(Runnable runnable) {
runnable.run();
}
パラメータ
関数にパラメータが必要となる例を見てみましょう。
FuncSample は、int 型の x と y パラメータを受け取る関数型インタフェースです。
@FunctionalInterface
public interface FuncSample {
void accept(int x, int y);
}
関数にパラメータがあるときは、ラムダ式の ( ) にパラメータを書きます。
クラスメソッドのパラメータ定義のような感じですね。
final FuncSample func = (int x, int y) -> {
System.out.println(x + y);
};
func.accept(1, 2); // 3
さらに、パラメータの型(int) も省略できます。
final FuncSample func = (x, y) -> {
System.out.println(x + y);
};
func.accept(1, 2); // 3
そしてさらに、処理が1文の場合は { } も省略できます。
final FuncSample func = (x, y) -> System.out.println(x + y);
func.accept(1, 2); // 3
戻り値
続いて、戻り値のある関数型インタフェースの例です。
// int x と int y を受け取り int を返す関数型インタフェース
@FunctionalInterface
public interface FuncSample {
int apply(int x, int y);
}
ラムダ式で値を返すには return文 を使います。
final FuncSample func = (x, y) -> {
return x + y;
};
final int result = func.apply(1, 2);
System.out.println(result); // 3
さらに、処理が1文の場合は { } と return文 も省略できます。
final FuncSample func = (x, y) -> x + y;
final int result = func.apply(1, 2);
System.out.println(result); // 3
標準で用意されている関数型インタフェース
標準API の java.util.function パッケージには、汎用的に使える関数型インタフェースが多数用意されています。
基本的にはここにあるもので済むことが多いですね。
※Runnbale は java.lang パッケージとなります。
よく使う関数型インタフェースをご紹介します。
API | 戻り値 | パラメータ | コード例 |
---|---|---|---|
Runnbale | なし | なし |
|
Consumer<T> | なし | (T param) |
|
Supplier<T> | T | なし |
|
Function<T,R> | R | (T param) |
|
Predicate<T> | boolean | (T param) |
|
この他にも
- 2つのパラメータを受け取る BiConsumer や BiFunction
- プリミティブ型を扱う IntConsumer や LongSupplier
などがあります。
詳細は、公式API仕様の「java.util.function (Java SE 20 & JDK 20)」をご確認ください。
Stream でラムダ式を使う例
Java 8 で追加されたAPIに、Stream インタフェースがあります。
このインタフェースでは、ふんだんに関数型インタフェースを使います。
API仕様にも、通常は ラムダ式(またはメソッド参照) を使うことを想定している、との記述がありますね。
少しだけコード例をご紹介します。
まずはラムダ式を使った例です。
// 文字のストリームです。
final var stream = Stream.of('A', 'b', 'C', 'D', 'e');
// 大文字だけを抽出(filter)して、
// その文字が3つ続く文字列に変換(map)して、
// 結果をリストで取得します。
final var result = stream
.filter(c -> Character.isUpperCase(c))
.map(c -> String.valueOf(c).repeat(3))
.toList();
System.out.println(result); // [AAA, CCC, DDD]
次に、ラムダ式を使わない例です。
final var stream = Stream.of('A', 'b', 'C', 'D', 'e');
final var result = stream
.filter(new Predicate<Character>() {
@Override
public boolean test(Character c) {
return Character.isUpperCase(c);
}
}).map(new Function<Character, String>() {
@Override
public String apply(Character c) {
return String.valueOf(c).repeat(3);
}
}).toList();
System.out.println(result); // [AAA, CCC, DDD]
ラムダ式を使ったコードに比べて、ちょっと見た目がごちゃつきますよね。
あとは、記述量が多くなるので、単純にコーディングが遅くなりそうです。
関連 : ストリームの基本 (Stream)
まとめ
ラムダ式、いかがでしたでしょうか?
最初は Java らしからぬシンプルさに戸惑うかもしれません。
個人的には、最初は型がいろいろと省略されているので分かりづらいな、と感じました。
ただ、IDE を使っていれば型も追いやすいので、今はシンプルな表記も悪くはないなと思っています。
今回ご紹介しきれなかったものとして、メソッド参照、コンストラクタ参照があります。
「メソッド参照の基本」の記事でご紹介しているので、そちらも合わせてご参照ください。
関連記事
- API 使用例
- @FunctionalInterface
- BiConsumer
- BiFunction
- BiPredicate
- BooleanSupplier
- Comparator (比較)
- Consumer
- DoubleConsumer
- DoubleFunction
- DoublePredicate
- DoubleSupplier
- Function
- IntConsumer
- IntFunction
- IntPredicate
- IntSupplier
- LongConsumer
- LongFunction
- LongPredicate
- LongSupplier
- Predicate
- Runnable
- Supplier
- ToIntFunction
- Stream
- Optional