用語集
- インタフェース:クラスに対して「このメソッドを実装してください」と決める共通ルール。呼び出す側が具体的なクラスを知らなくても使用できる
- 関数型インタフェース:抽象メソッドを1つだけ持つインタフェース。これにより、その唯一の抽象メソッドの実装をラムダ式で記述でき、匿名クラスの記述やメソッド名、引数型などを省略できる。つまりはラムダ式で処理を渡すための型、受け皿である
- 抽象メソッド:処理内容を持たず、「メソッド名・引数・戻り値だけ」を定義したメソッド。{}の処理内容blockが存在しなし
int abstractMethod(int a, int b)
この記事について
「なんとなく書けるけど、きちんと言語化できない」「コードは追えるけど、仕組みを一から言語化して説明できない」状態を卒業するために書きました。同じレベルの方に少しでも参考になれば幸いです。
そもそもラムダ式とは
ラムダ式とは、関数型インタフェースの抽象メソッドの実装を、その場で簡潔に記述するための構文である
1. 関数型インターフェースとラムダ式
関数型インタフェースとは、抽象メソッドを1つだけ持つインタフェースである。
Javaのラムダ式では、単独では型を持つことができない。
なのでラムダ式を利用する際はどのような引数を受け取り、どのような戻り値を返す処理なのかを示す「受け皿」が必要となる。その役割を持つのが関数型インタフェースである。
例えば、次のようなインタフェースがあるとする。
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
Calculatorには、calculateという抽象メソッドが一つだけ定義されている。
そのため、匿名クラスを作成せず、ラムダ式を使って処理を実装できる
Calculator add = (a,b) -> a + b;
このラムダ式は、次の匿名クラスとほぼ同じ役割をもつ
Calculator add = new Calculator() {
@Override
public int calculate(int a, int b) {
return a + b;
}
}
関数型インタフェースでは実装対象の抽象メソッドが一つしかないため、Javaはラムダ式がどのメソッドを実装しているのかを判断できる。(その結果クラス名、メソッド名、引数の型などを省略して簡潔に記述できる)
尚、ラムダ式は記述しただけでは実行されない。関数型インタフェースのメソッドを呼び出すことで実行される
int result = add.calculate(10,20);
2. Javaで標準用意されている関数型インターフェース
Javaでは、ラムダ式を扱うための代表的な関数型インターフェースがjava.util.functionパッケージに用意されています。
基本として押さえておきたいのは、次の4つです。
| 関数型インターフェース | 引数 | 戻り値 | 主な用途 |
|---|---|---|---|
Consumer<T> |
1つ | なし | 値を受け取って処理する |
Supplier<T> |
なし | 1つ | 値を生成して返す |
Predicate<T> |
1つ | boolean |
条件を判定する |
Function<T,R> |
1つ | 1つ | 値を別の値へ変換する |
※インタフェースの型のTはType,RはResultの頭文字を表わします。
5. なぜ引数の型を書かなくても動くの?
- ラムダ式では、代入先やメソッドの引数として指定された関数型インタフェースから、引数や戻り値の型が推論されます。
Function<Integer, Integer> doubleValue = x -> x * 2;
この場合、代入先がFunctionであるため、Javaは次のように判断します。
引数xはInteger型
処理結果はInteger型
そのため、ラムダ式の中で引数の型を明示する必要がありません。
- ターゲット型とは
ラムダ式の型を決めるために使われる、代入先や引数の型を「ターゲット型」と呼びます。
同じ形のラムダ式でも、ターゲット型によって扱われる型は異なります。
// Integerを受け取り、Integerを返す
Function<Integer, Integer> integerDoubler = x -> x * 2;
// Doubleを受け取り、Doubleを返す
Function<Double, Double> doubleDoubler = x -> x * 2;
どちらもラムダ式はx -> x * 2ですが、代入先の型が異なるため、xの型も異なります。
6. ラムダ式の構文と省略ルール
ラムダ式は、一定の条件を満たす場合に引数の型、丸括弧、波括弧、returnなどを省略できます。
- 最も省略していない形は下記となります。
(int x) -> {
return x * 2;
}
- 処理が一つだけの式である場合、{}とreturnを省略できます。
(int x) -> x * 2;
- 引数の型は、ターゲット型から推論できるため省略できます。
(x) -> x * 2;
- さらに、引数が1つだけの場合は()も省略可能です。
x -> x * 2;
従って、次のラムダ式はすべて同じ処理を表わします。
(int x) -> {
return x * 2;
}
(int x) -> x * 2;
(x) -> x * 2;
x -> x * 2;
- ただし、引数が0個または2個以上の場合、()は省略できません。
() -> "Hello";
(a,b) -> a + b;
7. ラムダ式とメソッド参照の関係
ラムダ式が既存のメソッドを呼び出すだけの場合、メソッド参照を使ってさらに簡潔にかけることがあります。
- メソッド参照なし
Function<String, Integer> length = value -> value.length();
- メソッド参照あり
Function<String, Integer> length = String::length;
よく使われる例は、次のような書き方です。
names.forEach(name -> System.out.println(name));
// メソッド参照
names.forEach(System.out::println);
メソッド参照はラムダ式とは別の機能ですが、既存のメソッドを処理として渡すためのより簡潔な記法です。
※本記事では掘り下げませんがメソッド参照には静的メソッド参照、インスタンスメソッド参照、特定のインスタンスのメソッド参照、コンストラクタ参照があります。
ラムダ式から外側の変数を参照する
ラムダ式の中から、外側で定義されたローカル変数を参照できます。
int base = 10;
Function<Integer, Integer> addBase = value -> value + base;
ただし、ラムダ式から参照するローカル変数は、再代入されていない「実質的final」でなければいけません。
int base = 10;
base = 20;
// コンパイルエラー
Function<Integer, Integer> addBase = value -> value + base;
- この制約は、ラムダ式が外側の変数の値を安全に保持するために設けられています。
変数を参照するだけであれば、明示的にfinalを書く必要はありません。
final int base = 10;
int base = 10;
どちらも、あとから際代入しなければラムダ式内で参照可能です。
9. 関数型インターフェースとラムダ式を使うメリット
ラムダ式を使う最大のメリットは、処理を値のようにメソッドへ渡せることです。
例えば、通常のループでは、要素の取得、条件判定、結果への追加を順番に記述する。
List<Integer> result = new ArrayList<>();
for (Integer number : numbers) {
if (number >= 10) {
result.add(number);
}
}
- Stream APIとラムダ式を使うと、何をしたいのかを中心に記述できます。
List<Integer> result = numbers.stream()
.filter(number -> number >= 10)
.toList();
前者は処理手順を順番に記述する「命令型」の書き方です。一方
後者は10以上の要素を抽出するという目的を記述する「宣言型」の書き方です。
ラムダ式には、主に次のメリットがあります。
- 匿名クラスより短く記述できる
- 処理をメソッドの引数として渡せる
- 条件判定や変換処理を部品として再利用できる
- Stream APIと組み合わせて、処理の目的を読み取りやすくできる