Javaでアプリケーションを開発している際、リストに要素を追加したり削除したりしようとしたタイミングで、突如として java.lang.UnsupportedOperationException という例外が発生し、プログラムが停止してしまうことがあります。
この例外は、名前の通り「サポートされていない操作」を実行しようとしたことを示すものですが、一見すると普通の java.util.List を操作しているように見えるため、初心者だけでなく経験のあるエンジニアでも原因の特定に戸惑うことが少なくありません。
特に、Java 9以降で導入された便利なリスト生成メソッドや、歴史の長い Arrays.asList() などの仕様を正確に把握していないと、この問題は頻繁に発生します。
本記事では、テクニカルライターの視点から、この例外が発生する内部メカニズムを解き明かし、具体的な再現コードとその解決策、さらには設計レベルでこの例外を防ぐためのベストプラクティスまでを網羅的に解説します。
java.lang.UnsupportedOperationExceptionとは何か
java.lang.UnsupportedOperationException は、Java標準ライブラリのランタイム例外(非チェック例外)の一つです。
この例外の定義は非常にシンプルで、「呼び出されたメソッドが、そのオブジェクトの型では実装されていない(許可されていない)場合」 にスローされます。
Javaのコレクションフレームワーク(Collections Framework)において、多くのインターフェース(例:List, Set, Map)は、要素の追加(add)や削除(remove)といった変更操作を「オプションの操作(Optional Operations)」として定義しています。
実装クラスはこれらのメソッドを実装する義務がありますが、もしそのクラスが「不変(Immutable)」であったり「固定長(Fixed-size)」であったりする場合、実装内容として単にこの例外をスローするように設計されています。
なぜコンパイルエラーにならないのか
開発者にとって厄介なのは、この問題が コンパイル時ではなく実行時に発覚する 点です。
インターフェースとしての List には add() メソッドが存在するため、コンパイラはそのリストが実際に要素追加を許可しているかどうかを判断できません。
その結果、プログラムを実行して初めて「このリストは変更できないタイプだった」という事実に直面することになります。
主な発生原因1:Arrays.asList() による固定長リスト
最も古典的かつ頻繁に見られる原因は、配列をリスト形式に変換する java.util.Arrays.asList() メソッドの使用です。
このメソッドは、配列をリストとして扱うための便利なラッパーを返しますが、その実体は java.util.ArrayList (よく使われる可変のArrayListとは別物の内部クラス)であり、サイズを変更する操作が禁止されています。
再現コードの例
以下のコードは、配列からリストを作成し、その後に要素を追加しようとして例外が発生する典型的なパターンです。
import java.util.Arrays;
import java.util.List;
public class ArraysAsListExample {
public static void main(String[] args) {
// 配列をリストに変換(固定長リストが生成される)
String[] colorsArray = {"Red", "Green", "Blue"};
List<String> colorList = Arrays.asList(colorsArray);
System.out.println("初期リスト: " + colorList);
try {
// リストに新しい要素を追加しようとする
colorList.add("Yellow"); // ここで UnsupportedOperationException が発生
} catch (UnsupportedOperationException e) {
System.err.println("エラー発生: 要素の追加はサポートされていません。");
e.printStackTrace();
}
}
}
初期リスト: [Red, Green, Blue]
エラー発生: 要素の追加はサポートされていません。
java.lang.UnsupportedOperationException
at java.base/java.util.AbstractList.add(AbstractList.java:153)
at java.base/java.util.AbstractList.add(AbstractList.java:111)
at ArraysAsListExample.main(ArraysAsListExample.java:14)
このケースでは、add() だけでなく remove() も同様に失敗します。
一方で、set() メソッドによる 既存要素の置換は可能 です。
これは、内部の配列構造を維持したまま値を書き換えるだけであり、リストのサイズ自体は変わらないためです。
主な発生原因2:List.of() や Set.of() による不変コレクション
Java 9で導入された便利メソッド List.of(), Set.of(), Map.of() は、コードを簡潔に記述できるため非常に普及しています。
しかし、これらのメソッドが返すコレクションは 完全に不変(Immutable) です。
前述の Arrays.asList() は「固定長(値の変更は可)」でしたが、List.of() などで作成されたリストは、要素の追加、削除、さらには 既存要素の書き換え(set)すら許可されません。
再現コードの例
import java.util.List;
public class ListOfExample {
public static void main(String[] args) {
// Java 9以降の不変リスト生成
List<String> immutableList = List.of("Apple", "Banana", "Orange");
try {
// 要素の置換を試みる
immutableList.set(0, "Grape"); // ここで UnsupportedOperationException が発生
} catch (UnsupportedOperationException e) {
System.err.println("エラー発生: 不変リストの内容は変更できません。");
}
}
}
エラー発生: 不変リストの内容は変更できません。
現代的なJava開発では、意図しない副作用を防ぐために不変リストが推奨される場面が多いですが、その性質を理解せずに従来の java.util.ArrayList と同じ感覚で扱ってしまうと、この例外に遭遇することになります。
主な発生原因3:Collections.unmodifiableList() による読み取り専用ビュー
既存の可変リストを他クラスに公開する際、勝手に変更されないよう Collections.unmodifiableList() でラップすることがあります。
これもまた、変更操作に対して例外をスローします。
List<String> mutableList = new ArrayList<>();
mutableList.add("Data");
// 読み取り専用ビューを作成
List<String> readOnlyList = Collections.unmodifiableList(mutableList);
// 元のリストは変更可能だが、readOnlyList経由の操作は禁止
readOnlyList.add("New Data"); // Exception!
この手法はカプセル化を守るために重要ですが、呼び出し側が「これは読み取り専用である」という制約を知らずに操作を試みると、実行時にエラーとなります。
UnsupportedOperationExceptionの解決策
この例外を解消するためのアプローチは、主に2つあります。
「リストを可変にする方法」と「設計を見直す方法」です。
1. 新しい可変リストへコピーする
最も直接的で一般的な解決策は、固定長や不変のリストをソースとして、新しく java.util.ArrayList などの可変リストをインスタンス化することです。
// Arrays.asList() を使う場合
List<String> mutableList = new ArrayList<>(Arrays.asList("A", "B", "C"));
mutableList.add("D"); // 成功
// List.of() を使う場合
List<String> anotherMutableList = new ArrayList<>(List.of("X", "Y"));
anotherMutableList.add("Z"); // 成功
このように コンストラクタに元のコレクションを渡すことで、内容をコピーした新しい独立した可変リストが作成されます。
これにより、元のリストの制約を受けることなく自由に操作が可能になります。
2. 生成方法と用途を比較する
用途に応じて、適切な生成メソッドを選択することが重要です。
以下の表に主要なメソッドの特性をまとめました。
| 生成メソッド | 追加・削除 | 要素の置換 (set) | 特徴 |
|---|---|---|---|
new ArrayList<>() | 可能 | 可能 | 標準的な可変リスト。 |
Arrays.asList(...) | 不可 | 可能 | 配列のビュー。固定長。 |
List.of(...) | 不可 | 不可 | Java 9+。完全不変。null不許可。 |
Collections.unmodifiableList(...) | 不可 | 不可 | 既存リストへの読み取り専用窓口。 |
3. Stream APIを利用したリスト生成
Java 8以降のStream APIを使用してリストを生成する場合、最終的な出力先が可変かどうかを意識する必要があります。
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
// 可変リストを生成する場合 (Collectors.toList() は実装依存だが、通常ArrayListを返す)
List<String> mutable = Stream.of("a", "b").collect(Collectors.toList());
// Java 16以降の toList() は不変リストを返すため、修正が必要な場合は以下のように書く
List<String> immutable = Stream.of("a", "b").toList(); // 不変
List<String> trulyMutable = new ArrayList<>(Stream.of("a", "b").toList()); // 可変
実践的な設計上の注意点
プログラムの規模が大きくなると、どこで生成されたリストが UnsupportedOperationException を引き起こすのか追跡が難しくなります。
これを防ぐための設計指針をいくつか紹介します。
インターフェースのドキュメント化
メソッドの戻り値としてリストを返す場合、そのリストが変更可能かどうかをJavaDocに明記することを強く推奨します。
「Returns an unmodifiable list containing…」といった記述があるだけで、利用者は事前にコピーを作成するなどの対策が取れます。
防御的コピー(Defensive Copy)の徹底
クラス内部の状態をリストとして外部に渡す際は、「内部リストをそのまま渡さない」 ことが鉄則です。
- 外部に変更させたくないなら:
List.copyOf()やCollections.unmodifiableList()を使う。 - 外部で自由に変更させて良いなら:
new ArrayList<>(internalList)でコピーを渡す。
これにより、内部データが意図せず書き換えられるリスクを抑えつつ、利用側の期待に沿った挙動を提供できます。
fail-fast(早期失敗)の活用
もしリストを操作する箇所が複数ある場合、操作の直前でチェックを行うのではなく、リストを受け取った段階で「変更可能であるべきかどうか」を意識した実装を心がけます。
デバッグ時には、スタックトレースを辿って 「どこでこの不変リストが生成されたか」 を特定することが解決への近道です。
まとめ
java.lang.UnsupportedOperationException は、Javaのコレクションフレームワークにおける「柔軟性」と「安全性」のトレードオフから生じる例外です。
すべてのリストが同じように振る舞うわけではなく、生成方法によって「可変」「固定長」「不変」という明確な性質の違いがあることを理解することが、この問題を解決する鍵となります。
現代のJava開発においては、「基本的には不変リスト(List.of)を使い、変更が必要な場合のみ明示的にArrayListを作成する」 というスタンスが推奨されています。
これにより、データの整合性を保ちつつ、予期せぬ例外を未然に防ぐ堅牢なコードを記述できるようになります。
今回の解説を参考に、エラーメッセージが表示された際は、まず「そのリストがどのように生成されたか」を確認し、適切なコピー処理や生成メソッドの選択を行うようにしてください。






