前回は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(挿入順保持) |