コレクションの反復処理

Java コレクションを効率的に走査するための様々な方法

コレクション反復処理の概要

Java のコレクションフレームワークでは、コレクション内の要素を走査するための様々な方法が提供されています。それぞれのアプローチには長所と短所があり、状況に応じて適切な方法を選択することが重要です。

主な反復処理の方法

  • for-each ループ: Java 5以降で導入された拡張for文
  • Iterator: 伝統的な反復子パターンの実装
  • ListIterator: List専用の双方向イテレータ
  • 従来のfor文: インデックスベースのアクセス(List専用)
  • Stream API: Java 8以降の関数型アプローチ

for-each ループ(拡張for文)

Java 5で導入された拡張for文(for-each ループ)は、コレクションを走査する最も簡潔で読みやすい方法です。内部的には Iterator を使用していますが、その詳細を隠蔽しています。

for-each ループの基本的な使用例

import java.util.List;
import java.util.ArrayList;

public class ForEachExample {
    public static void main(String[] args) {
        List<String> fruits = new ArrayList<>();
        fruits.add("りんご");
        fruits.add("バナナ");
        fruits.add("オレンジ");
        
        // for-each ループを使用した反復処理
        for (String fruit : fruits) {
            System.out.println(fruit);
        }
    }
}

for-each ループの特徴

  • メリット:
    • 簡潔で読みやすいコード
    • インデックス管理が不要
    • 配列とコレクションの両方で使用可能
  • 制限事項:
    • 反復中にコレクションを変更できない(ConcurrentModificationException が発生)
    • 要素の位置(インデックス)情報が不要な場合のみ適している
    • 反復方向を制御できない(常に先頭から末尾へ)

Iterator インターフェース

Iterator は Java コレクションフレームワークの基本的な反復メカニズムで、すべてのコレクションが iterator() メソッドを提供しています。Iterator を使用すると、反復処理中に要素の削除が可能になります。

Iterator の基本的な使用例

import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;

public class IteratorExample {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>();
        names.add("田中");
        names.add("佐藤");
        names.add("鈴木");
        
        // Iterator を使用した反復処理
        Iterator<String> iterator = names.iterator();
        while (iterator.hasNext()) {
            String name = iterator.next();
            System.out.println(name);
            
            // 条件に基づいて要素を安全に削除
            if (name.equals("佐藤")) {
                iterator.remove();  // 現在の要素を削除
            }
        }
        
        System.out.println("削除後のリスト: " + names);  // [田中, 鈴木]
    }
}

Iterator の特徴

  • メソッド:
    • hasNext(): 次の要素が存在するかを確認
    • next(): 次の要素を取得し、イテレータを進める
    • remove(): 最後に返された要素を削除
    • forEachRemaining(Consumer): Java 8以降、残りの要素に対して操作を実行
  • メリット:
    • 反復中に安全に要素を削除できる
    • すべてのコレクション型で一貫して使用可能
  • 制限事項:
    • for-each ループよりも冗長
    • 前方向への反復のみ(後方には戻れない)

ListIterator インターフェース

ListIterator は Iterator を拡張し、List コレクション専用の機能を提供します。双方向の反復処理や、要素の追加・置換などの操作が可能です。

ListIterator の基本的な使用例

import java.util.List;
import java.util.ArrayList;
import java.util.ListIterator;

public class ListIteratorExample {
    public static void main(String[] args) {
        List<String> colors = new ArrayList<>();
        colors.add("赤");
        colors.add("緑");
        colors.add("青");
        
        // ListIterator を使用した双方向反復処理
        ListIterator<String> listIterator = colors.listIterator();
        
        // 前方向への反復
        System.out.println("前方向への反復:");
        while (listIterator.hasNext()) {
            int index = listIterator.nextIndex();
            String color = listIterator.next();
            System.out.println("インデックス " + index + ": " + color);
        }
        
        // 後方向への反復
        System.out.println("後方向への反復:");
        while (listIterator.hasPrevious()) {
            int index = listIterator.previousIndex();
            String color = listIterator.previous();
            System.out.println("インデックス " + index + ": " + color);
            
            // 要素の置換
            if (color.equals("緑")) {
                listIterator.set("黄緑");
            }
            
            // 要素の追加
            if (color.equals("赤")) {
                listIterator.add("オレンジ");
            }
        }
        
        System.out.println("変更後のリスト: " + colors);  // [赤, オレンジ, 黄緑, 青]
    }
}

ListIterator の特徴

  • 追加メソッド:
    • hasPrevious(): 前の要素が存在するかを確認
    • previous(): 前の要素を取得し、イテレータを戻す
    • nextIndex(): 次に返される要素のインデックスを取得
    • previousIndex(): 前に返される要素のインデックスを取得
    • add(E): 指定された要素をリストに追加
    • set(E): 最後に返された要素を置換
  • メリット:
    • 双方向の反復処理が可能
    • 要素の追加・置換・削除が可能
    • 現在の位置のインデックス情報が利用可能
  • 制限事項:
    • List インターフェースを実装したコレクションでのみ使用可能
    • Iterator よりも複雑

従来のfor文(インデックスベース)

インデックスを持つコレクション(主に List)では、従来の for ループを使用して要素にアクセスすることも可能です。特にインデックス情報が重要な場合に有用です。

インデックスベースの for ループの例

import java.util.List;
import java.util.ArrayList;

public class IndexBasedForLoopExample {
    public static void main(String[] args) {
        List<String> cities = new ArrayList<>();
        cities.add("東京");
        cities.add("大阪");
        cities.add("名古屋");
        cities.add("福岡");
        
        // インデックスベースの for ループ
        for (int i = 0; i < cities.size(); i++) {
            System.out.println("都市 #" + (i + 1) + ": " + cities.get(i));
        }
        
        // 逆順の反復
        System.out.println("逆順:");
        for (int i = cities.size() - 1; i >= 0; i--) {
            System.out.println("都市 #" + (i + 1) + ": " + cities.get(i));
        }
        
        // 一部の要素のみを処理
        System.out.println("偶数インデックスの要素のみ:");
        for (int i = 0; i < cities.size(); i += 2) {
            System.out.println(cities.get(i));
        }
    }
}

インデックスベース for ループの特徴

  • メリット:
    • インデックス情報が利用可能
    • 反復の方向や範囲を柔軟に制御可能
    • 複数のコレクションを同時に処理する場合に便利
  • 制限事項:
    • List インターフェースを実装したコレクションでのみ効率的
    • LinkedList などでは get(i) 操作が O(n) となり非効率
    • コードが冗長になりがち
    • インデックス管理のミスによるバグが発生しやすい

Stream API(Java 8以降)

Java 8 で導入された Stream API は、コレクションに対する関数型プログラミングスタイルの操作を提供します。データの変換、フィルタリング、集約などの複雑な処理を簡潔に記述できます。

Stream API の基本的な使用例

import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;

public class StreamExample {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>();
        names.add("山田太郎");
        names.add("鈴木一郎");
        names.add("佐藤花子");
        names.add("田中次郎");
        names.add("高橋三郎");
        
        // フィルタリング: "田"を含む名前を抽出
        List<String> filteredNames = names.stream()
                .filter(name -> name.contains("田"))
                .collect(Collectors.toList());
        System.out.println("「田」を含む名前: " + filteredNames);
        
        // マッピング: 名前の長さを取得
        List<Integer> nameLengths = names.stream()
                .map(String::length)
                .collect(Collectors.toList());
        System.out.println("名前の長さ: " + nameLengths);
        
        // 集約: 名前の平均長を計算
        double averageLength = names.stream()
                .mapToInt(String::length)
                .average()
                .orElse(0);
        System.out.println("名前の平均長: " + averageLength);
        
        // 並列処理: 大規模データセットの処理を高速化
        boolean anyLongName = names.parallelStream()
                .anyMatch(name -> name.length() > 4);
        System.out.println("4文字より長い名前があるか: " + anyLongName);
    }
}

Stream API の特徴

  • 主な操作:
    • 中間操作: filter, map, flatMap, sorted, distinct, limit など
    • 終端操作: forEach, collect, reduce, count, min, max, anyMatch など
  • メリット:
    • 宣言的で読みやすいコード
    • 複雑なデータ処理を簡潔に記述可能
    • 並列処理(parallelStream)が容易
    • 遅延評価による効率的な処理
  • 制限事項:
    • 学習曲線がやや急
    • デバッグが従来のループより複雑な場合がある
    • 小規模なデータセットでは従来の方法より若干オーバーヘッドが大きい

反復処理方法の選択ガイド

反復方法 最適な使用シナリオ 避けるべきシナリオ
for-each ループ
  • シンプルな反復処理
  • 要素の読み取りのみ
  • インデックスが不要な場合
  • 反復中に要素を削除する必要がある場合
  • インデックス情報が必要な場合
Iterator
  • 反復中に要素を削除する場合
  • Map や Set など非インデックスコレクション
  • シンプルな読み取り専用の反復
  • 双方向の反復が必要な場合
ListIterator
  • 双方向の反復が必要な場合
  • 反復中に要素の追加・置換が必要な場合
  • インデックス情報が必要な場合
  • List 以外のコレクション
  • シンプルな前方向のみの反復
