目次
要約
- ラムダ式は関数型インターフェース(抽象メソッドが1つ)の実装を簡潔に記述するための構文です。
- メソッド参照(
Class::method
)で可読性が向上します。 java.util.function
の Predicate / 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優先を指針とする。