概要

関数合成は、小さな処理を組み合わせて複雑なロジックを組み立てる手法です。Java の標準 API には Function.andThen / compose、Predicate.and / or / negate、Consumer.andThen といった合成メソッドが用意されており、外部ライブラリなしで実務レベルのパイプラインを構築できます。この記事では、文字列の正規化(トリム→小文字変換→長さ取得)を andThen で段階的に組み立てる例を起点に、compose との実行順序の違い、Predicate の論理結合によるフィルタ条件の動的構築、Consumer の連結によるログ・監査の多段処理、そしてこれらを組み合わせたバリデーションパイプラインの実装までを扱います。関数合成を使いこなすと、条件分岐のネストが減り、処理の追加・除去が部品の差し替えで済むようになります。一方で、合成の段数が増えるとデバッグが難しくなる面もあるため、適切な粒度の見極め方も含めて整理します。

使いどころ

メールアドレスの入力正規化(trim → toLowerCase)と妥当性検証(空でない → @ を含む → ドメイン形式)をパイプラインとして定義する

CSV 取込時の各カラムに対して、複数の Predicate を and で連結した検証ルールを動的に構成し、エラー行を一括検出する

処理完了時のログ出力と監査記録を Consumer.andThen で連結し、通知先の追加・変更を既存コードに影響なく行う

コード例

FunctionCompositionDemo.java
import java.util.List;
import java.util.function.*;

public class FunctionCompositionDemo {

    public static void main(String[] args) {

        var trim = (Function<String, String>) String::trim;
        var toUpper = (Function<String, String>) String::toUpperCase;
        var length = (Function<String, Integer>) String::length;

        var trimThenUpper = trim.andThen(toUpper);
        System.out.println(trimThenUpper.apply("  hello  ")); // HELLO

        // 3段合成: trim → toUpper → length
        var pipeline = trim.andThen(toUpper).andThen(length);
        System.out.println(pipeline.apply("  hello  ")); // 5

        // toUpper.compose(trim) = toUpper(trim(x))
        var upperThenTrim = toUpper.compose(trim);
        System.out.println(upperThenTrim.apply("  world  ")); // WORLD

        var isPositive = (Predicate<Integer>) n -> n > 0;
        var isEven = (Predicate<Integer>) n -> n % 2 == 0;

        var isPositiveEven = isPositive.and(isEven);
        var isPositiveOrEven = isPositive.or(isEven);
        var isNotPositive = isPositive.negate();

        var numbers = List.of(-4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6);

        System.out.print("正の偶数: ");
        numbers.stream().filter(isPositiveEven)
            .forEach(n -> System.out.print(n + " "));
        System.out.println(); // 2 4 6

        System.out.print("正でない: ");
        numbers.stream().filter(isNotPositive)
            .forEach(n -> System.out.print(n + " "));
        System.out.println(); // -4 -3 -2 -1 0

        var log = (Consumer<String>) s -> System.out.print("[LOG] " + s);
        var audit = (Consumer<String>) s -> System.out.print(" [AUDIT] " + s);
        var logAndAudit = log.andThen(audit);
        logAndAudit.accept("処理完了");
        System.out.println();

        var normalize = trim.andThen(String::toLowerCase);
        var notEmpty = (Predicate<String>) s -> !s.isEmpty();
        var isEmail = (Predicate<String>) s -> s.contains("@");
        var isValidEmail = notEmpty.and(isEmail);

        var inputs = List.of(
            " [email protected] ", "invalid", "  ", "[email protected]");
        for (var input : inputs) {
            var normalized = normalize.apply(input);
            var valid = isValidEmail.test(normalized);
            System.out.println(
                input.trim() + " → " + normalized + " → valid=" + valid);
        }
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

var で中間変数の型推論が効き、合成チェーンの記述がコンパクトになる。List.of と組み合わせてテストデータも簡潔に用意できる。

Java 17
// Java 17: var + List.of で簡潔に
var trim = (Function<String, String>) String::trim;
var toUpper = (Function<String, String>) String::toUpperCase;

var pipeline = trim.andThen(toUpper);
System.out.println(pipeline.apply("  hello  ")); // HELLO

var normalize = trim.andThen(String::toLowerCase);
var isValid = ((Predicate<String>) s -> !s.isEmpty())
    .and(s -> s.contains("@"));

List.of(" [email protected] ", "invalid", "[email protected]")
    .forEach(s -> System.out.println(
        normalize.apply(s) + " → " + isValid.test(normalize.apply(s))));

Library Comparison

標準 API(Function / Predicate の合成メソッド)andThen / compose / and / or / negate で実務上十分なパイプラインを構築できる。Stream API との親和性が高い。合成チェーンが長くなるとデバッグが難しく、エラー箇所の特定に工夫が必要になる。
Vavr(Function の合成 + パターンマッチ)カリー化、部分適用、リフティングなど高度な関数合成を多用する場合。標準 API と API が重複するため、チーム内で使い分けルールが必要。学習コストも高い。
Spring Expression Language (SpEL)条件式を設定ファイルや DB から動的に読み込んで評価したいとき。実行時評価のためコンパイル時の型安全性がなく、パフォーマンスも関数合成より劣る。用途が異なる。

注意点

andThen は f → g の順、compose は g → f の順で実行される。チーム内で混在すると可読性が落ちるため、どちらかに統一するルールを設けるとよい(andThen を推奨する現場が多い)

Predicate.and は短絡評価されるため、左辺が false なら右辺は評価されない。null チェックを左辺に置くことで NPE を防げるが、順序の意図をコメントで明示するのが望ましい

関数合成の段数が 4〜5 段を超えると、例外発生時にどの段で落ちたかスタックトレースから追いにくくなる。長いパイプラインは中間結果をローカル変数に取り出してデバッグしやすくする

Consumer.andThen で連結した処理は、前段が例外を投げると後段は実行されない。ログと監査の両方を確実に実行したい場合は try-finally か個別呼び出しにする

FAQ

andThen と compose はどちらを使うべきですか。

andThen の方が左から右へ読めるため直感的です。チーム内で統一するなら andThen を推奨します。compose は数学的な関数合成(f of g)に慣れている場合に使うことがあります。

Predicate の合成順序でパフォーマンスは変わりますか。

短絡評価されるため、false になりやすい条件や軽い条件を左辺に置くと不要な評価を減らせます。実測で差が出るのは大量データのフィルタリング時です。

合成した関数のデバッグはどうすればよいですか。

中間結果をローカル変数に取り出すか、peek(Stream の場合)でログ出力を挟むのが実用的です。合成段数が増えすぎたら、名前付きメソッドに分割することを検討してください。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます