やり直しJava

コレクションフレームワーク

コレクションフレームワークは、オブジェクトの集合を扱うためのインタフェースと実装クラスの集合です。可変長配列、連想配列、キューといった枠組みが用意されています。

Python・Rubyなどでは 配列は可変長で各種メソッドも持っているため、データ集合を扱う場合は 配列や連想配列が良く使われますが、Javaでは通常の配列は固定長で 配列自体にはメソッドが用意されていなく、更には連想配列もないため、データ集合を扱う場合は 利便性の高いコレクションが使われることが多いです。

J2SE 5.0で総称型が導入され コレクションを型安全に利用できるようになりました。また、Java SE 8でストリームが追加され コレクションの要素をストリームとして扱うことができるようになりました。

コレクションフレームワークの主なクラス

コレクションフレームワークの主なクラスやインタフェースを次に挙げます。

コレクションフレームワークの主なクラスやインタフェース
インタフェース概要主な実装クラス概要
List順序付けられたコレクションArrayList一般的な可変長のListです。
LinkedListリンク構造のListです。要素の挿入、削除を行う際の処理がArrayListに比べて少ないため、頻繁に要素の挿入、削除を行う場合はLinkedListを使う方が性能が上がります。その代わり、インデックスを指定して要素にアクセスする場合は リンク構造を辿らないといけないため ArrayListに比べて性能が劣ります。
Set重複要素のないコレクションHashSet重複要素のない可変長のSetです。要素の順番は不定です。
LinkedHashSet順序を保持したSetです。順序はSetに追加した順番になります。
TreeSet自然順序付け等でソートされたSetです。
Mapキーと値のコレクション(連想配列に該当)HashMap連想配列のようなキー・値のセットの集合です。要素の順番は不定です。
LinkedHashMapキーの順序を保持したMapです。順序はMapに追加した順番になります。
TreeMapキーを自然順序付け等でソートされたMapです。
Queue片端から追加、片端から取り出しができるキューArrayDequeArrayDequeはQueueとDequeの両方のインタフェースを実装していて、Queueとして扱う場合はFIFOのキューとして利用できます。
Deque両端から追加、両端から取り出しができるキューArrayDequeDequeとして扱う場合は、FIFOのキューとしても、LILOのスタックとしても利用することができます。

List

Listインタフェース

Listは順序付けられたコレクションで、Collectionインタフェースのサブインタフェースになります。

Listの要素が可変オブジェクトの場合、オブジェクトの状態が変更になっても equals()が返す結果が変わらないようにする必要があります。equals()が返す結果が変わってしまうと、contains(Object)、indexOf(Object)、remove(Object)といった 対象要素を検索するメソッドにおいて、対象要素を特定できなくなってしまうからです

ArrayListクラス

ArrayListはListインタフェースの実装の1つで 可変長のリストです。内部的には要素を配列で保持しているので インデックスによるアクセスは高速ですが、リストの途中に要素の挿入・削除をする場合の性能は 次に紹介するLinkedListに比べて劣ります。

生成・初期化

インスタンスはダイヤモンド演算子を使って生成します。

List<String> list = new ArrayList<>();

コンストラクタの引数で 初期サイズを設定することができます。デフォルト値は10です。Listのサイズは可変長で 必要に応じて増やすことはできますが、動的なサイズ拡張はコストの高い処理となりますので 予め必要なサイズが分かっている場合は 生成の際にサイズを明示しておいた方が良いです。

インスタンスイニシャライザで add()メソッドを使って要素を追加することにより 初期化することもできます。

List<String> list = new ArrayList<>() {
    {
        add("One");
        add("Two");
        add("Three");
    }
};

出力

System.out.println()にListを渡すとListの要素を [] で囲んで出力してくれます。

System.out.println(list);  // [One, Two, Three]

尚、配列の場合は Arrays.toString()を使わないと 配列の要素ではなく配列のハッシュコードなどが出力されてしまいます。

走査

Listの要素を走査するには、いくつかの方法があります。

  • for文や拡張for文による走査
  • Iteratorによる走査
  • forEach()メソッドによる走査
List<String> list = new ArrayList<>() {
    {
        add("One");
        add("Two");
        add("Three");
    }
};

// 拡張 for 文による走査
for(String str : list) {
    System.out.println(str);
}

// Iterator による走査
for(Iterator<String> it = list.iterator(); it.hasNext(); ) {
    System.out.println(it.next());
}

// forEach による走査。ラムダ式、メソッド参照で処理内容を指定。
list.forEach(System.out::println);

ラムダ式やメソッド参照の導入で コレクション走査の処理がすっきりと記述できるようになりました。

走査中に要素の削除を行う場合は Iteratorを使います。拡張for文で削除を行うとConcurrentModificationExceptionが発生することがあります。

List<String> list = new ArrayList<>() {
    {
        add("One");
        add("Two");
        add("Three");
    }
};

for(Iterator<String> it = list.iterator(); it.hasNext(); ) {
    String str = it.next();
    if(str.equals("Two")) {
        it.remove();
    }
}

主なメソッド

ArrayListの主なメソッドを次にまとめます。

AddListの主なメソッド
分類メソッド概要
追加・削除・更新addリストの最後に要素を追加します。
addAllリストの最後に別のリストの要素を全て追加します。
remove(int)指定したインデックスの要素を削除します。
remove(Object)指定した要素を削除します。
set指定したインデックスの要素を置き換えます。
clearリストから全ての要素を削除します。
取得・検索get指定したインデックスの要素を返します。
sizeリストの要素の数を返します。
isEmptyリストが空かどうか判定します。
contains指定した要素が含まれるかどうか判定します。
indexOf指定した要素のインデックスを返します。存在しない場合は-1を返しますので、要素の包含チェックに使うこともできます。
部分リスト作成subList指定した範囲の部分リストを作成して返します。

ソート

並べ替えを行うにはCollections.sort()を使います。自然順序付け(ComparableインタフェースのcompareTo())に基づいて並べ替えます。並べ替えたリストを作成して返すのではなく、自身の要素を並べ替えることに注意してください。ソートの順序付けを独自に指定したい場合は、Collections.sort()にComparatorを渡します。

List<String> list = new ArrayList<>() {
    {
        add("orange");
        add("apple");
        add("pineapple");
    }
};

Collections.sort(list);
System.out::println(list);  // [apple, orange, pineapple]

Collections.sort(list, (s1, s2) -> s2.length() - s1.length());  // 文字数の長い順にソート
System.out::println(list);  // [pineapple, orange, apple]

LinkedListクラス

LinkedListは Listインタフェースの実装クラスの1つで 双方向(自分の前の要素と後ろの要素の参照を持つ)のリストを持ちます。そのため、リストの途中に要素の挿入・削除をする場合は リンク先の付け替えだけで済むので ArrayListに比べて高速に行うことができます。一方でインデックスを指定して要素にアクセスするためには リンクを辿らないといけないため、ArrayListに比べて遅くなります。

用途に応じてArrayListとLinkedListを使い分けると良いです。

Set

Setインタフェース

Setは重複要素のないコレクションで、Collectionインタフェースのサブインタフェースになります。

Setの要素が可変オブジェクトの場合、オブジェクトの状態が変更になっても equals()が返す結果が変わらないようにする必要があります。HashSetクラスやLinkedHashSetクラスの場合は、オブジェクトの状態が変更になっても hashCode()が返す結果が変わらないようにする必要があります。また、TreeSetクラスの場合は、オブジェクトの状態が変更になっても compareTo()が返す結果が変わらないようにする必要があります。

equals()等が返す結果が変わってしまうと、contains(Object)、remove(Object)といった 対象要素を検索するメソッドにおいて、対象要素を特定できなくなってしまうからです

HashSetクラス

HashSetは Setインタフェースの実装の1つで 重複しない要素の集合です。要素の順番は保持しません。

生成・初期化

インスタンスは ダイヤモンド演算子を使って生成します。

Set<String> set = new HashSet<>();

HashSetもArrayListと同様 必要に応じて容量が動的に拡張します。コンストラクタで初期容量と負荷係数を指定することができますが、デフォルト値はそれぞれ16と0.75です。内部のハッシュテーブルのバケットが16割り当てられ、0.75にあたる12が埋まると拡張されます。動的な容量拡張の処理はコストの高い処理となるため、ArrayList同様 全体の要素数が予め分かっていれば、初期容量を適切な値で明示すると良いです。ただ、ArrayListの場合とは異なり、必要な要素数=初期容量にはならず、正確に見積もることはできません。また、ハッシュテーブルの構造上、容量を大きくし過ぎると Iterator等による走査で全要素にアクセスする際にパフォーマンスの低下を招いてしまいます。小さ過ぎても 大き過ぎても性能劣化につながります。おおよその目安として 必要な要素数の4/3程度を初期容量とするのが良いと考えられます。

インスタンスイニシャライザで add()メソッドを使って要素を追加することにより 初期化することもできます。

Set<String> set = new HashSet<>() {
    {
        add("apple");
        add("orange");
        add("banana");
        add("apple");  // 重複しているので追加されず false が返る
    }
};

HashMapの注意点」(プログラマはサイコロを振らない)

出力

System.out.println()にSetを渡すと Setの要素を [] で囲んで出力してくれます(Listと同様)。

System.out.println(set);  // [orange, banana, apple]

走査

Setの要素を走査するには、いくつかの方法があります。

  • for文や拡張for文による走査
  • Iteratorによる走査
  • forEach()による走査
Set<String> set = new HashSet<>() {
    {
        add("apple");
        add("orange");
        add("banana");
        add("apple");
    }
};

// 拡張 for 文で走査
for(String str: set) {
    System.out.println(str);  // ex. apple
}

// Iterator で走査
for(Iterator<String> it = set.iterator(); it.hasNext(); ) {
    System.out.println(it.next());  // ex. apple
}

// forEach で走査。ラムダ式、メソッド参照で処理内容を指定。
set.forEach(System.out::println);  // ex. apple

ラムダ式やメソッド参照の導入で コレクション走査の処理がすっきりと記述できるようになりました。

走査中に要素の削除を行う場合は Iterator を使います。拡張for文で削除を行うとConcurrentModificationExceptionが発生することがあります。

Set<String> set = new HashSet<>() {
    {
        add("apple");
        add("orange");
        add("banana");
        add("apple");
    }
};

// Iterator で走査して削除
for(Iterator<String> it = set.iterator(); it.hasNext(); ) {
    String str = it.next();
    if(str.equals("orange")) {
        it.remove();
    }
}
System.out.println(set);  // [banana, apple]

主なメソッド

HashSetの主なメソッドを次にまとめます。

HashSetの主なメソッド
分類メソッド概要
追加・削除・更新addセットに要素を追加します。
remove指定した要素を削除します。
clearセットから全ての要素を削除します。
取得・検索sizeセットの要素の数を返します。
isEmptyセットが空かどうか判定します。
contains指定した要素が含まれるかどうか判定します。

ハッシュコード

HashSetでは 要素のhashCode()メソッドによりハッシュ値を導出しています。「Object」の章の「hashCode()メソッド」で説明した通り、hashCode()メソッドの一般契約として、equals()がtrueを返すインスタンス同士は hashCode()の戻り値が同じである必要があります。そうしないと、contains()やremove()といった 指定された要素の特定を行うメソッドが正しく機能しなくなってしまいます。contains()やremove()では、同じハッシュバケットの要素に対してequals()を呼び出して 指定された要素かどうかの検査を行います。そのため、異なるハッシュバケットに入れられてしまった要素は 検査対象にすらならないためです。

独自クラスをHashSetの要素にする場合は、「Object」の章で説明した eqauls()とhashCode() の一般契約に従う必要があります。

LinkedHashSetクラス

LinkedHashSetは Setインタフェースの実装の1つで 要素の順番を保持します。要素の順番は Setに追加した順番となります。自然順序付けや 独自の順序付けで並べたい場合は、次のに紹介するTreeSetクラスを使います。

Set<String> set = new LinkedHashSet<>() {
    {
        add("apple");
        add("orange");
        add("banana");
        add("apple");
    }
};
System.out.println(set);  // [apple, orange, banana]

