Javaコレクションフレームワークの基礎: List, Map, Set

前回はJavaの三大特性(カプセル化、継承、ポリモーフィズム)について復習しました。今回はコレクションフレームワークを解説します。

コレクションの概要

Javaの開発では、基本データ型やStringオブジェクトに加えて、コレクション関連のクラスを頻繁に利用します。コレクションクラスはオブジェクト自体ではなく、オブジェクトへの参照を格納します。本記事では説明の便宜上、「コレクション内のオブジェクト」という場合は参照を指します。

コレクションの主な型は3つ: List、Set、Mapです。なお、MapはCollectionのサブインタフェースではありませんが、コレクションフレームワークと統合されています。

List

ListインタフェースはCollectionを継承し、重複を許可する順序付きコレクションを定義します。位置ベースの操作(追加、取得、削除)を提供します。

シングルスレッドでは主にArrayListやLinkedList、マルチスレッドではVectorまたはCollections.synchronizedList()でラップしたリストを使います。

  • ArrayList: 内部は配列。要素への高速ランダムアクセスが可能。途中挿入・削除は配列のコピーが発生しコストが高い。ランダム検索・走査に適し、挿入・削除には不向き。
  • LinkedList: 双方向リンクリスト構造。動的な挿入・削除に優れる。ランダムアクセスや走査は遅い。リストの先頭・末尾操作用の専用メソッドを持ち、スタック・キュー・両端キューとしても利用可能。
  • Vector: 配列ベースだが、メソッドがsynchronizedでスレッドセーフ。ArrayListより速度は劣る。

使用例:

List<String> list1 = new ArrayList<>();
List<String> list2 = new LinkedList<>();
List<String> list3 = new Vector<>();
List<String> list4 = Collections.synchronizedList(new ArrayList<>());

性能比較(挿入、読み取り、削除)のサンプルコード:

private static final int ITEMS = 50000;

private static ArrayList<Object> arrList = new ArrayList<>();
private static LinkedList<Object> linkList = new LinkedList<>();
private static Vector<Object> vec = new Vector<>();

public static void main(String[] args) {
    insertTest(arrList);
    insertTest(linkList);
    insertTest(vec);
    System.out.println("----------------");
    readTest(arrList);
    readTest(linkList);
    readTest(vec);
    System.out.println("----------------");
    deleteTest(arrList);
    deleteTest(linkList);
    deleteTest(vec);
}

private static void insertTest(List<Object> list) {
    long start = System.currentTimeMillis();
    Object obj = new Object();
    for (int i = 0; i < ITEMS; i++) {
        list.add(0, obj);
    }
    System.out.println(list.getClass().getSimpleName() + " 插入" + ITEMS + "条耗时: " + (System.currentTimeMillis() - start) + "ms");
}

private static void readTest(List<Object> list) {
    long start = System.currentTimeMillis();
    for (int i = 0; i < ITEMS; i++) {
        list.get(i);
    }
    System.out.println(list.getClass().getSimpleName() + " 读取" + ITEMS + "条耗时: " + (System.currentTimeMillis() - start) + "ms");
}

private static void deleteTest(List<Object> list) {
    long start = System.currentTimeMillis();
    for (int i = 0; i < ITEMS; i++) {
        list.remove(0);
    }
    System.out.println(list.getClass().getSimpleName() + " 删除" + ITEMS + "条耗时: " + (System.currentTimeMillis() - start) + "ms");
}

実行結果(環境依存):

ArrayList 插入50000条耗时: 281ms
LinkedList 插入50000条耗时: 2ms
Vector 插入50000条耗时: 274ms
----------------
ArrayList 读取50000条耗时: 1ms
LinkedList 读取50000条耗时: 1060ms
Vector 读取50000条耗时: 2ms
----------------
ArrayList 删除50000条耗时: 143ms
LinkedList 删除50000条耗时: 1ms
Vector 删除50000条耗时: 137ms

この結果から、ArrayListとLinkedListの挿入・削除・検索性能の違いが明確です。

Listの集合演算(和集合、積集合、差集合、重複なし和集合)は標準メソッドで実現可能です。

/**
 * 和集合(addAll)
 */
public static <T> List<T> union(List<T> a, List<T> b) {
    a.addAll(b);
    return a;
}

/**
 * 積集合(retainAll): aからbに含まれない要素を削除
 */
public static <T> List<T> intersect(List<T> a, List<T> b) {
    a.retainAll(b);
    return a;
}

/**
 * 差集合(removeAll): aからbに含まれる要素を削除
 */
public static <T> List<T> difference(List<T> a, List<T> b) {
    a.removeAll(b);
    return a;
}

/**
 * 重複なしの和集合: aとbを結合し重複を排除
 */
