【プログラミングしてみよう58】JavaのFunctional Interfaceを自作する

Java

前回はswitch式とパターンマッチングをSealed Classesと組み合わせて深掘りしました。

今回は「Functional Interface(関数型インターフェース)」を自作する方法について解説します。 これまでラムダ式を使う際に Runnable や Comparator などの既存の関数型インターフェースを使ってきましたが、自分でも定義できることを知ることで、より柔軟な設計ができるようになります。

Functional Interfaceとは

「Functional Interface(関数型インターフェース)」とは、抽象メソッドをちょうど1つだけ持つインターフェースのことです。@FunctionalInterface アノテーションをつけることで、抽象メソッドが1つだけであることをコンパイラが保証します。

@FunctionalInterface
public interface MyFunction {
    int apply(int x); // 抽象メソッドは1つだけ
}

これをラムダ式で実装できます。

MyFunction square = x -> x * x;
System.out.println(square.apply(5)); // 25

Javaに標準で用意されている関数型インターフェース

java.util.function パッケージに代表的な関数型インターフェースが用意されています。

Function<T, R>      // T を受け取り R を返す
Consumer<T>         // T を受け取り何も返さない
Supplier<T>         // 何も受け取らず T を返す
Predicate<T>        // T を受け取り boolean を返す
BiFunction<T, U, R> // T と U を受け取り R を返す
UnaryOperator<T>    // T を受け取り T を返す(Function の特殊版)
BinaryOperator<T>   // T を2つ受け取り T を返す(BiFunction の特殊版)

多くの場合はこれらの標準インターフェースで十分ですが、独自のセマンティクスや引数の組み合わせが必要な場合は自作します。

サンプルコード(Functional Interfaceを自作する)

ファイル作成

「Java46」フォルダに Transformer.java、Validator.java、TriFunction.java、FunctionalTest.java という名前でファイルを作成します

コード入力

Transformer.java に以下のコードを入力します。

@FunctionalInterface
public interface Transformer<T> {
    T transform(T input);

    // defaultメソッドは複数持てる
    default Transformer<T> andThen(Transformer<T> after) {
        return input -> after.transform(this.transform(input));
    }
}

Validator.java に以下のコードを入力します。

@FunctionalInterface
public interface Validator<T> {
    boolean validate(T value);

    default Validator<T> and(Validator<T> other) {
        return value -> this.validate(value) && other.validate(value);
    }

    default Validator<T> or(Validator<T> other) {
        return value -> this.validate(value) || other.validate(value);
    }

    default Validator<T> negate() {
        return value -> !this.validate(value);
    }
}

TriFunction.java に以下のコードを入力します。

@FunctionalInterface
public interface TriFunction<A, B, C, R> {
    R apply(A a, B b, C c);
}

FunctionalTest.java に以下のコードを入力します。

public class FunctionalTest {
    public static void main(String[] args) {
        // Transformerの使い方
        System.out.println("--- Transformer ---");
        Transformer<String> trim   = String::trim;
        Transformer<String> upper  = String::toUpperCase;
        Transformer<String> exclaim = s -> s + "!";

        // andThenで変換をチェーンする
        Transformer<String> pipeline = trim.andThen(upper).andThen(exclaim);
        System.out.println(pipeline.transform("  hello world  "));

        // Validatorの使い方
        System.out.println("\n--- Validator ---");
        Validator<String> notEmpty  = s -> !s.isEmpty();
        Validator<String> notTooLong = s -> s.length() <= 10;
        Validator<String> noSpaces  = s -> !s.contains(" ");

        Validator<String> nameValidator = notEmpty.and(notTooLong).and(noSpaces);

        String[] names = {"ぱんまる", "", "とても長すぎる名前です", "pan maru"};
        for (String name : names) {
            System.out.println("\"" + name + "\": " + (nameValidator.validate(name) ? "OK" : "NG"));
        }

        // TriFunctionの使い方
        System.out.println("\n--- TriFunction ---");
        TriFunction<String, Integer, String, String> format =
                (name, age, city) -> name + "(" + age + "歳, " + city + ")";

        System.out.println(format.apply("ぱんまる", 25, "名古屋"));
        System.out.println(format.apply("たろう",   30, "東京"));
    }
}

コンパイル

javac Transformer.java Validator.java TriFunction.java FunctionalTest.java

実行

java FunctionalTest

実行結果は次のようになります。

--- Transformer ---
HELLO WORLD!

--- Validator ---
"ぱんまる": OK
"": NG
"とても長すぎる名前です": NG
"pan maru": NG

--- TriFunction ---
ぱんまる(25歳, 名古屋)
たろう(30歳, 東京)

サンプルコード(標準の関数型インターフェースの活用)

ファイル作成

「Java46」フォルダに StandardFuncTest.java という名前でファイルを作成します

コード入力

以下のコードを入力します。

import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;

public class StandardFuncTest {
    // 高階関数: 関数を引数に取るメソッド
    static <T> List<T> filterList(List<T> list, Predicate<T> pred) {
        return list.stream().filter(pred).toList();
    }

    static <T, R> List<R> mapList(List<T> list, Function<T, R> func) {
        return list.stream().map(func).toList();
    }

    static <T> void forEachIf(List<T> list, Predicate<T> pred, Consumer<T> action) {
        list.stream().filter(pred).forEach(action);
    }

    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // Predicateで絞り込む
        Predicate<Integer> isEven = n -> n % 2 == 0;
        Predicate<Integer> isGt5  = n -> n > 5;

        System.out.println("偶数: " + filterList(numbers, isEven));
        System.out.println("5より大きい: " + filterList(numbers, isGt5));
        System.out.println("偶数かつ5より大きい: " + filterList(numbers, isEven.and(isGt5)));
        System.out.println("偶数または5より大きい: " + filterList(numbers, isEven.or(isGt5)));

        // Functionで変換する
        Function<Integer, String> toStr = n -> n + "番";
        System.out.println("\n変換: " + mapList(numbers, toStr));

        // Function.compose と andThen
        Function<Integer, Integer> doubler  = n -> n * 2;
        Function<Integer, Integer> addTen   = n -> n + 10;
        Function<Integer, String>  toString = n -> "結果:" + n;

        Function<Integer, String> pipeline = doubler.andThen(addTen).andThen(toString);
        System.out.println("\nパイプライン(5): " + pipeline.apply(5));

        // UnaryOperatorで繰り返し変換
        UnaryOperator<String> addBang = s -> s + "!";
        String result = "Java";
        for (int i = 0; i < 3; i++) {
            result = addBang.apply(result);
        }
        System.out.println("\nUnaryOperator: " + result);

        // Supplierで遅延評価
        Supplier<List<String>> expensiveData = () -> {
            System.out.println("(データ生成中...)");
            return List.of("A", "B", "C");
        };

        System.out.println("\nSupplierを呼び出す前");
        List<String> data = expensiveData.get();
        System.out.println("取得したデータ: " + data);

        // forEachIfで条件付き処理
        System.out.println("\n偶数だけ表示:");
        forEachIf(numbers, isEven, n -> System.out.print(n + " "));
        System.out.println();
    }
}

コンパイル

javac StandardFuncTest.java

実行

java StandardFuncTest

実行結果は次のようになります。

偶数: [2, 4, 6, 8, 10]
5より大きい: [6, 7, 8, 9, 10]
偶数かつ5より大きい: [6, 8, 10]
偶数または5より大きい: [2, 4, 6, 7, 8, 9, 10]

変換: [1番, 2番, 3番, 4番, 5番, 6番, 7番, 8番, 9番, 10番]

パイプライン(5): 結果:20

UnaryOperator: Java!!!

Supplierを呼び出す前
(データ生成中...)
取得したデータ: [A, B, C]

偶数だけ表示:
2 4 6 8 10 

コードの解説

Transformer はString→Stringの変換を表す自作の関数型インターフェースです。default メソッドの andThen を使うことで、変換処理をチェーンできるようにしています。trim.andThen(upper).andThen(exclaim) のように変換パイプラインを組み立てられます。これはStream APIの .map() をチェーンするのと似た考え方です。

Validator は and・or・negate の default メソッドを持ち、複数のバリデーションを組み合わせられます。標準の Predicate も同じ and・or・negate メソッドを持っており、Validator はその考え方を自作で再現したものです。

TriFunction は標準ライブラリにない「3引数の関数」を自作した例です。BiFunction は2引数まで対応していますが、3引数以上が必要な場合は自作します。

Supplier<T> は引数なしで値を返す関数型インターフェースです。実際に get() が呼ばれるまで処理が実行されない「遅延評価」のパターンに使われます。重い処理の結果をSupplierで包んでおくと、必要なときだけ実行できます。

まとめ

@FunctionalInterface をつけた1抽象メソッドのインターフェースは、ラムダ式で実装できる関数型インターフェースになります。default メソッドを使って andThen や and・or のような合成メソッドを追加すると、複数の関数を組み合わせる柔軟な設計が実現できます。標準の Function・Predicate・Consumer・Supplier で足りる場合は標準を使い、3引数以上など標準にないパターンが必要な場合は自作しましょう。

2026/08/06(木) ぱんまる

コメント

タイトルとURLをコピーしました