1. 関数型インターフェイスを掘り下げる
関数型インターフェイスとは、ちょうど1つの抽象(つまり未実装の)メソッドだけを持つインターフェイスのことです。この性質のおかげで Java は「なるほど、ここにはラムダを渡せる!」あるいはメソッド参照を使える、と判断します。
間違いを防ぐため、この種のインターフェイスには通常アノテーション @FunctionalInterface が付けられます。必須ではありませんが、もし付けた状態でうっかり2つ目の抽象メソッドを書いてしまうと、コンパイラがすぐに警告してくれます。
例:
@FunctionalInterface
interface MyAction {
void run();
}
MyAction action = () -> System.out.println("ラムダからのこんにちは!");
action.run(); // 出力: ラムダからのこんにちは!
何のために必要?
- 無名クラスを作る代わりにラムダ式やメソッド参照を使える(ボイラープレートが減る)。
- そのインターフェイスが関数型プログラミング向けであることをコンパイラに明確に伝えられる。
豆知識: Java の標準ライブラリには既に多数のこうしたインターフェイスがあります。車輪の再発明は不要です!
2. 標準の関数型インターフェイス概観
java.util.function パッケージには数十の関数型インターフェイスがあります。ここでは特に使用頻度の高い4つを取り上げます(Java のインターフェイスの中でも「出番」が多いものです)。
| インターフェイス | 引数 | 戻り値 | 主な用途 |
|---|---|---|---|
|
|
|
条件チェック(フィルタリング) |
|
|
|
オブジェクトに対して処理を実行 |
|
なし | |
オブジェクトの取得/生成 |
|
|
|
T を R に変換 |
Predicate<T>
説明: 型 T のオブジェクトを受け取り、true または false を返す関数。典型例はリストのフィルタ。主要メソッドは test。
Predicate<String> isLong = s -> s.length() > 5;
System.out.println(isLong.test("Java")); // false
System.out.println(isLong.test("Functional")); // true
Consumer<T>
説明: 型 T のオブジェクトを受け取り、処理を行い、何も返さない。主要メソッドは accept。
Consumer<String> printer = s -> System.out.println("出力: " + s);
printer.accept("Hello, world!"); // 出力: Hello, world!
Supplier<T>
説明: 何も受け取らず、型 T のオブジェクトを返す。「値のジェネレーター」と考えることができる。主要メソッドは get。
Supplier<Double> randomSupplier = () -> Math.random();
System.out.println(randomSupplier.get()); // 例えば、0.1234567
Function<T, R>
説明: 型 T のオブジェクトを受け取り、型 R のオブジェクトを返す。典型例はデータの変換。主要メソッドは apply。
Function<String, Integer> stringToLength = s -> s.length();
System.out.println(stringToLength.apply("Java")); // 4
補足: UnaryOperator, BinaryOperator, BiFunction
- UnaryOperator<T> — Function<T, T> と同じ。受け取る型と返す型が同じ。
- BinaryOperator<T> — BiFunction<T, T, T> と同じ。2つの T を受け取り、1つの T を返す。
- BiFunction<T, U, R> — 2つの異なる型を受け取り、3つ目の型を返す。
UnaryOperator<Integer> square = x -> x * x;
BinaryOperator<Integer> sum = (a, b) -> a + b;
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);
3. 使用例
これらのインターフェイスが実際の課題、特にコレクションや Stream API でどう使われるかを見てみましょう。
コレクションや Stream API のメソッドに渡す
例1: Predicate とフィルタリング
List<String> words = List.of("java", "stream", "lambda", "code");
List<String> longWords = words.stream()
.filter(word -> word.length() > 4) // Predicate<String>
.toList();
System.out.println(longWords); // [stream, lambda]
例2: Consumer と forEach
words.forEach(word -> System.out.println("単語: " + word)); // Consumer<String>
例3: Function と map
List<Integer> lengths = words.stream()
.map(word -> word.length()) // Function<String, Integer>
.toList();
System.out.println(lengths); // [4, 6, 6, 4]
例4: Supplier と値の生成
Supplier<String> greetingSupplier = () -> "こんにちは、Java!";
System.out.println(greetingSupplier.get()); // こんにちは、Java!
無名クラスとの比較
以前は次のように書く必要がありました:
Predicate<String> isShort = new Predicate<String>() {
@Override
public boolean test(String s) {
return s.length() < 5;
}
};
ラムダを使えばずっと簡潔になります:
Predicate<String> isShort = s -> s.length() < 5;
4. 実践: 各インターフェイス向けにラムダ式を書く
小さなアプリとしてユーザー一覧を実装してみましょう。各ユーザーはクラス User で表現します:
public class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public String toString() {
return name + " (" + age + ")";
}
}
ユーザーのリストを作成:
List<User> users = List.of(
new User("Anna", 23),
new User("Boris", 17),
new User("Vika", 31),
new User("Gosha", 15)
);
Predicate: 成人のフィルタ
Predicate<User> isAdult = user -> user.getAge() >= 18;
List<User> adults = users.stream()
.filter(isAdult)
.toList();
System.out.println("成人: " + adults); // 成人: [Anna (23), Vika (31)]
Consumer: ユーザーの出力
Consumer<User> printUser = user -> System.out.println("ユーザー: " + user);
adults.forEach(printUser);
Supplier: ユーザーの生成
Supplier<User> randomUserSupplier = () -> {
String[] names = {"Dima", "Katya", "Lyosha"};
int randomAge = 10 + (int)(Math.random() * 30);
String randomName = names[(int)(Math.random() * names.length)];
return new User(randomName, randomAge);
};
User randomUser = randomUserSupplier.get();
System.out.println("ランダムなユーザー: " + randomUser);
Function: ユーザー名の取得
Function<User, String> getName = user -> user.getName();
List<String> names = users.stream()
.map(getName)
.toList();
System.out.println("名前: " + names); // 名前: [Anna, Boris, Vika, Gosha]
5. 役に立つポイント
Stream API での使い所: filter, map, forEach など
すべてを組み合わせて、変換のチェーンを書いてみます:
users.stream()
.filter(user -> user.getAge() >= 18) // Predicate<User>
.map(user -> user.getName().toUpperCase()) // Function<User, String>
.forEach(name -> System.out.println("成人: " + name)); // Consumer<String>
結果:
成人: ANNA
成人: VIKA
早見表: どこに何を渡すか
| どこで使うか | 必要なインターフェイス | 使用例 |
|---|---|---|
| filter (Stream) | |
|
| map (Stream) | |
|
| forEach (Stream, List) | |
|
| generate (Stream) | |
|
なぜ関数型インターフェイスを知っておくことが重要?
- Java におけるすべてのラムダ式の基盤だから。
- 汎用的で再利用可能かつ簡潔なコードが書けるから。
- コレクション、ストリーム、非同期タスクの扱いが簡単になるから。
どのインターフェイスをいつ使う?
- Predicate — 条件をチェックするとき(フィルタや検索)。
- Consumer — オブジェクトに対して何か処理をするとき(出力、保存、送信)。
- Supplier — オブジェクトを取得・生成するとき(ファクトリ、ジェネレーター)。
- Function — オブジェクトをある型から別の型へ変換するとき。
6. よくあるミス
エラー1: インターフェイスの選択ミス。 初学者は Predicate と Function を混同しがちです。例えば、Function から boolean を返そうとしてしまい、本来は Predicate を使うべきところで使わない、など。覚えておきましょう: Predicate は常に boolean を返し、Function はそれ以外の型を返します。
エラー2: 標準インターフェイスを使わない。 「Checker」のように boolean check(T t) を持つ独自インターフェイスを定義してしまうことがありますが、Predicate を使うほうが良いです。標準のものは広くサポートされ、他の開発者にもコード意図が伝わりやすくなります。
エラー3: ラムダが複雑すぎる。 ラムダが10行の「短編小説」のようになってきたら、別メソッドやクラスに切り出しましょう。ラムダは簡潔さと可読性が命です。
エラー4: アノテーション @FunctionalInterface の付け忘れ。 独自の関数型インターフェイスを書く場合は、このアノテーションを忘れないこと。うっかり2つ目の抽象メソッドを追加してしまう誤りから守ってくれます。
エラー5: ラムダ内で可変状態を使う。 ラムダが外部の変数やコレクションを変更すると、特に並行処理では予期せぬバグの原因になります。副作用は可能な限り避けましょう。
GO TO FULL VERSION