変更可能・変更不可能なList、Set、Mapのまとめ

リスト、セット、マップ（コレクション）は、生成方法によって変更可否が変わってきます。

本記事では、どのようなメソッドを利用した場合にList、Set、Map型の変数が変更可能・変更不可能になるのか、注意点と合わせて解説していきます。

まずはリストを中心に解説します。その後にセットとマップについて解説します。

## リスト

### new ArrayList<>() で生成

`new ArrayList<>()` メソッドで生成したリストは変更可能です。

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

// ... 略 ...

// 初期値なし
List<String> modifiableList1 = new ArrayList<>();

// 初期値あり
@SuppressWarnings("serial")
List<String> modifiableList2 = new ArrayList<>() {
  {
    add("foo");
  }
};
List<String> modifiableList3 = new ArrayList<>(Arrays.asList("foo", "bar"));

// いずれの処理でも例外は発生しない
modifiableList1.add("foo");
modifiableList2.add("bar");
modifiableList3.add("baz");
```

### Arrays.asList() で生成

`Arrays.asList()` メソッドを用いて生成したリストは少し変わっています。
**要素の更新は可能**ですが、**要素の追加と削除は不可能**です。

```java
import java.util.Arrays;
import java.util.List;

// ... 略 ...

List<String> arraysList = Arrays.asList("foo", "bar");

arraysList.set(0, "baz"); // 例外は発生しない
arraysList.add("qux"); // UnsupportedOperationException が発生
arraysList.remove(0); // UnsupportedOperationException が発生
```

asList メソッドは、引数の配列をもとにリストを生成するメソッドです。
このメソッドを用いて生成したリストは配列と同様の特徴を備えています。配列は要素の更新は可能で、要素の追加と削除は不可能です。生成したリストはその特徴を引き継いでいます。
asList メソッドで生成したリストは、「リストの皮を被った配列」とイメージすると良いかもしれません。

### Collections クラスのメソッドで生成

Collections クラスから、変更不可能なコレクションを生成する unmodifiableXxx メソッドが提供されています。（Java 8 以降）
このメソッドを用いて生成したリストは、変更不可能です。

要素の追加、更新、削除を行うと、例外 `java.lang.UnsupportedOperationException` が発生します。

```java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

// ... 略 ...

List<String> aList = new ArrayList<>(Arrays.asList("foo", "bar"));
// 変更不可能なリストを生成
List<String> unmodifiableList1 = Collections.unmodifiableList(aList);

// いずれの処理でも UnsupportedOperationException が発生
unmodifiableList1.add("baz");
unmodifiableList1.set(0, "baz");
unmodifiableList1.remove(0);
```

なお、空コレクションを生成する emptyXxx メソッドと、単一の要素を持つコレクションを生成する singletonXxx メソッドを用いて生成したリストも、変更不可能です。

```java
import java.util.Collections;
import java.util.List;

// ... 略 ...

List<String> emptyList = Collections.emptyList();
List<String> singletonList = Collections.singletonList("foo");

// いずれの処理でも UnsupportedOperationException が発生
emptyList.add("foo");
singletonList.set(0, "bar");
```

### List.of() で生成

List クラスの of メソッドを用いてリストを生成できます。（Java 9 以降）
このメソッドを用いて生成したリストは、変更不可能です。

```java
import java.util.List;

// ... 略 ...

List<String> unmodifiableList2 = List.of("foo", "bar");

unmodifiableList2.add("foo"); // UnsupportedOperationException が発生
```

List クラスの copyOf メソッドを利用することで、リストのコピーを生成できます。（Java 10 以降）
このメソッドを用いて生成したリストは、変更不可能です。

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

// ... 略 ...

List<String> aList = new ArrayList<>(Arrays.asList("foo", "bar"));
List<String> unmodifiableList3 = List.copyOf(aList);

unmodifiableList3.add("foo"); // UnsupportedOperationException が発生
```

### Stream API で生成

Java 8 から追加された Stream API の場合、終端処理によって変更の可否が決まります。
そのため処理対象のリストの変更可否は、Stream API で生成されるリストの変更可否とは無関係です。
toXxx メソッドを利用して生成したリストは、変更可能です。

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

// ... 略 ...

List<String> streamApiList1 = new ArrayList<String>().stream().collect(Collectors.toList());
List<String> streamApiList2 = Arrays.asList("foo").stream().collect(Collectors.toList());
List<String> streamApiList3 = List.of("foo", "bar").stream().collect(Collectors.toList());

// いずれの処理でも例外は発生しない
streamApiList1.add("foo");
streamApiList2.add("bar");
streamApiList3.add("baz");
```

toUnmodifiableXxx メソッドを利用することで、変更不可能なリストを生成できます。（Java 10 以降）

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

// ... 略 ...

List<String> streamApiList4 = List.of("foo", "bar").stream().collect(Collectors.toUnmodifiableList());

streamApiList4.add("baz"); // UnsupportedOperationException が発生
```

### 標準ライブラリを用いる際の注意点

さて、ここまで Java 標準ライブラリを用いて変更可能・変更不可能なリストを生成する方法について記載しました。
サードパーティのライブラリを用いなくても変更可能・変更不可能なリストを容易に生成することができます。

標準ライブラリに用意されているメソッドは確かに便利ではありますが、これらのメソッドを用いて生成したリストにはいくつか注意点があります。

1 つは、変更可能なリストと変更不可能なリストの変数の型が両者とも同じという点です。
処理内容によっては、変数を一見しただけではその変数の変更可否が把握できません。

```java
List<String> fruits = new ArrayList<>(Arrays.asList("apple", "melon"));

MyUser myUser = new MyUser();
myUser.setFavoriteFruits(fruits);
List<String> myUserFavoriteFruits = myUser.getFavoriteFruits();

// UnsupportedOperationException は発生する？
myUserFavoriteFruits.add("foo");
```

MyUser インスタンスに、変更可能なリストである fruits をセットした後、そのインスタンスから fruits を取り出しています。
上記のコードの最後の処理で `UnsupportedOperationException` は発生するのでしょうか？
残念ながら上記のコードだけでは判断できません。setFavoriteFruits メソッドや getFavoriteFruits メソッド内部でどのような処理が行われているかによるからです。
例えば setFavoriteFruits メソッド内部で引数の値を変更不可能なリストとして保持するような処理を実装している場合、getFavoriteFruits メソッドが返すリストは変更不可能です。
ただしそれを把握するためには MyUser クラス内部の処理を確認する必要があります。

もう 1 つの注意点は、処理実行時まで例外がスローされないという点です。

先ほどの例で `UnsupportedOperationException` が発生するかどうかを確認するもう1つの方法として、「実際にコードを実行して例外が発生するか確認する」という方法があります。
この方法であれば MyUser クラスの処理内容を確認する必要はありません。もし例外が発生しなければ変更可能なリスト、例外が発生すれば変更不可能なリストです。
ただしこの手法での確認は現実的に難しい場面も少なくないと思います。

そもそも処理実行時ではなく、コードを実装している段階で変更可否を把握したいところです。
![Spring Tool Suiteで警告メッセージが表示されない](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/945225/2828921a-8c08-d90c-e698-c702d5a5ca8c.png)
上記は Spring Tool Suite のスクショです。

`fruits2.add("banana");` を書いた時点で、そのコードが動作しないことを知らせてほしいところだと思います。
しかし残念ながら、画面上には警告やエラーメッセージは表示されていません。

### サードパーティのライブラリで生成（Google Guava）

サードパーティのライブラリには、より扱いやすいコレクションを提供しているものがあります。

有名なところでいうと Google Guava の ImmutableXxx です。
Google Guava は Apache Commons と同じ類のライブラリと言ったところです。
文字列操作やファイル操作などといった実装頻度の高い処理をより手軽に実装するためのパッケージが含まれています。

Google Guava を用いることで、前述の点を解消した変更不可能なリストを生成することができます。

```java
import java.util.ArrayList;
import com.google.common.collect.ImmutableList;

// ... 略 ...

ImmutableList<String> list1 = ImmutableList.of();

ImmutableList.Builder<String> builder = ImmutableList.builder();
ImmutableList<String> list2 = builder.add("foo").build();

ImmutableList<String> list3 =
    List.of("foo", "bar").stream().collect(ImmutableList.toImmutableList());

// いずれの処理でも UnsupportedOperationException が発生
list1.add("foo");
list2.add("bar");
list3.add("baz");
```

`ImmutableList` という型の変更不可能なリストを生成できます。
これを使用して、変更可能なリストと変更不可能なリストで型を使い分けるようにすれば、生成されたリストの変更可否を容易に確認できます。

