Java 8の関数型プログラミング特性
Java 8では、言語仕様に大きな変更が加わり、関数型プログラミングのパラダイムが導入されました。主な新機能には以下が含まれます。
- Lambda式(クロージャ)
- 関数型インターフェース
- Stream API
- Optionalクラス
- 新しいDate and Time API
中でもLambda式は、コードの簡潔性と可読性を飛躍的に向上させる最も重要な機能です。これにより、メソッドの引数として関数(振る舞い)を渡すことが容易になりました。
Lambda式の基本構文と特徴
Lambda式の基本的な構文は以下の通りです。
(parameters) -> expression
// または
(parameters) -> { statements; }
主な特徴:
- 型推論:パラメータの型宣言は省略可能で、コンパイラが文脈から推論します。
- 括弧の省略:パラメータが1つだけの場合、丸括弧
()を省略できます。 - 中括弧とreturnの省略:本体が単一の式である場合、中括弧
{}とreturnキーワードを省略でき、その式の値が自動的に返されます。
Lambda式の実装例
以下は、さまざまなパターンを示すLambda式の例です。
// 1. パラメータなし、定数を返す
() -> 42
// 2. 単一パラメータ、2乗して返す(型と括弧を省略)
n -> n * n
// 3. 2つのパラメータ、文字列を結合する
(str1, str2) -> str1 + " " + str2
// 4. 明示的な型宣言と複数ステートメント
(int base, int exponent) -> {
int result = 1;
for(int i = 0; i < exponent; i++) result *= base;
return result;
}
// 5. 戻り値なし(void)、コンソールに出力
(String msg) -> System.out.println("Log: " + msg)
カスタムインターフェースでの利用
public class LambdaDemo {
public static void main(String[] args) {
// 明示的な型宣言あり
TextFormatter upperCaseFormatter = (String text) -> text.toUpperCase();
// 型推論とreturnの省略
TextFormatter reverseFormatter = (text) -> new StringBuilder(text).reverse().toString();
System.out.println(processText("hello", upperCaseFormatter));
System.out.println(processText("world", reverseFormatter));
// 単一パラメータによる括弧の省略
Notifier consoleNotifier = message -> System.out.println("Notification: " + message);
consoleNotifier.send("System started.");
}
interface TextFormatter {
String format(String input);
}
interface Notifier {
void send(String message);
}
private static String processText(String rawText, TextFormatter formatter) {
return formatter.format(rawText);
}
}
変数のスコープとEffective Finalの制約
Lambda式内部から外部のローカル変数を参照する場合、その変数は final または effectively final(実質的にfinal、つまり初期化後に変更されない)である必要があります。
public class ScopeDemo {
public static void main(String[] args) {
String prefix = "Data: "; // effectively final
Processor proc = value -> System.out.println(prefix + value);
proc.execute(100);
// prefix = "New Data: "; // これをアンコメントするとコンパイルエラーになる
}
interface Processor {
void execute(int value);
}
}
なぜEffectively Finalが必要なのか?
ローカル変数はスタックメモリに割り当てられ、メソッドの実行が終了すると破棄されます。一方、Lambda式(や匿名クラス)は別のスレッドや遅延実行コンテキストで評価される可能性があります。変数がスタックから消えた後にアクセスすると不整合が生じるため、JavaコンパイラはLambdaが参照するローカル変数の値を内部でコピー(キャプチャ)します。このキャプチャされた値が変更可能だと、元の値とコピーの値の同期問題が発生するため、不変性(final)が強制されます。
外部変数を変更したい場合のワークアラウンド
どうしてもLambda式内で外部の状態を変更したい場合は、配列やアトミック変数、あるいはミュータブルなオブジェクトを使用します。
// 配列を使用したワークアラウンド
int[] counter = {0};
List<String> items = Arrays.asList("A", "B", "C");
items.forEach(item -> {
counter[0]++;
});
System.out.println("Total items: " + counter[0]);
// AtomicIntegerを使用したスレッドセーフなワークアラウンド
AtomicInteger atomicCount = new AtomicInteger(0);
items.forEach(item -> atomicCount.incrementAndGet());
関数型インターフェースと匿名クラスの代替
抽象メソッドをちょうど1つだけ持つインターフェースは「関数型インターフェース」と呼ばれ、@FunctionalInterface アノテーションで明示できます(必須ではありませんが、意図を明確にするために推奨されます)。
Java 8以前では、このようなインターフェースを実装するために冗長な匿名内部クラスを使用していました。
// Java 8以前の冗長な書き方
Thread oldThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Running in old anonymous class");
}
});
// Java 8のLambda式による簡潔な書き方
Thread newThread = new Thread(() -> System.out.println("Running in Lambda expression"));
newThread.start();
Stream APIとコレクション操作の実践
Lambda式はStream APIと組み合わせることで、コレクションに対する複雑な操作を宣言的に記述できます。以下の例では、商品(Product)データを扱います。
@Data
@AllArgsConstructor
public class Product {
private int id;
private String name;
private String category;
private double price;
}
1. コレクションの遍历とフィルタリング
List<Product> inventory = Arrays.asList(
new Product(1, "Laptop", "Electronics", 1200.00),
new Product(2, "Mouse", "Electronics", 25.50),
new Product(3, "Desk Chair", "Furniture", 150.00),
new Product(4, "Monitor", "Electronics", 300.00),
new Product(5, "Bookshelf", "Furniture", 80.00)
);
// forEachによる単純な遍历
inventory.forEach(p -> System.out.println(p.getName()));
// Stream APIによるフィルタリングとマッピング
List<String> expensiveElectronics = inventory.stream()
.filter(p -> "Electronics".equals(p.getCategory()))
.filter(p -> p.getPrice() > 100.0)
.map(Product::getName)
.collect(Collectors.toList());
System.out.println("High-end Electronics: " + expensiveElectronics);
2. Mapの操作とエントリのフィルタリング
Map<String, Integer> stockLevels = new HashMap<>();
stockLevels.put("Laptop", 15);
stockLevels.put("Mouse", 120);
stockLevels.put("Desk Chair", 8);
stockLevels.put("Monitor", 25);
// Java 8のMap#forEach
stockLevels.forEach((item, qty) -> System.out.println(item + ": " + qty + " in stock"));
// 在庫が20未満のアイテムを抽出して新しいMapを生成
Map<String, Integer> lowStockItems = stockLevels.entrySet().stream()
.filter(entry -> entry.getValue() < 20)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
System.out.println("Low stock alert: " + lowStockItems);
3. グループ化、パーティショニング、集計
// カテゴリごとのグルーピング
Map<String, List<Product>> productsByCategory = inventory.stream()
.collect(Collectors.groupingBy(Product::getCategory));
// 価格の合計と平均の計算
DoubleSummaryStatistics priceStats = inventory.stream()
.mapToDouble(Product::getPrice)
.summaryStatistics();
System.out.println("Total Inventory Value: $" + priceStats.getSum());
System.out.println("Average Product Price: $" + priceStats.getAverage());
// カテゴリごとの価格合計
Map<String, Double> totalValueByCategory = inventory.stream()
.collect(Collectors.groupingBy(
Product::getCategory,
Collectors.summingDouble(Product::getPrice)
));
4. ソートとトップNの取得
// 価格の降順でソートし、上位2件を取得
List<Product> topTwoExpensive = inventory.stream()
.sorted(Comparator.comparingDouble(Product::getPrice).reversed())
.limit(2)
.collect(Collectors.toList());
// 名前の昇順でソート
List<Product> sortedByName = inventory.stream()
.sorted(Comparator.comparing(Product::getName))
.collect(Collectors.toList());
5. OptionalによるNullセーフな処理
Optional は、NullPointerExceptionを防ぎ、値が存在しない場合の処理を優雅に記述するためのコンテナです。
Optional<Product> foundProduct = inventory.stream()
.filter(p -> p.getId() == 99)
.findFirst();
// 値が存在する場合のみ実行
foundProduct.ifPresent(p -> System.out.println("Found: " + p.getName()));
// デフォルト値の提供
Product defaultProduct = foundProduct.orElse(new Product(0, "Unknown", "N/A", 0.0));
// 値が存在しない場合に例外をスロー
Product requiredProduct = inventory.stream()
.filter(p -> p.getName().equals("Tablet"))
.findFirst()
.orElseThrow(() -> new NoSuchElementException("Tablet not found in inventory"));
6. 条件マッチング
// いずれかの商品が1000ドル以上か?
boolean hasExpensiveItem = inventory.stream()
.anyMatch(p -> p.getPrice() >= 1000.0);
// すべての商品が10ドル以上か?
boolean allAboveTen = inventory.stream()
.allMatch(p -> p.getPrice() > 10.0);
// 家具カテゴリの商品が一つもないか?
boolean noFurniture = inventory.stream()
.noneMatch(p -> "Furniture".equals(p.getCategory()));