概要
関数合成は、小さな処理を組み合わせて複雑なロジックを組み立てる手法です。Java の標準 API には Function.andThen / compose、Predicate.and / or / negate、Consumer.andThen といった合成メソッドが用意されており、外部ライブラリなしで実務レベルのパイプラインを構築できます。この記事では、文字列の正規化(トリム→小文字変換→長さ取得)を andThen で段階的に組み立てる例を起点に、compose との実行順序の違い、Predicate の論理結合によるフィルタ条件の動的構築、Consumer の連結によるログ・監査の多段処理、そしてこれらを組み合わせたバリデーションパイプラインの実装までを扱います。関数合成を使いこなすと、条件分岐のネストが減り、処理の追加・除去が部品の差し替えで済むようになります。一方で、合成の段数が増えるとデバッグが難しくなる面もあるため、適切な粒度の見極め方も含めて整理します。
使いどころ
メールアドレスの入力正規化(trim → toLowerCase)と妥当性検証(空でない → @ を含む → ドメイン形式)をパイプラインとして定義する
CSV 取込時の各カラムに対して、複数の Predicate を and で連結した検証ルールを動的に構成し、エラー行を一括検出する
処理完了時のログ出力と監査記録を Consumer.andThen で連結し、通知先の追加・変更を既存コードに影響なく行う
コード例
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);
}
}
}Version Coverage
var で中間変数の型推論が効き、合成チェーンの記述がコンパクトになる。List.of と組み合わせてテストデータも簡潔に用意できる。
// 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
注意点
andThen は f → g の順、compose は g → f の順で実行される。チーム内で混在すると可読性が落ちるため、どちらかに統一するルールを設けるとよい(andThen を推奨する現場が多い)
Predicate.and は短絡評価されるため、左辺が false なら右辺は評価されない。null チェックを左辺に置くことで NPE を防げるが、順序の意図をコメントで明示するのが望ましい
関数合成の段数が 4〜5 段を超えると、例外発生時にどの段で落ちたかスタックトレースから追いにくくなる。長いパイプラインは中間結果をローカル変数に取り出してデバッグしやすくする
Consumer.andThen で連結した処理は、前段が例外を投げると後段は実行されない。ログと監査の両方を確実に実行したい場合は try-finally か個別呼び出しにする
FAQ
andThen の方が左から右へ読めるため直感的です。チーム内で統一するなら andThen を推奨します。compose は数学的な関数合成(f of g)に慣れている場合に使うことがあります。
短絡評価されるため、false になりやすい条件や軽い条件を左辺に置くと不要な評価を減らせます。実測で差が出るのは大量データのフィルタリング時です。
中間結果をローカル変数に取り出すか、peek(Stream の場合)でログ出力を挟むのが実用的です。合成段数が増えすぎたら、名前付きメソッドに分割することを検討してください。