ラムダ式

要約

  • ラムダ式は関数型インターフェース(抽象メソッドが1つ)の実装を簡潔に記述するための構文です。
  • メソッド参照Class::method)で可読性が向上します。
  • java.util.functionPredicate / Function / Consumer / Supplier 等を活用します。
  • 変数キャプチャは「実質的final」が条件。Checked例外は包む/補助メソッドで対処します。
  • Streamと組み合わせてmap/filter/collectなどの宣言的処理を書けます。

1. ラムダ式とは(関数型インターフェース)

@FunctionalInterface
interface Validator<T> {
    boolean test(T value); // 抽象メソッドは1つ
}

Validator<String> notEmpty = s -> s != null && !s.isBlank();
System.out.println(notEmpty.test("hi")); // true
  • @FunctionalInterface は任意ですが、誤用を検出できるので推奨です。
  • 既存のRunnable/Callable/Comparatorなども関数型インターフェースです。

2. 構文とメソッド参照

// パラメータ型は推論される
Function<String, Integer> len = s -> s.length();

// 複文は波括弧+return
Function<String, String> trimUpper = s -> {
    String t = s.trim();
    return t.toUpperCase();
};

// メソッド参照(簡潔)
Function<String, Integer> len2 = String::length;     // インスタンスメソッド参照
Supplier<List<String>> newList = ArrayList::new;     // コンストラクタ参照
BiFunction<Integer,Integer,Integer> max = Math::max; // staticメソッド参照
  • 読みやすさ重視で、置換可能な場面はメソッド参照を優先します。

3. java.util.function 主要インターフェース

import java.util.function.*;

Predicate<String>   isLong    = s -> s.length() >= 5;         // boolean返し
Function<String,Integer> toLen= String::length;                // T -> R
Consumer<String>    printer   = System.out::println;           // 受け取って消費
Supplier<String>    nonce     = () -> java.util.UUID.randomUUID().toString();
UnaryOperator<String> trimU   = s -> s.trim().toUpperCase();   // T -> T
BinaryOperator<Integer> sum   = Integer::sum;                  // (T,T) -> T
BiFunction<String,Integer,String> padRight = (s,n) -> String.format("%-" + n + "s", s);
  • IntFunction / IntUnaryOperator など基本型専用の派生もあります(ボクシング回避)。

4. 合成(andThen / compose / negate / and / or)

Function<String,String> trim = String::trim;
Function<String,String> upper= String::toUpperCase;
Function<String,String> pipeline = trim.andThen(upper);

Predicate<String> notBlank = s -> !s.isBlank();
Predicate<String> longAndNotBlank = notBlank.and(isLong);
Predicate<String> shortOrBlank = isLong.negate().or(String::isBlank);
  • 関数合成で再利用性が高まり、テストも容易になります。

5. Comparator×ラムダ

import java.util.*;

record User(String name, int age) {}
List<User> users = new ArrayList<>(List.of(
    new User("Alice", 30),
    new User("Bob", 25),
    new User("Bob", 40)
));

// name昇順→age昇順
users.sort(Comparator.comparing(User::name).thenComparingInt(User::age));
  • comparingInt/Long/Double を使うとボクシングを避けられます。

6. Streamとラムダの基本操作

import java.util.*;
import java.util.stream.*;

List<String> words = List.of(" apple ", "banana", "  kiwi", "pear  ");

List<String> normalized = words.stream()
        .map(String::trim)                 // 変換
        .filter(w -> w.length() >= 4)      // 絞り込み
        .sorted(Comparator.naturalOrder()) // 並べ替え
        .toList();                         // 収集(Java16+)
System.out.println(normalized); // [apple, banana, pear]

int totalLen = words.stream().map(String::trim).mapToInt(String::length).sum();
  • 集計は count/sum/average/reduce/collect(groupingBy/partitioningBy) など。
  • 副作用のあるラムダは可読性・安全性を下げるので最小限に。

7. 変数キャプチャとスコープ(実質的final)

int base = 10; // 実質的final(以後、再代入しない)
Function<Integer,Integer> addBase = x -> x + base;  // 参照可

