CodeGym /コース /JAVA 25 SELF /関数型インターフェイス: Predicate, Consumer, Supplier, Function

関数型インターフェイス: Predicate, Consumer, Supplier, Function

JAVA 25 SELF
レベル 49 , レッスン 0
使用可能

1. 関数型インターフェイスを掘り下げる

関数型インターフェイスとは、ちょうど1つの抽象(つまり未実装の)メソッドだけを持つインターフェイスのことです。この性質のおかげで Java は「なるほど、ここにはラムダを渡せる!」あるいはメソッド参照を使える、と判断します。

間違いを防ぐため、この種のインターフェイスには通常アノテーション @FunctionalInterface が付けられます。必須ではありませんが、もし付けた状態でうっかり2つ目の抽象メソッドを書いてしまうと、コンパイラがすぐに警告してくれます。

例:

@FunctionalInterface
interface MyAction {
    void run();
}
MyAction action = () -> System.out.println("ラムダからのこんにちは!");
action.run(); // 出力: ラムダからのこんにちは!

何のために必要?

  • 無名クラスを作る代わりにラムダ式やメソッド参照を使える(ボイラープレートが減る)。
  • そのインターフェイスが関数型プログラミング向けであることをコンパイラに明確に伝えられる。

豆知識: Java の標準ライブラリには既に多数のこうしたインターフェイスがあります。車輪の再発明は不要です!

2. 標準の関数型インターフェイス概観

java.util.function パッケージには数十の関数型インターフェイスがあります。ここでは特に使用頻度の高い4つを取り上げます(Java のインターフェイスの中でも「出番」が多いものです)。

インターフェイス 引数 戻り値 主な用途
Predicate<T>
T
boolean
条件チェック(フィルタリング)
Consumer<T>
T
void
オブジェクトに対して処理を実行
Supplier<T>
なし
T
オブジェクトの取得/生成
Function<T, R>
T
R
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)
Predicate<T>
filter(u -> u.getAge() > 18)
map (Stream)
Function<T, R>
map(u -> u.getName())
forEach (Stream, List)
Consumer<T>
forEach(u -> System.out.println(u))
generate (Stream)
Supplier<T>
Stream.generate(() -> ...)

なぜ関数型インターフェイスを知っておくことが重要?

  • Java におけるすべてのラムダ式の基盤だから。
  • 汎用的で再利用可能かつ簡潔なコードが書けるから。
  • コレクション、ストリーム、非同期タスクの扱いが簡単になるから。

どのインターフェイスをいつ使う?

  • Predicate — 条件をチェックするとき(フィルタや検索)。
  • Consumer — オブジェクトに対して何か処理をするとき(出力、保存、送信)。
  • Supplier — オブジェクトを取得・生成するとき(ファクトリ、ジェネレーター)。
  • Function — オブジェクトをある型から別の型へ変換するとき。

6. よくあるミス

エラー1: インターフェイスの選択ミス。 初学者は PredicateFunction を混同しがちです。例えば、Function から boolean を返そうとしてしまい、本来は Predicate を使うべきところで使わない、など。覚えておきましょう: Predicate は常に boolean を返し、Function はそれ以外の型を返します。

エラー2: 標準インターフェイスを使わない。Checker」のように boolean check(T t) を持つ独自インターフェイスを定義してしまうことがありますが、Predicate を使うほうが良いです。標準のものは広くサポートされ、他の開発者にもコード意図が伝わりやすくなります。

エラー3: ラムダが複雑すぎる。 ラムダが10行の「短編小説」のようになってきたら、別メソッドやクラスに切り出しましょう。ラムダは簡潔さと可読性が命です。

エラー4: アノテーション @FunctionalInterface の付け忘れ。 独自の関数型インターフェイスを書く場合は、このアノテーションを忘れないこと。うっかり2つ目の抽象メソッドを追加してしまう誤りから守ってくれます。

エラー5: ラムダ内で可変状態を使う。 ラムダが外部の変数やコレクションを変更すると、特に並行処理では予期せぬバグの原因になります。副作用は可能な限り避けましょう。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION