5 変更不可能なリスト、セットおよびマップの作成

リストセットおよびマップの各インタフェースのコンビニエンス静的ファクトリ・メソッドによって、変更不可能なリスト、セットおよびマップを簡単に作成できるようになりました。

要素を追加、削除または置換できない場合、コレクションは変更不可能とみなされます。コレクションの変更不可能なインスタンスが作成されると、これに対する参照が存在するかぎり、ここには同じデータが保持され続けます。

変更可能なコレクションは、ブックキーピング・データを保持して将来の変更に対応する必要があります。これにより、変更可能なコレクションに保存されているデータのオーバーヘッドが増加します。変更不可能なコレクションには、このような追加のブックキーピング・データは不要です。コレクションを変更する必要がないので、コレクションに含まれているデータは高密度に圧縮できます。一般に、変更不可能なコレクションのインスタンスは、同じデータが含まれている変更可能なコレクションのインスタンスよりも、消費メモリーが大幅に少なくなります。

ユースケース

変更不可能なコレクションと変更可能なコレクションのどちらを使用するかは、コレクション内のデータによって異なります。

変更不可能なコレクションは領域効率上の利点がある他、コレクションが誤って変更されたためにプログラムが正しく動作しなくなる可能性を防ぐことができます。変更不可能なコレクションは、次の場合に推奨されます。

  • プログラムの記述時に既知であった定数から初期化されるコレクション
  • プログラムの開始時に、構成ファイルなどから計算または読み込まれるデータから初期化されるコレクション

プログラム全体を通して変更されるデータを保持するコレクションには、変更可能なコレクションが最適な選択肢です。変更はインプレースで実行されるので、データ要素の逐次追加や削除にかかる負荷は非常に少なくて済みます。これを変更不可能なコレクションで行うと、1つの要素を追加または削除するために完全なコピーを作成する必要があり、通常、許容できないオーバーヘッドが発生します。

構文

これらの新しいコレクションのAPIは、要素の数が少ない場合は特にシンプルです。

変更不可能なリストの静的ファクトリ・メソッド

List.of静的ファクトリ・メソッドを使用すると、変更不可能なリストを簡単に作成できます。

リストは順序付けされたコレクションであり、重複要素が許可されます。NULL値は設定できません。

これらのメソッドの構文は次のとおりです。

List.of()
List.of(e1)
List.of(e1, e2)         // fixed-argument form overloads up to 10 elements
List.of(elements...)   // varargs form supports an arbitrary number of elements or an array

例5-1 例

JDK 8の場合:

List<String> stringList = Arrays.asList("a", "b", "c");
stringList = Collections.unmodifiableList(stringList);

JDK 9以降の場合:

List<String> stringList = List.of("a", "b", "c");

「変更不可能なリスト」を参照してください。

変更不可能なセットの静的ファクトリ・メソッド

Set.of静的ファクトリ・メソッドを使用すると、変更不可能なセットを簡単に作成できます。

セットは、重複要素を含まないコレクションです。重複エントリが検出された場合は、IllegalArgumentExceptionがスローされます。NULL値は設定できません。

これらのメソッドの構文は次のとおりです。

Set.of()
Set.of(e1)
Set.of(e1, e2)         // fixed-argument form overloads up to 10 elements
Set.of(elements...)   // varargs form supports an arbitrary number of elements or an array

例5-2 例

JDK 8の場合:

Set<String> stringSet = new HashSet<>(Arrays.asList("a", "b", "c"));
stringSet = Collections.unmodifiableSet(stringSet);

JDK 9以降の場合:

Set<String> stringSet = Set.of("a", "b", "c");

「変更不可能なセット」を参照してください。

変更不可能なマップの静的ファクトリ・メソッド

Map.ofMap.ofEntriesの各静的ファクトリ・メソッドを使用すると、変更不可能なマップを簡単に作成できます。

マップには重複するキーを含めることはできません。重複キーが検出された場合は、IllegalArgumentExceptionがスローされます。各キーは1つの値に関連付けられます。Nullはマップのキーまたは値のいずれにも使用できません。

これらのメソッドの構文は次のとおりです。

Map.of()
Map.of(k1, v1)
Map.of(k1, v1, k2, v2)    // fixed-argument form overloads up to 10 key-value pairs
Map.ofEntries(entry(k1, v1), entry(k2, v2),...)
 // varargs form supports an arbitrary number of Entry objects or an array

例5-3 例

JDK 8の場合:

Map<String, Integer> stringMap = new HashMap<String, Integer>(); 
stringMap.put("a", 1); 
stringMap.put("b", 2);
stringMap.put("c", 3);
stringMap = Collections.unmodifiableMap(stringMap);

JDK 9以降の場合:

Map<String, Integer> stringMap = Map.of("a", 1, "b", 2, "c", 3);

例5-4 任意の数のペアを持つマップ

キーと値のペアの数が10を超える場合は、Map.entryメソッドを使用してマップのエントリを作成し、それらのオブジェクトをMap.ofEntriesメソッドに渡します。次に例を示します。

import static java.util.Map.entry;
Map <Integer, String> friendMap = Map.ofEntries(
   entry(1, "Tom"),
   entry(2, "Dick"),
   entry(3, "Harry"),
   ...
   entry(99, "Mathilde"));

「変更不可能なマップ」を参照してください。

コレクションの変更不可能なコピーの作成

要素を追加し、それを変更することによってコレクションを作成し、ある時点で、そのコレクションの変更不可能なスナップショットが必要になる場合があるとします。copyOfメソッド・ファミリを使用してコピーを作成します。

たとえば、いくつかの場所から要素を収集するコードがあるとします。   
   List<Item> list = new ArrayList<>();
   list.addAll(getItemsFromSomewhere());
   list.addAll(getItemsFromElsewhere());
   list.addAll(getItemsFromYetAnotherPlace());
List.ofメソッドを使用して変更不可能なコレクションを作成することは不便です。このことを実行するには、正しいサイズの配列を作成し、リストの要素を配列にコピーして、その後、List.of(array)を呼び出して変更不可能なスナップショットを作成する必要があります。かわりにcopyOf staticファクトリ・メソッドを使用して、1つのステップで実行します。  
   List<Item> snapshot = List.copyOf(list); 

セットマップには、Set.copyOfおよびMap.copyOfと呼ばれる対応する静的ファクトリ・メソッドがあります。List.copyOfSet.copyOfのパラメータはコレクションなので、セットの要素が含まれる変更不可能なリストリストの要素が含まれる変更不可能なセットを作成できます。Set.copyOfを使用してリストからセットを作成する場合、リストに重複要素が含まれていても、例外はスローされません。かわりに、作成されるセットには、重複要素の中から任意の1つが含められます。

コピーするコレクションが変更可能である場合、copyOfメソッドは、元のコピーである変更不可能なコレクションを作成します。つまり、結果には元と同じすべての要素が含まれます。元のコレクションに要素が追加されたり、元のコレクションから要素が削除された場合、そのコピーには影響しません。

元のコレクションがすでに変更不可能である場合、copyOfメソッドは元のコレクションへの参照のみを戻します。コピーを作成する理由は、戻されたコレクションを元のコレクションの変更から分離するためです。ただし、元のコレクションを変更できない場合は、コピーを作成する必要がありません。

これらのケースの両方において、要素が可変で、その1つが変更された場合、この変更によって元のコレクションとコピーの両方が変更されたように見えます。

ストリームからの変更不可能なコレクションの作成

ストリーム・ライブラリには、コレクタと呼ばれる端末操作のセットが含まれます。コレクタは、ストリームの要素を含む新規コレクションを作成する場合に最もよく使用されます。java.util.stream.Collectorsクラスに、ストリームの要素から新しい変更不可能なコレクションを作成するコレクタが含まれています。

戻されたコレクションが変更不可能であることを保証する必要がある場合は、toUnmodifiable-コレクタのいずれかを使用する必要があります。これらのコレクタは次のとおりです。

   Collectors.toUnmodifiableList()
   Collectors.toUnmodifiableSet()
   Collectors.toUnmodifiableMap(keyMapper, valueMapper)     
   Collectors.toUnmodifiableMap(keyMapper, valueMapper, mergeFunction)

たとえば、ソース・コレクションの要素を変換して結果を変更不可能なセットに配置するには、次のようにします。

   Set<Item> unmodifiableSet =
      sourceCollection.stream()
                      .map(...) 
                      .collect(Collectors.toUnmodifiableSet());

ストリームに重複要素がある場合、toUnmodifiableSetコレクタは重複の中から任意の1つを選択して、作成されるセットに含めます。toUnmodifiableMap(keyMapper, valueMapper)コレクタでは、keyMapper関数によって重複キーが生成されると、IllegalStateExceptionがスローされます。キーが重複する可能性がある場合は、かわりにtoUnmodifiableMap(keyMapper, valueMapper, mergeFunction)コレクタを使用します。重複キーが生じると、mergeFunctionが呼び出され、重複する各キーの値が1つの値にマージされます。

toUnmodifiable-コレクタは、同等のtoListtoSet、および対応する2つのtoMapメソッドと概念的に似ていますが、特性が異なります。具体的には、toListtoSetおよびtoMapの各メソッドで戻されるコレクションの変更可能性は保証されませんが、toUnmodifiable-コレクタの結果は確実に変更不可能です。

ランダム化された反復順序

Set要素およびMapキーの反復順序がランダム化され、JVMの実行ごとに異なる可能性があります。これは意図的な動作であり、これによって、反復順序に依存するコードの識別が容易になります。想定外の反復順序の依存性によって、デバッグするのが難しい問題が発生することがあります。

次の例は、jshellを再起動した後で、反復順序がどのように異なるかを示しています。

jshell> var stringMap = Map.of("a", 1, "b", 2, "c", 3);
stringMap ==> {b=2, c=3, a=1}

jshell> /exit
|  Goodbye

C:\Program Files\Java\jdk\bin>jshell

jshell> var stringMap = Map.of("a", 1, "b", 2, "c", 3);
stringMap ==> {a=1, b=2, c=3}

ランダム化された反復順序は、Set.ofMap.ofおよびMap.ofEntriesの各メソッドとtoUnmodifiableSetおよびtoUnmodifiableMapの各コレクタによって作成されたコレクションのインスタンスに適用されます。HashMapおよびHashSetなどのコレクション実装の反復順序に変更はありません。

変更不可能なコレクションについて

JDK 9で追加されたコンビニエンス・ファクトリ・メソッドによって返されるコレクションは変更不可能です。これらのコレクションに対して要素の追加、設定または削除を試行すると、UnsupportedOperationExceptionがスローされます。

ただし、含まれている要素が可変要素である場合は、コレクションの動作の一貫性が失われるか、コレクションのコンテンツが変更されます。

変更不可能なコレクションに可変要素が含まれている場合の例を示します。jshellを使用して、ArrayListクラスを使用した文字列オブジェクトのリストを2つ作成します。ここで、2番目のリストは1番目のリストをコピーして作成します。トリビアルjshell出力は削除されました。

jshell> List<String> list1 = new ArrayList<>();
jshell> list1.add("a")
jshell> list1.add("b")
jshell> list1
list1 ==> [a, b]

jshell> List<String> list2 = new ArrayList<>(list1);
list2 ==> [a, b]

次に、List.ofメソッドを使用して、最初に作成したリストをポイントするunmodlist1unmodlist2を作成します。unmodlist1は変更不可能であるため、unmodlist1を変更しようとすると、例外エラーが表示されます。変更を試行すると例外がスローされます。

jshell> List<List<String>> unmodlist1 = List.of(list1, list1);
unmodlist1 ==> [[a, b], [a, b]]

jshell> List<List<String>> unmodlist2 = List.of(list2, list2);
unmodlist2 ==> [[a, b], [a, b]]