TreeSetクラス

TreeSetは Setインタフェースの実装の1つで 要素の順番を保持します。引数なしのコンストラクタで生成すると 要素の順序は 自然順序付けに従って並べられます

Set<String> set = new TreeSet<>() {
    {
        add("orange");
        add("apple");
        add("banana");
        add("apple");
    }
};
System.out.println(set);  // [apple, banana, orange]

自然順序付けではなく 独自の順序付けを行いたい場合は、TreeSetのコンストラクタにComparatorを渡します。ラムダ式を用いると簡潔に記述することができます。

Set<String> set = new TreeSet<>((s1, s2) -> s2.compareTo(s1)) {
    {
        add("orange");
        add("apple");
        add("banana");
        add("apple");
    }
};
System.out.println(set);  // [orange, banana, apple]  // 逆順にソート

Map

Mapインタフェース

Mapは キーと値のペアのコレクションで、Collectionインタフェースのサブインタフェースになります。他の言語の連想配列に該当します。

Mapのキーは、基本的には不変オブジェクトにします。Mapのキーが可変オブジェクトの場合、オブジェクトの状態が変更になっても equals()が返す結果が変わらないようにする必要があります。HashMapクラスやLinkedHashMapクラスの場合は、オブジェクトの状態が変更になっても hashCode()が返す結果が変わらないようにする必要があります。また、TreeMapクラスの場合は、オブジェクトの状態が変更になっても compareTo()が返す結果が変わらないようにする必要があります。

equals()等が返す結果が変わってしまうと、get(Object)、containsKey(Object)、remove(Object)といった 対象要素を検索するメソッドにおいて、対象要素を特定できなくなってしまうからです

HashMapクラス

HashMapは Mapインタフェースの実装の1つで、キーと値のペアの集合で 要素の順番は保持しません。

生成・初期化

インスタンスは ダイヤモンド演算子を使って行います。キーと値の両方の型を指定します。

Map<String, String> map = new HashMap<>();

HashMapの初期容量と拡張の仕組みは HashSetと同じになります。

インスタンスイニシャライザで put()メソッドを使って要素を追加することにより 初期化することもできます。

Map<Integer, String> map = new HashMap<>() {
    {
        put(101, "Taro");
        put(102, "Jiro");
        put(103, "Saburo");
    }
};
System.out.println(map); // {101=Taro, 102=Jiro, 103=Saburo}

出力

System.out.println()にMapを渡すと Mapの要素を {} で囲んで出力してくれます。

System.out.println(map);  // {101=Taro, 102=Jiro, 103=Saburo}

走査

Mapの要素を走査するには、いくつかの方法があります。

  • for文や拡張for文による走査(キー/値/キーと値のペア)
  • Iteratorによる走査(キー/値/キーと値のペア)
  • forEach()による走査(キーと値のペア)
Map<Integer, String> map = new HashMap<>() {
    {
        put(101, "Taro");
        put(102, "Jiro");
        put(103, "Saburo");
    }
};

// 拡張 for で key を走査
for(int id : map.keySet()) {
    System.out.println(id);  // ex. 101
}

// 拡張 for で  value を走査
for(String name : map.values()) {
    System.out.println(name);  // ex. Taro
}

// 拡張 for で Entry を走査
for(Map.Entry<Integer, String> entry : map.entrySet()) {
    System.out.println(entry);  // ex. 101=Taro
}

System.out.println("Iterator");
// Iterator で key を走査
for(Iterator<Integer> it = map.keySet().iterator(); it.hasNext(); ) {
    Integer id = it.next();
    System.out.println(id);  // ex. 101
}

// Iterator で value を走査
for(Iterator<String> it = map.values().iterator(); it.hasNext(); ) {
    String name = it.next();
    System.out.println(name);  // ex. Taro
}

// Iterator で Entry を走査
for(Iterator<Map.Entry<Integer, String>> it = map.entrySet().iterator(); it.hasNext(); ) {
    Map.Entry<Integer, String> entry = it.next();
    System.out.println(entry);  // ex. 101=Taro
}

