Skip to content

拡張forループ vs Stream API 比較

拡張forループ vs Stream API 完全比較

Section titled “拡張forループ vs Stream API 完全比較”

Javaでのコレクション操作において、拡張forループ(Enhanced For Loop)とStream APIの使い分けと比較を詳しく解説します。

拡張forループ(Enhanced For Loop)は、Java 5で導入された構文で、コレクションや配列を簡単に反復処理できます。

// 拡張forループの構文
for (要素の型 変数名 : コレクションまたは配列) {
// 処理
}

Stream APIは、Java 8で導入された関数型プログラミングスタイルでコレクションを操作するAPIです。

// Stream APIの構文
コレクション.stream()
.中間操作()
.終端操作()
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
for (String name : names) {
System.out.println(name);
}
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream()
.forEach(System.out::println);

比較:

  • 拡張forループ: シンプルで直感的、副作用を起こしやすい
  • Stream API: 関数型スタイル、副作用を避けやすい
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> evens = new ArrayList<>();
for (Integer number : numbers) {
if (number % 2 == 0) {
evens.add(number);
}
}
// 結果: [2, 4, 6, 8, 10]
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> evens = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
// 結果: [2, 4, 6, 8, 10]

比較:

  • 拡張forループ: 手続き的、中間コレクションが必要
  • Stream API: 宣言的、何をしたいかが明確
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> upperNames = new ArrayList<>();
for (String name : names) {
upperNames.add(name.toUpperCase());
}
// 結果: [ALICE, BOB, CHARLIE]
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> upperNames = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
// 結果: [ALICE, BOB, CHARLIE]

比較:

  • 拡張forループ: 明示的な変換処理が必要
  • Stream API: 関数型スタイルで簡潔

例4: 複数の条件と変換を組み合わせる

Section titled “例4: 複数の条件と変換を組み合わせる”
List<User> users = Arrays.asList(
new User("Alice", 25, true),
new User("Bob", 17, true),
new User("Charlie", 30, false),
new User("David", 22, true)
);
List<String> activeAdultNames = new ArrayList<>();
for (User user : users) {
if (user.isActive() && user.getAge() >= 18) {
activeAdultNames.add(user.getName().toUpperCase());
}
}
// 結果: [ALICE, DAVID]
List<User> users = Arrays.asList(
new User("Alice", 25, true),
new User("Bob", 17, true),
new User("Charlie", 30, false),
new User("David", 22, true)
);
List<String> activeAdultNames = users.stream()
.filter(User::isActive)
.filter(u -> u.getAge() >= 18)
.map(User::getName)
.map(String::toUpperCase)
.collect(Collectors.toList());
// 結果: [ALICE, DAVID]

比較:

  • 拡張forループ: ネストが深くなりやすい、可読性が低下
  • Stream API: パイプラインで処理が明確、可読性が高い
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = 0;
for (Integer number : numbers) {
sum += number;
}
// 結果: 15
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.mapToInt(Integer::intValue)
.sum();
// 結果: 15
// または
int sum2 = numbers.stream()
.reduce(0, Integer::sum);
// 結果: 15

比較:

  • 拡張forループ: 変数の状態管理が必要
  • Stream API: 関数型スタイルで副作用がない
List<User> users = Arrays.asList(
new User("Alice", 25, "Engineering"),
new User("Bob", 30, "Engineering"),
new User("Charlie", 22, "Sales"),
new User("David", 28, "Sales")
);
Map<String, List<User>> byDepartment = new HashMap<>();
for (User user : users) {
String dept = user.getDepartment();
byDepartment.computeIfAbsent(dept, k -> new ArrayList<>())
.add(user);
}
// 結果: {Engineering=[Alice, Bob], Sales=[Charlie, David]}
List<User> users = Arrays.asList(
new User("Alice", 25, "Engineering"),
new User("Bob", 30, "Engineering"),
new User("Charlie", 22, "Sales"),
new User("David", 28, "Sales")
);
Map<String, List<User>> byDepartment = users.stream()
.collect(Collectors.groupingBy(User::getDepartment));
// 結果: {Engineering=[Alice, Bob], Sales=[Charlie, David]}

比較:

  • 拡張forループ: 複雑なロジックが必要、エラーが起きやすい
  • Stream API: 簡潔で読みやすい、エラーが起きにくい
// 拡張forループ: オーバーヘッドが少ない
List<String> names = // 100件のデータ
for (String name : names) {
// 処理
}
// Stream API: オーバーヘッドがあるが、実用上問題なし
names.stream()
.forEach(name -> {
// 処理
});

結果: 実用上、パフォーマンス差はほとんどない

// 拡張forループ: 順次処理のみ
List<Integer> numbers = // 10万件のデータ
List<Integer> evens = new ArrayList<>();
for (Integer number : numbers) {
if (number % 2 == 0) {
evens.add(number);
}
}
// Stream API: 並列処理が可能
List<Integer> evens = numbers.parallelStream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());

結果: 大規模データでは、Stream APIの並列処理が有利

  1. シンプルな反復処理

    // 各要素を出力するだけ
    for (String name : names) {
    System.out.println(name);
    }
  2. 副作用が必要な場合

    // 外部変数を変更する必要がある
    int count = 0;
    for (User user : users) {
    if (user.isActive()) {
    count++;
    processUser(user); // 副作用
    }
    }
  3. 早期終了が必要な場合

    // 条件を満たしたら処理を終了
    for (User user : users) {
    if (user.getId() == targetId) {
    return user;
    }
    }
  4. パフォーマンスが極めて重要な場合

    // 非常に小さなデータセットで、オーバーヘッドを避けたい
    for (int i = 0; i < 10; i++) {
    // 処理
    }
  1. 複雑な変換処理

    // フィルタリング、変換、集約を組み合わせる
    List<String> result = users.stream()
    .filter(User::isActive)
    .map(User::getName)
    .map(String::toUpperCase)
    .sorted()
    .collect(Collectors.toList());
  2. 関数型スタイルを維持したい場合

    // 副作用を避け、不変性を保つ
    List<Integer> doubled = numbers.stream()
    .map(n -> n * 2)
    .collect(Collectors.toList());
  3. 並列処理が必要な場合

    // 大規模データの並列処理
    List<ProcessedData> result = largeDataSet.parallelStream()
    .map(this::process)
    .collect(Collectors.toList());
  4. 複雑な集約処理

    // グループ化、集計など
    Map<String, Double> averages = users.stream()
    .collect(Collectors.groupingBy(
    User::getDepartment,
    Collectors.averagingInt(User::getAge)
    ));

シナリオ1: ユーザーリストの処理

Section titled “シナリオ1: ユーザーリストの処理”
  • アクティブなユーザーのみを抽出
  • 18歳以上のみを抽出
  • 名前を大文字に変換
  • アルファベット順にソート
public List<String> getActiveAdultNames(List<User> users) {
List<User> activeUsers = new ArrayList<>();
for (User user : users) {
if (user.isActive() && user.getAge() >= 18) {
activeUsers.add(user);
}
}
List<String> names = new ArrayList<>();
for (User user : activeUsers) {
names.add(user.getName().toUpperCase());
}
Collections.sort(names);
return names;
}
public List<String> getActiveAdultNames(List<User> users) {
return users.stream()
.filter(User::isActive)
.filter(u -> u.getAge() >= 18)
.map(User::getName)
.map(String::toUpperCase)
.sorted()
.collect(Collectors.toList());
}

比較:

  • 拡張forループ: コードが長い、中間コレクションが必要、可読性が低い
  • Stream API: 簡潔、パイプラインで処理が明確、可読性が高い
  • 部門ごとにグループ化
  • 各部門の平均年齢を計算
  • 平均年齢が30以上の部門のみを抽出
public Map<String, Double> getDepartmentsWithHighAverageAge(List<User> users) {
Map<String, List<User>> byDepartment = new HashMap<>();
for (User user : users) {
String dept = user.getDepartment();
byDepartment.computeIfAbsent(dept, k -> new ArrayList<>())
.add(user);
}
Map<String, Double> averages = new HashMap<>();
for (Map.Entry<String, List<User>> entry : byDepartment.entrySet()) {
int totalAge = 0;
for (User user : entry.getValue()) {
totalAge += user.getAge();
}
double average = (double) totalAge / entry.getValue().size();
if (average >= 30) {
averages.put(entry.getKey(), average);
}
}
return averages;
}
public Map<String, Double> getDepartmentsWithHighAverageAge(List<User> users) {
return users.stream()
.collect(Collectors.groupingBy(
User::getDepartment,
Collectors.averagingInt(User::getAge)
))
.entrySet().stream()
.filter(entry -> entry.getValue() >= 30)
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue
));
}

比較:

  • 拡張forループ: 複雑、エラーが起きやすい、保守が困難
  • Stream API: 簡潔、読みやすい、保守しやすい

7. パフォーマンスベンチマーク

Section titled “7. パフォーマンスベンチマーク”
  • データサイズ: 100万件
  • 処理: フィルタリング + 変換 + 集約
// 拡張forループ
long start = System.currentTimeMillis();
List<String> result1 = new ArrayList<>();
for (User user : users) {
if (user.isActive() && user.getAge() >= 18) {
result1.add(user.getName().toUpperCase());
}
}
long time1 = System.currentTimeMillis() - start;
// Stream API(順次)
start = System.currentTimeMillis();
List<String> result2 = users.stream()
.filter(User::isActive)
.filter(u -> u.getAge() >= 18)
.map(User::getName)
.map(String::toUpperCase)
.collect(Collectors.toList());
long time2 = System.currentTimeMillis() - start;
// Stream API(並列)
start = System.currentTimeMillis();
List<String> result3 = users.parallelStream()
.filter(User::isActive)
.filter(u -> u.getAge() >= 18)
.map(User::getName)
.map(String::toUpperCase)
.collect(Collectors.toList());
long time3 = System.currentTimeMillis() - start;

結果(参考値):

  • 拡張forループ: 約150ms
  • Stream API(順次): 約180ms
  • Stream API(並列): 約80ms(CPUコア数に依存)

A: プロジェクトの方針と要件に応じて選択します。

  • シンプルな処理: 拡張forループ
  • 複雑な処理: Stream API
  • 並列処理が必要: Stream API

Q2: パフォーマンスはどちらが良いか?

Section titled “Q2: パフォーマンスはどちらが良いか?”

A:

  • 小規模データ: 実用上、差はほとんどない
  • 大規模データ: Stream APIの並列処理が有利
  • 極小規模データ: 拡張forループがわずかに有利

A:

  • シンプルな処理: 拡張forループが直感的
  • 複雑な処理: Stream APIが宣言的で読みやすい

メリット:

  • シンプルで直感的
  • オーバーヘッドが少ない
  • 早期終了が可能
  • 副作用を起こしやすい(メリットでもある)

デメリット:

  • 複雑な処理ではコードが長くなる
  • 中間コレクションが必要
  • 並列処理が困難

メリット:

  • 宣言的で読みやすい
  • パイプライン処理が可能
  • 並列処理が容易
  • 関数型スタイルで副作用を避けやすい

デメリット:

  • オーバーヘッドがある(実用上は問題なし)
  • 学習コストがある
  • 早期終了が難しい
状況推奨方法理由
シンプルな反復処理拡張forループ直感的で読みやすい
複雑な変換処理Stream APIパイプラインで処理が明確
並列処理が必要Stream APIparallelStream()が利用可能
早期終了が必要拡張forループbreakが使用可能
副作用が必要拡張forループ外部変数の変更が容易
関数型スタイルを維持Stream API不変性を保てる

実務では、複雑な処理にはStream API、シンプルな処理には拡張forループを使い分けることが推奨されます。