jshell> unmodlist1.add(new ArrayList<String>())
|  java.lang.UnsupportedOperationException thrown:
|        at ImmutableCollections.uoe (ImmutableCollections.java:71)
|        at ImmutableCollections$AbstractImmutableList.add (ImmutableCollections
.java:75)
|        at (#8:1)

ただし、unmodlist1が変更不可能であっても、元のlist1を変更すると、unmodlist1の内容が変更されます。

jshell> list1.add("c")
jshell> list1
list1 ==> [a, b, c]
jshell> unmodlist1
ilist1 ==> [[a, b, c], [a, b, c]]

jshell> unmodlist2
ilist2 ==> [[a, b], [a, b]]

jshell> unmodlist1.equals(unmodlist2)
$14 ==> false

変更不可能なコレクションと変更不可能なビュー

変更不可能なコレクションは、Collections.unmodifiable...メソッドで戻される変更不可能なビューと同じように動作します。ただし、変更不可能なコレクションはビューではありません - これらはクラスによって実装されたデータ構造であり、データの変更を試行すると例外がスローされます。

リストを作成してCollections.unmodifiableListメソッドに渡した場合は、変更不可能なビューを取得します。基となるリストは変更可能であり、これを変更した場合は返されるリストに表示されるため、実際には不変ではありません。

この動作を確認するには、リストを作成してCollections.unmodifiableListに渡します。そのリストに直接追加しようとすると、例外がスローされます。

jshell> List<String> list1 = new ArrayList<>();
jshell> list1.add("a")
jshell> list1.add("b")
jshell> list1
list1 ==> [a, b]

jshell> List<String> unmodlist1 = Collections.unmodifiableList(list1);
unmodlist1 ==> [a, b]

jshell> unmodlist1.add("c")
|  Exception java.lang.UnsupportedOperationException
|        at Collections$UnmodifiableCollection.add (Collections.java:1058)
|        at (#8:1)

unmodlist1list1のビューであることに注意してください。ビューは直接変更できませんが、元のリストを変更することによってビューを変更できます。元のlist1を変更した場合、エラーは生成されず、unmodlist1リストが変更されます。

jshell> list1.add("c")
$19 ==> true
jshell> list1
list1 ==> [a, b, c]

jshell> unmodlist1
unmodlist1 ==> [a, b, c]

変更不可能なビューと呼ばれる理由は、ビューではメソッドを呼び出してコレクションを変更できないためです。ただし、基になるコレクションへの参照を持ち、それを変更する権限があるユーザーなら誰でも、変更不可能なビューを変更できます。

領域効率

コンビニエンス・ファクトリ・メソッドによって返されるコレクションは、同等の変更可能なコレクションより領域効率に優れています。

これらのコレクションのすべての実装は、静的ファクトリ・メソッドの背後にあるプライベート・クラスです。これがコールされると、静的ファクトリ・メソッドはサイズに基づいて実装クラスを選択します。データは圧縮されたフィールドベースまたは配列ベースのレイアウトに保存されます。

2つの代替実装によって消費されるヒープ領域を示します。まず、2つの文字列が含まれる変更不可能なHashSetを示します。
Set<String> set = new HashSet<>(3);   // 3 buckets
set.add("silly");
set.add("string");
set = Collections.unmodifiableSet(set);
セットには、変更不可能なラッパー、HashSet、それに含まれるHashMap、バケットの表(配列)、2つのノード・インスタンス(要素ごとに1つ)、の6つのオブジェクトが含まれています。オブジェクトごとに12バイトのヘッダーを持つ通常のVMでは、セットのオーバーヘッド合計は96バイト + 28 * 2 = 152バイトになります。これは、保存されるデータ量に対して大きなオーバーヘッド容量です。さらに、データにアクセスするために複数のメソッドのコールや異なるポインタが必要になります。

かわりに、Set.ofを使用してセットを実装します。

Set<String> set = Set.of("silly", "string");

これはフィールドベースの実装であるため、セットには1つのオブジェクトと2つのフィールドが含まれます。オーバーヘッドは20バイトです。新しいコレクションが消費するヒープ領域は、要素ごとの固定オーバーヘッドであるため、小さくなります。

変更をサポートしないこともまた、領域の節約に貢献します。さらに、データを保持する必要があるオブジェクトが少なくなるため、ローカル参照が改善されます。

スレッド・セーフティ

変更可能なデータ構造を複数のスレッドで共有している場合、1つのスレッドによって加えられた変更がその他のスレッドに予期せぬ悪影響を及ぼすことがないように、措置を講じる必要があります。ただし、不変オブジェクトは変更できないことからスレッド・セーフとみなされ、追加的な対策は必要ありません。

プログラムの複数の部分でデータ構造を共有している場合、プログラムのある部分によって構造に加えられた変更は、他の部分からも確認できます。プログラムの他の部分がデータの変更に対応できていないと、バグやクラッシュなどの予期しない動作が発生する可能性があります。一方、プログラムの様々な部分で不変データ構造を共有している場合、共有されている構造は変更できないので、このような予期しない動作は発生しません。

同様に、複数のスレッドでデータ構造を共有している場合、各スレッドはそのデータ構造を変更する際に注意する必要があります。通常、スレッドは共有データ構造に対する読取り/書込み操作時に、ロックを保持する必要があります。適切にロックしていないと、データ構造内で競合状態または不整合が生じて、バグやクラッシュなどの予期しない動作が発生する可能性があります。一方、複数のスレッドで不変データ構造を共有している場合は、ロックがなくてもこのような問題は発生しません。したがって、不変データ構造は、追加的な対策(ロック・コードの追加など)が不要なスレッド・セーフです。

要素を追加、削除または置換できない場合、コレクションは変更不可能とみなされます。ただし、変更不可能なコレクションが不変であるのは、そのコレクションに含まれている要素が不変な場合にかぎります。スレッド・セーフとみなされるには、静的ファクトリ・メソッドとtoUnmodifiable-コレクタを使用して作成されたコレクションに不変な要素のみが含まれている必要があります。