System.out.println("forEach");
// forEach で走査
map.forEach((k, v) -> System.out.println(k + "=" + v));

ラムダ式やメソッド参照の導入で コレクション走査の処理がすっきりと記述できるようになりました。

走査中に要素の削除を行う場合は、Iteratorを使います。拡張for文で削除を行うとConcurrentModificationExceptionが発生することがあります。

// Iterator で Entry を走査
for(Iterator<Map.Entry<Integer, String>> it = map.entrySet().iterator(); it.hasNext(); ) {
    Map.Entry<Integer, String> entry = it.next();
    if(entry.getKey() == 102) {
        it.remove();
    }
}
System.out.println(map);  // {101=Taro, 103=Saburo}

主なメソッド

HashMapの主なメソッドを次にまとめます。

HashMapの主なメソッド
分類メソッド概要
追加・削除・更新putキーと値のペアを追加します。
removeキーを指定してキーと値のペアを削除します。
clearマップの全ての要素を削除します。
取得・検索get指定したキーの値を返します。
sizeマップの要素の数を返します。
isEmptyマップが空かどうか判定します。
containsKey指定したキーが含まれるかどうか判定します。
containsValue指定した値が含まれるかどうか判定します。
keySet全てのキーを返します。
values全ての値を返します。
entrySet全てのキー・値のペアを返します。

ハッシュコード

HashMapも HashSetと同様、「Object」の章で説明した equals()とhashCode()の一般契約に従う必要があります。

LinkedHashMapクラス

LinkedHashMapは Mapインタフェースの実装の1つで 要素の順番を保持します。要素の順番は Mapに追加した順番となります。自然順序付けや独自の順序付けで並べたい場合は、次に紹介するTreeMapを使います。

Map<Integer, String> map = new LinkedHashMap<>() {
    {
        put(103, "Saburo");
        put(102, "Jiro");
        put(101, "Taro");
    }
};
System.out.println(map);  // {103=Saburo, 102=Jiro, 101=Taro}  // LinkedHashMap の場合は追加した順番

TreeMapクラス

TreeMapは Mapインタフェースの実装の1つで キーの順番を保持します。引数なしのコンストラクタで生成すると キーの順序は自然順序付けに従って並べられます

Map<Integer, String> map = new TreeMap<>() {
    {
        put(103, "Saburo");
        put(102, "Jiro");
        put(101, "Taro");
    }
};
System.out.println(map);  // {101=Taro, 102=Jiro, 103=Saburo}  // 自然順序付けによって並べられる

自然順序付けではなく 独自の順序付けを行いたい場合は、TreeMapのコンストラクタにComparatorを渡します。ラムダ式を用いると簡潔に記述することができます。

Map<Integer, String> map = new TreeMap<>((i1, i2) -> i2 - i1) {
    {
        put(103, "Saburo");
        put(102, "Jiro");
        put(101, "Taro");
    }
};
System.out.println(map);  // {103=Saburo, 102=Jiro, 101=Taro}  // 逆順に並び替え

Queue、Deque(J2SE 5.0~)

Queueインタフェース

J2SE 5.0以前では、FIFOのキューを実現するには Listを利用した実装が良く見られました。また、LILOのスタックを実現するには、java.util.Stackクラスが用意されていました。J2SE 5.0から QueueDequeインタフェースが提供され、キューとスタックの枠組みが導入されました。元からあるStackクラスは QueueもDequeインタフェースも実装しておらず、Java APIドキュメントでも StackよりQueue、Dequeを使うことが推奨されています

Queueインタフェースは 片端からの追加と 片端からの取り出しを定義しています。あくまでも片端からの追加と片端からの取り出しを定義しているだけであって、取り出し順番は実装クラスに任せられています(FIFOでもLIFOでも優先度付きでも構いません)。

Queueインタフェースでは 主に次のメソッドを定義しています。それぞれの操作に対して 操作が失敗したときに例外を投げるメソッドと、特殊な値(操作に応じてnullまたはfalse)を返すメソッド両方が用意されています。nullを特殊な値として扱うため、Queueに対してnull要素の追加はするべきではないとされています。