インデックスベース for ループ
  • ArrayList などのランダムアクセスが効率的なリスト
  • インデックスが重要な処理
  • 逆順や特定のパターンでの反復
  • LinkedList などのシーケンシャルアクセスのリスト
  • Map や Set など非インデックスコレクション
Stream API
  • 複雑なデータ処理(フィルタリング、マッピング、集約)
  • 大規模データセットの並列処理
  • 関数型プログラミングスタイルを好む場合
  • 非常にシンプルな反復処理
  • 反復中にコレクションを変更する必要がある場合

パフォーマンスの考慮事項

コレクション型とアクセスパターン

  • ArrayList: インデックスベースのアクセスが効率的(O(1))
  • LinkedList: シーケンシャルアクセス(Iterator/ListIterator)が効率的、ランダムアクセスは非効率(O(n))
  • HashSet/HashMap: Iterator を使用した反復が最適

LinkedList での効率的な反復処理

import java.util.LinkedList;
import java.util.List;

public class LinkedListIterationPerformance {
    public static void main(String[] args) {
        List<Integer> numbers = new LinkedList<>();
        for (int i = 0; i < 100000; i++) {
            numbers.add(i);
        }
        
        long startTime, endTime;
        
        // 非効率的: インデックスベースのアクセス - O(n²)
        startTime = System.currentTimeMillis();
        int sum1 = 0;
        for (int i = 0; i < numbers.size(); i++) {
            sum1 += numbers.get(i);  // 各 get(i) は O(i) 操作
        }
        endTime = System.currentTimeMillis();
        System.out.println("インデックスベース: " + (endTime - startTime) + "ms");
        
        // 効率的: Iterator を使用 - O(n)
        startTime = System.currentTimeMillis();
        int sum2 = 0;
        for (Integer num : numbers) {  // 内部的に Iterator を使用
            sum2 += num;
        }
        endTime = System.currentTimeMillis();
        System.out.println("Iterator (for-each): " + (endTime - startTime) + "ms");
    }
}

並行修正の問題と対策

コレクションの反復中に同じコレクションを変更すると、ConcurrentModificationException が発生する可能性があります。これを回避するためのいくつかの方法があります。

並行修正の問題と解決策

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class ConcurrentModificationExample {
    public static void main(String[] args) {
        // 問題: for-each ループ中の削除
        List<String> list1 = new ArrayList<>();
        list1.add("項目1"); list1.add("項目2"); list1.add("項目3");
        
        try {
            for (String item : list1) {
                if (item.equals("項目2")) {
                    list1.remove(item);  // ConcurrentModificationException が発生
                }
            }
        } catch (Exception e) {
            System.out.println("例外が発生: " + e.getClass().getName());
        }
        
        // 解決策1: Iterator の remove() メソッドを使用
        List<String> list2 = new ArrayList<>();
        list2.add("項目1"); list2.add("項目2"); list2.add("項目3");
        
        Iterator<String> iterator = list2.iterator();
        while (iterator.hasNext()) {
            String item = iterator.next();
            if (item.equals("項目2")) {
                iterator.remove();  // 安全に削除
            }
        }
        System.out.println("Iterator 使用後: " + list2);  // [項目1, 項目3]
        
        // 解決策2: removeIf() メソッド(Java 8以降)
        List<String> list3 = new ArrayList<>();
        list3.add("項目1"); list3.add("項目2"); list3.add("項目3");
        
        list3.removeIf(item -> item.equals("項目2"));
        System.out.println("removeIf() 使用後: " + list3);  // [項目1, 項目3]
        
        // 解決策3: CopyOnWriteArrayList の使用
        List<String> list4 = new CopyOnWriteArrayList<>();
        list4.add("項目1"); list4.add("項目2"); list4.add("項目3");
        
        for (String item : list4) {
            if (item.equals("項目2")) {
                list4.remove(item);  // 安全に削除(ただし非効率)
            }
        }
        System.out.println("CopyOnWriteArrayList 使用後: " + list4);  // [項目1, 項目3]
    }
}

まとめ

Java のコレクションフレームワークは、様々な反復処理の方法を提供しており、それぞれに長所と短所があります。適切な反復方法を選択することで、コードの可読性、保守性、パフォーマンスを向上させることができます。

  • シンプルな読み取り操作: for-each ループが最適
  • 要素の削除が必要: Iterator の remove() メソッドを使用
  • 双方向の反復や要素の追加/置換: ListIterator を使用
  • インデックスが重要: インデックスベースの for ループ(ArrayList の場合)
  • 複雑なデータ処理: Stream API を使用

コレクションの種類と操作の性質に基づいて、最適な反復処理方法を選択することが重要です。特に大規模なデータセットを扱う場合は、パフォーマンスの影響を考慮してください。