AIによる要約
ClassCastExceptionは、実際のオブジェクトが変換先の型ではないのにキャストした場合に発生します。変数の宣言型がObjectでも、実体の型は変わりません。instanceofで分岐する方法はありますが、キャストが多い設計ではジェネリクス、専用DTO、型付きAPIへ見直します。raw ListやMap


Object、Map
とりあえずinstanceofを増やせば例外は避けられますが、型情報が失われた入口を放置すると分岐が各所へ広がります。型付きのAPIやDTOへ変換する境界を作ることが根本対策です。
この記事のポイント
- キャストは実体の型を変更しない
- 互換性のない型へのキャストでClassCastExceptionになる
- instanceofで安全に確認できる
- raw typeやObject中心の設計は実行時エラーを増やす
- ジェネリクスと専用DTOで型情報をコンパイル時へ戻す
キャストはオブジェクトを変換しない
キャストは、参照しているオブジェクトを別の型のインスタンスへ作り変える処理ではありません。その実体を指定型として扱えることをコンパイラとJVMへ伝えます。
Object value = Integer.valueOf(100);
String text = (String) value; // ClassCastExceptionIntegerの実体はStringではありません。数値を文字列へ変換したいなら、キャストではなくString.valueOf(value)などの変換APIを使います。
継承関係があっても実体を確認する
親型の変数から子型へダウンキャストできるのは、実際のオブジェクトがその子型である場合だけです。
Animal animal = new Dog();
Dog dog = (Dog) animal; // 正常
Animal another = new Cat();
Dog invalid = (Dog) another; // ClassCastExceptionメソッドがAnimalを返すのに呼び出し側が毎回Dogへキャストするなら、戻り値契約やポリモーフィズムの使い方を見直します。
instanceofのパターンマッチング
型が複数あり得る入力では、instanceofで確認してから扱います。対応外の型を黙って無視せず、エラーや既定動作を決めます。
if (value instanceof String text) {
return text.trim();
}
throw new IllegalArgumentException(
"文字列を期待しました: " + value.getClass().getName());valueがnullならinstanceofはfalseです。ただしエラーメッセージでgetClassを呼ぶ前にnullを確認します。ログへ型名を出すのは有用ですが、値全体を不用意に出しません。
raw Listが実行時エラーを生む
ジェネリクスを付けないListには異なる型を混在できます。取り出し時の暗黙キャストでClassCastExceptionになります。
List values = new ArrayList();
values.add("A");
values.add(100);
for (Object value : values) {
String text = (String) value; // 100で例外
}Listにすれば、Integerをaddした時点でコンパイルエラーになります。問題を本番実行時ではなく開発時に発見できます。
Mapの境界を作る
JSON、テンプレートモデル、汎用パラメータでMap
Object rawAge = attributes.get("age");
if (!(rawAge instanceof Number number)) {
throw new InvalidInputException("age", "数値ではありません");
}
int age = number.intValue();業務ロジックの中までMapを渡さず、Controllerやアダプターで専用DTOへ変換します。必須項目、型、範囲を1か所で検証できます。
ジェネリクスで型を伝える
共通処理がObjectを返し、呼び出し側がキャストする設計では、型の対応関係が外から見えません。型パラメータやClassトークンを使い、APIで期待型を表します。
interface Converter<S, T> {
T convert(S source);
}
Converter<String, Integer> quantityConverter =
Integer::valueOf;無理なジェネリクス化でunchecked castを内部へ隠すだけでは改善しません。コンパイラが型安全性を検証できる設計になっているか確認します。
Class.castを使う場面
実行時に期待型を引数として受け取る共通部品では、Class.castで型チェックとキャストをまとめられます。失敗時はClassCastExceptionですが、期待型をAPIに明示できます。
static <T> T requireType(Object value, Class<T> type) {
return type.cast(value);
}
String text = requireType(value, String.class);通常の業務ServiceでClass.castを多用する必要はありません。フレームワークや共通基盤のように型を動的に扱う場面に限定します。
JUnitで異なる実体型を入れる
正常な型だけでなく、異なる型、null、サブクラス、数値型の違いをテストします。変換境界ならClassCastExceptionをそのまま期待せず、入力エラーへ変換できているか確認します。
@Test
void ageが文字列なら入力エラー() {
Map<String, Object> input = Map.of("age", "twenty");
assertThrows(InvalidInputException.class,
() -> mapper.toUser(input));
}現場レビューでよくある指摘
キャストのレビューではinstanceofの追加だけでなく、なぜ型情報がObjectまで失われたのか、境界で型付きDTOへ戻せないかを確認します。
// レビューコメント例
valueの実体はIntegerなのでStringへキャストできません。
文字列化が目的ならString.valueOfを使ってください。
// レビューコメント例
raw Listのため異なる型を追加でき、取り出し時まで検出できません。
List<String>のようにジェネリクスを指定してください。
// レビューコメント例
Map<String,Object>をServiceまで渡すと各所でキャストが必要です。
Controller境界で入力DTOへ変換し、型と必須項目を検証してください。例外が起きた値だけを特別扱いせず、同じAPIからどの型が返り得るか一覧化します。型を推測するコードを減らし、コンパイル時に検出できる形へ寄せます。
提出前のセルフチェック
レビュー前に確認すること
- 実体の型を確認したか
- キャストと値変換を混同していないか
- ダウンキャスト前にinstanceofが必要か
- raw Listやraw Mapを使っていないか
- unchecked警告を無視していないか
- Map
を業務層まで渡していないか - ジェネリクスで型を表せないか
- 異なる型とnullをテストしたか
Javaの型システムを実践的に学ぶ参考書
継承、ポリモーフィズム、ジェネリクスを実践例で学ぶと、キャストで型を合わせるのではなく、型安全なAPIを設計する判断が身に付きます。
スッキリわかるJava入門 実践編 第5版
基礎文法の次に必要な、現場寄りのJava知識を補う。
コレクション、ジェネリクス、ラムダ式、ストリームなど、業務コードで出会いやすい機能を入門編の次に学べます。
- Java基礎の次に何を学ぶか迷っている
- コレクションやStreamを整理したい
当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。
この記事とあわせて読みたい
まとめ
ClassCastExceptionは、実体が変換先の型ではないオブジェクトをキャストしたときに発生します。キャストは値やオブジェクトを別の型へ変換する処理ではありません。
instanceofで確認できますが、raw typeやMap