Queueインタフェースの主なメソッド
操作操作失敗時に例外を投げる操作失敗時に特殊な値を返す
追加addoffer
削除removepoll
調査(キューから要素は削除しない)elementpeek

Dequeインタフェース

Dequeは dequeue(キューから取り出す)を表しているのではなく、Double Ended Queue両端キュー)を表しています。Queueでは片端に要素を追加して 片端から要素を取り出しますが、追加する端と取り出す端は固定です(FIFOの場合、追加する端は末尾 取り出す端は先頭になります)。これに対して、Dequeは どちらの端に追加することもでき どちらの端から取り出すこともできます使い方次第で FIFOのキューとしても、LIFOのスタックとしても利用することができます

Dequeインタフェースでは Queueインタフェース同様、それぞれの操作に対して 操作が失敗したときに例外を投げるメソッド特殊な値を返すメソッド両方が用意されています

Dequeインタフェースの主なメソッド
操作対象が先頭操作対象が末尾
操作操作失敗時に
例外を投げる
操作失敗時に
特殊な値を返す
操作失敗時に
例外を投げる
操作失敗時に
特殊な値を返す
追加addFirstofferFirstaddLastofferLast
削除removeFirstpollFirstremoveLastpollLast
調査(キューから要素は削除しない)getFirstpeekFirstgetLastpeekLast

Dequeは Queueのサブインタフェースなので、Queueとして扱うこともできます。その場合は FIFOの形でマッピングされます。具体的には、Queueのメソッドは 次のようにDequeのメソッドにマッピングされます。

DequeをQueueとして扱うの場合のマッピング
QueueDeque
操作操作失敗時に
例外を投げる
操作失敗時に
特殊な値を返す
操作失敗時に
例外を投げる
操作失敗時に
特殊な値を返す
追加addofferaddLastofferLast
削除removepollremoveFirstpollFirst
調査(キューから要素は削除しない)elementpeekgetFirstpeekFirst

また、Dequeには スタックとして利用するためのメソッドも定義されていて、内部的には次のようにマッピングされます。

Dequeをスタックとして扱う場合のマッピング
スタック用メソッドDeque
操作操作失敗時に
例外を投げる
操作失敗時に
特殊な値を返す
操作失敗時に
例外を投げる
操作失敗時に
特殊な値を返す
追加pushaddLast
削除popremoveFirst
調査(キューから要素は削除しない)peekpeekFirst

ArrayDequeクラス

ArrayDequeは Dequeインタフェースの実装クラスの1つです。Dequeインタフェースの説明の通り、FIFOのキューとして利用することもできますし、LIFOのスタックとして利用することもできます。ArrayDequeはスレッドセーフではありません。

FIFOのキューとしての利用例を次に挙げます。

// キューとして利用
Deque<String> queue = new ArrayDeque<>();
queue.add("Taro");
queue.add("Jiro");
queue.add("Saburo");

System.out.println(queue);  // [Taro, Jiro, Saburo]
System.out.println(queue.element());  // Taro  // 要素は削除しない
System.out.println(queue);  // [Taro, Jiro, Saburo]
System.out.println(queue.remove());  // Taro  // 要素を削除
System.out.println(queue);  // [Jiro, Saburo]

LIFOのスタックとしての利用例を次に挙げます。

// スタックとして利用
Deque<String> stack = new ArrayDeque<>();
stack.push("Taro");
stack.push("Jiro");
stack.push("Saburo");

System.out.println(stack);  // [Saburo, Jiro, Taro]
System.out.println(stack.peek());  // Saburo  // 要素は削除しない
System.out.println(stack);  // [Saburo, Jiro, Taro]
System.out.println(stack.pop());  // Saburo  // 要素を削除
System.out.println(stack);  // [Jiro, Taro]

Java スタック・キューのメモ」(Qiita)

相互変換

コレクションと配列の相互変換や 各コレクション間での相互変換をいくつか取り上げます。

Listと配列の相互変換