// base = 20; // ←再代入するとコンパイルエラー(キャプチャは実質的finalが条件)
  • ラムダ内のthis外側のインスタンス(匿名クラスと挙動が異なる点に注意)。
  • ループ変数をキャプチャする際は意図せぬ共有に注意。必要ならローカルコピーを作ります。

8. Checked例外への対処

// 1) その場でtry-catchしてラップ
Function<String, Integer> toIntSafe = s -> {
    try { return Integer.parseInt(s); }
    catch (NumberFormatException e) { return 0; }
};

// 2) 補助メソッドで包む(関数型インターフェースを自作)
@FunctionalInterface interface ThrowingFunc<T,R> { R apply(T t) throws Exception; }
static <T,R> Function<T,R> wrap(ThrowingFunc<T,R> f) {
    return t -> {
        try { return f.apply(t); }
        catch (Exception e) { throw new RuntimeException(e); }
    };
}
// 使用例
var lines = java.util.List.of("1","x","3");
var ints  = lines.stream().map(wrap(Integer::parseInt)).toList();
  • どこでログ/ハンドリングするかレイヤ方針を統一します(多重ログ禁止)。

9. パフォーマンスと注意点

  • ボクシング/アンボクシングmapToInt 等のプリミティブStreamを活用。
  • 状態を持つラムダは避ける(並列処理で破綻)。必要ならCollectorや外部同期。
  • 小さなコレクションではStreamより従来forが速いことも。可読性と頻度で選択。
  • parallel()は乱用しない(計算/IO境界、分割コスト、順序要件を検討)。

10. 実用サンプル(1ファイル)

import java.util.*;
import java.util.function.*;
import java.util.stream.*;

public class LambdaDemo {

    @FunctionalInterface
    interface ThrowingFunc<T, R> { R apply(T t) throws Exception; }

    static <T, R> Function<T, R> wrap(ThrowingFunc<T, R> f) {
        return t -> {
            try { return f.apply(t); }
            catch (Exception e) { throw new RuntimeException(e); }
        };
    }

    public static void main(String[] args) {
        // 合成と述語
        Predicate<String> notBlank = s -> s != null && !s.isBlank();
        Predicate<String> longStr  = s -> s.length() >= 4;
        Predicate<String> ok       = notBlank.and(longStr);

        // 変換パイプライン
        List<String> raw = List.of(" apple ", "  kiwi", "", "pear  ", "x");
        List<String> filtered = raw.stream()
                .map(String::trim)
                .filter(ok)
                .sorted(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()))
                .toList();
        System.out.println(filtered); // [kiwi, pear, apple]

        // Comparator + メソッド参照
        record User(String name, int age) {}
        List<User> users = new ArrayList<>(List.of(
            new User("Alice", 30), new User("Bob", 25), new User("Bob", 40)
        ));
        users.sort(Comparator.comparing(User::name).thenComparingInt(User::age));
        System.out.println(users);

        // 基本型ストリームで合計(ボクシング回避)
        int totalLen = filtered.stream().mapToInt(String::length).sum();
        System.out.println(totalLen);

        // Checked例外を補助メソッドで包む例
        List<String> nums = List.of("10","x","20");
        List<Integer> parsed = nums.stream()
                .map(s -> {
                    try { return Integer.parseInt(s); }
                    catch (NumberFormatException e) { return 0; }
                })
                .toList();
        System.out.println(parsed); // [10, 0, 20]

        // wrapを使う例(RuntimeExceptionでラップ)
        List<Integer> parsed2 = nums.stream().map(wrap(Integer::parseInt))
                .map(n -> n >= 0 ? n : 0) // ここでは単純化
                .mapToInt(Integer::intValue).boxed().toList();
        System.out.println(parsed2);
    }
}
  • 要点総合:関数合成・Comparator・プリミティブStream・Checked例外の取り扱いを一通り確認できます。

ベストプラクティス要点

  • 関数型インターフェースを前提に、メソッド参照を積極活用する。
  • 副作用の少ないラムダで宣言的に記述し、テスト可能性を高める。
  • 実質的finalの原則を守り、キャプチャ変数の再代入を避ける。
  • Checked例外はその場で包むヘルパーで統一的に扱う(多重ログ禁止)。
  • パフォーマンスはボクシング回避・過度なparallel禁止・小規模はfor優先を指針とする。