また、`ImmutableList` は、 add メソッドのような変更を加えるメソッドを使用した場合に、警告メッセージが表示されます。
![ImmutableListの警告メッセージが表示されている](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/945225/752dba68-04f1-e398-67e3-6ef5279a8524.png)
コンパイルは通りますが、 Eclipse などの IDE 上で使用しているメソッドが非推奨である旨が表示されます。
そのため誤ったコードを実装していることに容易に気付くことができます。

## セット、マップ

さて、ここまでリストを中心に説明してきましたがセット、マップも同様です。
セット、マップそれぞれの変更可否についても以下に記載します。

### 変更可能なセット

```java
// new HashSet
Set<Integer> set1 = new HashSet<>();
@SuppressWarnings("serial")
Set<Integer> set2 = new HashSet<>() {
  {
    add(123);
  }
};

// Stream API の toXxx メソッド（Java 8 以降）
Set<Integer> set3 = new HashSet<Integer>().stream().collect(Collectors.toSet());
Set<Integer> set4 = new HashSet<>(Arrays.asList(123)).stream().collect(Collectors.toSet());
Set<Integer> set5 = Set.of(123).stream().collect(Collectors.toSet());

// いずれの処理でも例外は発生しない
set1.add(1);
set2.add(2);
set3.add(3);
set4.add(4);
set5.add(5);
```

### 変更不可能なセット

```java
// Collections クラスのメソッド（Java 8 以降）
Set<Integer> set1 = Collections.unmodifiableSet(new HashSet<>());
Set<Integer> set2 = Collections.emptySet();
Set<Integer> set3 = Collections.singleton(123);

// of メソッド（Java 9 以降）
Set<Integer> set4 = Set.of();

// Stream API の toUnmodifiableXxx メソッド（Java 10 以降）
Set<Integer> set5 = Set.of(123).stream().collect(Collectors.toUnmodifiableSet());

// Google Guava
ImmutableSet<Integer> set6 = ImmutableSet.of();
ImmutableSet.Builder<Integer> builder = ImmutableSet.builder();
ImmutableSet<Integer> set7 = builder.add(123).build();
ImmutableSet<Integer> set8 = new HashSet<Integer>().stream().collect(ImmutableSet.toImmutableSet());

// いずれの処理でも UnsupportedOperationException が発生
set1.add(1);
set2.add(2);
set3.add(3);
set4.add(4);
set5.add(5);
set6.add(6);
set7.add(7);
set8.add(8);
```

### 変更可能なマップ

```java
// new HashMap
Map<String, Object> map1 = new HashMap<>();
@SuppressWarnings("serial")
Map<String, Object> map2 = new HashMap<>() {
  {
    put("key1", "value1");
  }
};

// Stream API の toXxx メソッド（Java 8 以降）
Map<String, Object> map3 = new HashMap<String, Object>().entrySet().stream()
    .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
Map<String, Object> map4 = Map.of("key1", "value1").entrySet().stream()
    .collect(Collectors.toMap(Entry::getKey, Entry::getValue));

// いずれの処理でも例外は発生しない
map1.put("key1", "value1");
map2.put("key2", "value2");
map3.put("key3", "value3");
map4.put("key4", "value4");
```

### 変更不可能なマップ

```java
// Collections クラスのメソッド（Java 8 以降）
Map<String, Object> map1 = Collections.unmodifiableMap(new HashMap<>());
Map<String, Object> map2 = Collections.emptyMap();
Map<String, Object> map3 = Collections.singletonMap("key1", "value1");

// of メソッド（Java 9 以降）
Map<String, Object> map4 = Map.of();

// Stream API の toUnmodifiableXxx メソッド（Java 10 以降）
Map<String, Object> map5 = Map.of("key1", "value1").entrySet().stream()
    .collect(Collectors.toUnmodifiableMap(Entry::getKey, Entry::getValue));

// Google Guava
ImmutableMap<String, Object> map6 = ImmutableMap.of();
ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder();
ImmutableMap<String, Object> map7 = builder.put("key1", "value1").build();
ImmutableMap<String, Object> map8 = new HashMap<String, Object>().entrySet().stream()
    .collect(ImmutableMap.toImmutableMap(Entry::getKey, Entry::getValue));

// いずれの処理でも UnsupportedOperationException が発生
map1.put("key1", "value1");
map2.put("key2", "value2");
map3.put("key3", "value3");
map4.put("key4", "value4");
map5.put("key5", "value5");
map6.put("key6", "value6");
map7.put("key7", "value7");
map8.put("key8", "value8");
```