Listから配列に変換するには、ListのtoArray()メソッドを使用します。toArray()は引数ありのメソッドを使い、引数には実際の型の配列(例えば String[])を生成して渡します。その際に渡す配列のサイズは List.size()ではなく 0を指定します。(List.size()よりも 0を指定した方がパフォーマンスが良くなります。Effective Javaの中で その検証を行ったサイトが紹介されています。)

引数なしのtoArray()もありますが、そちらはObject[] が返ってきてしまい、実際の型の配列(例えば String[])へキャストすると ClassCastExceptionが発生してしまい使い勝手が悪いです。

List<String> list = new ArrayList<>() {
    {
        add("One");
        add("Two");
        add("Three");
    }
};
String[] array = list.toArray(new String[0]);
for(String str: array) {
	System.out.print(str + ",");
}  // One, Two, Three

配列からListに変換するには、ArraysのクラスメソッドasList()を使います。asList()で返されるのはjava.util.Arrays.$ArrayListで これはArraysクラスの内部クラスであり、java.util.ArrayListとは異なります。したがって 要素の追加や削除を行うことはできず、要素の追加や削除を行うとUnsupportedOperationExceptionが発生します。

String[] array = {"One", "Two", "Three"};
List<String> list = Arrays.asList(array);
System.out.println(list); // [One, Two, Three]
list.add("Four"); // UnsupportedOperationException 発生

ListとSetの相互変換

ListからSetに変換するには、Setの実装クラスのコンストラクタにListのインスタンスを渡します。渡したListに重複要素がある場合は 重複要素が取り除かれます。また、HashSetのように順不同のSetの場合は 当然ながらListの順番は保持されません。

List<String> list = new ArrayList<>() {
    {
        add("Three");
        add("Two");
        add("One");
        add("Two");
    }
};

Set<String> set = new HashSet<>(list);
System.out.println(set);  // [One, Two, Three]

Listの順番を保持したい場合は LinkedHashSetのように順番を保持するSetの実装を使います。

逆に SetからListに変換する場合は、Listの実装クラスのコンストラクタにSetのインスタンスを渡します。HashSetのように順不同のSetを渡した場合は Listの順番は順不同となります。

Set<String> set = new HashSet<>() {
    {
        add("Three");
        add("Two");
        add("One");
    }
};
List<String> list = new ArrayList<>(set);
System.out.println(list);  // [One, Two, Three]

ListとMapの変換

ListからMapへの変換の前提として Listの要素からキーと値を生成できることとします。

ListからMapへの変換には Streamのcollect()メソッドを使うと 簡潔に処理を記述することができます。

class Data {
    String id;
    String name;
    Data(String id, String name){
        this.id = id;
        this.name = name;
    }
};

List<Data> list = new ArrayList<>() {
    {
        add(new Data("101", "Taro"));
        add(new Data("102", "Jiro"));
        add(new Data("103", "Saburo"));
        add(new Data("104", "Shiro"));
    }
};

Map<String, String> map = list.stream().collect(
        Collectors.toMap(d -> d.id, d -> d.name));
System.out.println(map);  // {101=Taro, 102=Jiro, 103=Saburo, 104=Shiro}

逆に MapのキーをListに変換する場合は keySet()メソッドを、値をListに変換する場合はvalues()メソッドを呼び出して Listの実装クラスのコンストラクタに渡します。

List<String> keyList = new ArrayList<>(map.keySet());
List<String> valueList = new ArrayList<>(map.values());

スレッドセーフなコレクション

HashMap、HashSetなどはスレッドセーフではなく、マルチスレッド環境で利用する場合は 自前で排他制御を行う必要があります。マルチスレッド環境で 並列性を維持しつつ スレッドセーフに操作したい場合は、ConcurrentHashMapやConcurrentHashSetなどの java.util.concurrentパッケージのコレクション実装を使います。

以前は Collections.synchronizedMap()等のメソッドを利用する方法がありましたが、Iterator等を用いて要素の走査を行う場合にスレッドセーフでなく、自前で排他処理を実装する必要がありました。

これを改善すべく J2SE 5.0からConcurrentHashMapやConcurrentHashSet等の実装クラスが追加されました。詳しくは「マルチスレッド」の章の「ロックフリーでスレッドセーフな操作を提供するクラスを利用する」を参照してください。