public static <T> List<T> unionDistinct(List<T> a, List<T> b) {
    List<T> result = new ArrayList<>(a);
    for (T item : b) {
        if (!result.contains(item)) {
            result.add(item);
        }
    }
    return result;
}

リストの走査には主に3つの方法があります: 通常のforループ、拡張forループ、Iterator。

List<String> sample = Arrays.asList("x", "y", "z");

// 通常for
for (int i = 0; i < sample.size(); i++) {
    System.out.println(sample.get(i));
}

// 拡張for
for (String s : sample) {
    System.out.println(s);
}

// Iterator
Iterator<String> it = sample.iterator();
while (it.hasNext()) {
    System.out.println(it.next());
}

注意: 通常forはインデックス取得可能、拡張forは簡潔。Iteratorは走査中の要素削除(add/remove)を安全に行える。『Alibaba Java開発マニュアル』でも「foreachループ内で要素のremove/addを行うな。removeはIteratorを使え」と明記されています。

foreach内でremove/addを行うとConcurrentModificationExceptionが発生する可能性があります(breakを入れれば回避可能だが本来は避けるべき)。

Map

MapはCollectionを継承せず、キーと値のマッピングを提供します。重複キーは不可、各キーは1つの値にマッピング。キー集合、値集合、キー-値マッピングの3つのビューを提供。

主な実装クラス:

  • HashMap: ハッシュテーブルベース。キーのhashCodeで高速検索。nullキーは1つ、null値は複数許可。非スレッドセーフで高効率。
  • TreeMap: 赤黒木。キーの自然順序またはComparatorでソート。Iteratorで走査するとソート済み。nullキー不可。非スレッドセーフ。
  • LinkedHashMap: HashMap + 双方向リンクリスト。挿入順またはアクセス順を保持。HashMapより挿入はやや遅いが、順序保証が必要な場合に使用。
  • Hashtable: スレッドセーフ。nullキー/値不可。同期化によるオーバーヘッドあり。レガシー。
  • ConcurrentHashMap: セグメントロック方式でスレッドセーフ。Hashtableの代替としてJava 1.5で導入。マルチスレッド環境で推奨。

TreeMapのソート例:

Map<String, Integer> hm = new HashMap<>();
hm.put("banana", 2);
hm.put("apple", 1);
hm.put("cherry", 3);
System.out.println("HashMap: " + hm); // 順不同

Map<String, Integer> tm = new TreeMap<>(hm);
System.out.println("TreeMap: " + tm); // {apple=1, banana=2, cherry=3}

Mapの走査方法:

Map<String, String> map = new HashMap<>();
// keySet
for (String key : map.keySet()) {
    System.out.println(key + " -> " + map.get(key));
}
// entrySet + Iterator
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
    Map.Entry<String, String> e = it.next();
    System.out.println(e.getKey() + " -> " + e.getValue());
}
// entrySet + foreach (大容量で効率的)
for (Map.Entry<String, String> e : map.entrySet()) {
    System.out.println(e.getKey() + " -> " + e.getValue());
}
// valuesのみ
for (String v : map.values()) {
    System.out.println(v);
}

容量が大きい場合はentrySetを使うと効率的です。

Set

Setは重複を許さないコレクション。任意の2要素e1, e2に対してe1.equals(e2)=false。nullは最大1つ。抽象インタフェースのため直接インスタンス化不可。

主な実装:

  • HashSet: ハッシュセット。非スレッドセーフ、順序未保証、null可。
  • TreeSet: 赤黒木。自然順序またはComparatorでソート。null不可。非スレッドセーフ。
  • LinkedHashSet: 挿入順を保持。HashSet+LinkedList。非スレッドセーフ、null可。

使用例:

Set<String> hs = new HashSet<>();
Set<String> ts = new TreeSet<>();
Set<String> lhs = new LinkedHashSet<>();

Setは重複排除に便利です。下記はListから重複を検出する例:

List<String> items = Arrays.asList("Java", "Python", "Java", "C++");
Set<String> unique = new HashSet<>();
for (String item : items) {
    if (!unique.add(item)) {
        System.out.println("重複: " + item);
    }
}
System.out.println("一意リスト: " + unique);

オブジェクトの重複排除にはequals()とhashCode()の適切なオーバーライドが必要です。

まとめ

コレクション特徴主な実装
List順序付き、重複許可。位置ベース操作。ArrayList(高速ランダムアクセス)、LinkedList(高速挿入削除)、Vector(スレッドセーフだが非推奨)
Mapキー-値マッピング。キー重複不可。HashMap(高速、null許可)、TreeMap(ソート)、LinkedHashMap(順序保持)、Hashtable(レガシー)、ConcurrentHashMap(スレッドセーフ推奨)
Set重複不可。集合演算に有用。HashSet(高速、順序不保証)、TreeSet(ソート)、LinkedHashSet(挿入順保持)

タグ: Java List map set arraylist

7月22日 04:09 投稿