JavaでClassCastExceptionになる原因と型変換の確認ポイントを解説する記事のアイキャッチ。

Java

JavaでClassCastExceptionになる原因|型変換で確認すべきポイント

AIによる要約

ClassCastExceptionは、実際のオブジェクトが変換先の型ではないのにキャストした場合に発生します。変数の宣言型がObjectでも、実体の型は変わりません。instanceofで分岐する方法はありますが、キャストが多い設計ではジェネリクス、専用DTO、型付きAPIへ見直します。raw ListやMap、外部ライブラリの戻り値は特に実行時まで誤りを検出できないため、入力境界で検証します。

新人SE
新人SE
ObjectをStringへキャストした行でClassCastExceptionになりました。コンパイルは通ったのに。
ポンコツSE
ポンコツSE
実体がStringではなかったためです。コンパイラが保証できないキャストは実行時に確認されます。

Object、Map、古いraw List、外部ライブラリの汎用戻り値を扱うと、キャストが必要になることがあります。コンパイルが通っても、実データの型が異なるとClassCastExceptionです。

とりあえずinstanceofを増やせば例外は避けられますが、型情報が失われた入口を放置すると分岐が各所へ広がります。型付きのAPIやDTOへ変換する境界を作ることが根本対策です。

この記事のポイント

  • キャストは実体の型を変更しない
  • 互換性のない型へのキャストでClassCastExceptionになる
  • instanceofで安全に確認できる
  • raw typeやObject中心の設計は実行時エラーを増やす
  • ジェネリクスと専用DTOで型情報をコンパイル時へ戻す

キャストはオブジェクトを変換しない

キャストは、参照しているオブジェクトを別の型のインスタンスへ作り変える処理ではありません。その実体を指定型として扱えることをコンパイラとJVMへ伝えます。

Object value = Integer.valueOf(100);
String text = (String) value; // ClassCastException

Integerの実体は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を使うと、値の型を呼び出し側が推測します。JSONライブラリによって整数がInteger、Long、BigDecimalなどになることもあります。

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版」の表紙
商品画像:Amazon.co.jp
PR Java基礎学習後の若手向け

スッキリわかるJava入門 実践編 第5版

中山 清喬 (著), 株式会社フレアリンク (監修)

基礎文法の次に必要な、現場寄りのJava知識を補う。

コレクション、ジェネリクス、ラムダ式、ストリームなど、業務コードで出会いやすい機能を入門編の次に学べます。

  • Java基礎の次に何を学ぶか迷っている
  • コレクションやStreamを整理したい

当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。

この記事とあわせて読みたい

まとめ

ClassCastExceptionは、実体が変換先の型ではないオブジェクトをキャストしたときに発生します。キャストは値やオブジェクトを別の型へ変換する処理ではありません。

instanceofで確認できますが、raw typeやMapが広がる設計では、ジェネリクスと専用DTOで型情報を戻すことが根本対策です。異なる実体型とnullを境界でテストしてください。

-Java
-, , , ,