こんにちは!バックエンドエンジニアのキョンです。今回は、Java 8から追加されたStream APIについて、実務での経験を元に詳しく解説していきます。
目次
1. Stream APIとは?
導入背景
正直に言うと、私も最初はStream APIの必要性がピンときませんでした。forループで十分じゃない?って思ってたんです。
でも、実際の開発現場で大量のデータを扱う中で、その真価に気づきました。特に:
- コードの可読性向上
- 並列処理の簡単な実装
- 関数型プログラミングの恩恵
これらが実感できたんです。
2. 基本的な使い方
コレクションからStreamの生成
List<String> names = Arrays.asList("田中", "鈴木", "佐藤");
// Stream生成
Stream<String> stream = names.stream();
よく使う中間操作
List<User> users = getUserList(); // ユーザーリストを取得
users.stream()
.filter(user -> user.getAge() >= 20) // 20歳以上をフィルタリング
.map(User::getName) // 名前を取得
.sorted() // ソート
.distinct() // 重複除去
.forEach(System.out::println); // 出力
3. 実践的なユースケース
ケース1: データの集計
実際の案件で使用した売上集計の例です:
public class SalesAggregator {
public Map<String, Double> aggregateSalesByProduct(List<Sale> sales) {
return sales.stream()
.collect(Collectors.groupingBy(
Sale::getProductName,
Collectors.summingDouble(Sale::getAmount)
));
}
public DoubleSummaryStatistics getSalesStatistics(List<Sale> sales) {
return sales.stream()
.mapToDouble(Sale::getAmount)
.summaryStatistics();
}
}
ケース2: データ変換処理
API連携でよく使うデータ変換処理です:
public class DataTransformer {
public List<UserDTO> transformUsers(List<User> users) {
return users.stream()
.filter(user -> user.getStatus().equals("ACTIVE"))
.map(user -> new UserDTO(
user.getId(),
user.getFullName(),
user.getEmail(),
user.getAge()
))
.collect(Collectors.toList());
}
}
ケース3: 並列処理
大量データ処理での実践例です:
public class BatchProcessor {
public void processLargeData(List<Data> dataList) {
dataList.parallelStream()
.filter(this::isValid)
.map(this::processData)
.forEach(this::saveToDatabase);
}
private boolean isValid(Data data) {
// バリデーションロジック
return data != null && data.getValue() != null;
}
private ProcessedData processData(Data data) {
// 重い処理
return new ProcessedData(data);
}
}
4. パフォーマンスとベストプラクティス
パフォーマンス最適化のコツ
実務で学んだ最適化のポイントです:
// 悪い例:不必要なStream生成
list.stream().forEach(item -> {
// 処理
});
// 良い例:直接forEachを使用
list.forEach(item -> {
// 処理
});
// 悪い例:複数回のStream生成
long count = list.stream().filter(x -> x > 0).count();
double avg = list.stream().filter(x -> x > 0).mapToInt(x -> x).average().orElse(0);
// 良い例:Stream再利用
Stream<Integer> filteredStream = list.stream().filter(x -> x > 0);
long count = filteredStream.count();
double avg = filteredStream.mapToInt(x -> x).average().orElse(0);
メモリ使用量の最適化
public class LargeDataProcessor {
public void processLargeFile(String filePath) {
try (Stream<String> lines = Files.lines(Paths.get(filePath))) {
lines.filter(line -> !line.isEmpty())
.map(this::processLine)
.forEach(this::saveResult);
} catch (IOException e) {
logger.error("ファイル処理エラー", e);
}
}
}
5. よくあるミスと解決策
Stream再利用の罠
// 悪い例:Streamの再利用
Stream<String> stream = list.stream();
stream.forEach(System.out::println);
stream.forEach(System.out::println); // IllegalStateException!
// 良い例:必要に応じて新しいStreamを生成
list.stream().forEach(System.out::println);
list.stream().forEach(System.out::println);
並列処理での注意点
// 悪い例:副作用のある処理を並列実行
List<String> result = Collections.synchronizedList(new ArrayList<>());
stream.parallel().forEach(result::add); // 順序が保証されない!
// 良い例:collectを使用
List<String> result = stream.parallel()
.collect(Collectors.toList());
まとめ
Stream APIは、使いこなせると本当に便利なツールです。私の経験上、以下のポイントを押さえておくと良いでしょう:
- 基本的な操作(filter, map, collect)を完璧に使えるようにする
- パフォーマンスを意識した使い方を心がける
- 並列処理の特性を理解する
- 適材適所を見極める(すべてをStreamで書く必要はない)
今後の学習のために
- Java Stream APIのドキュメント
- Java 8 in Action(書籍)
- モダンJava実践ガイド
次回は「Java Lambda式の実践的な使い方」について解説する予定です。お楽しみに!
コメント