ClassCastExceptionとは、オブジェクトを「その中身の型ではない型」にキャストしたときに実行時に出る例外です。 直し方は大きく3つで、キャストの前に instanceof で型を確かめる、数値は Number 経由で変換する、ジェネリクスの型引数を付けてキャスト自体をなくす、のどれかに当てはまります。

この記事では、ダウンキャスト、Integer から Long、Object 配列から String 配列、raw型のリストなど8つのパターンを実際に動かし、エラー文の全文を載せました。エラー文の一部から原因と直し方を引ける表と、そのまま貼って使える書き換え用のコードを用意しています。

確認環境:Ubuntu 24.04 / OpenJDK 21.0.10(javac でコンパイルし、java コマンドで実行)。

ClassCastExceptionの早見表(エラー文から原因を引く)

エラー文は「class A cannot be cast to class B」の形です。A が実際に入っていた型、B がキャストしようとした型です。A と B の組み合わせで原因がほぼ決まります。

エラー文に出る組み合わせ(A → B) よくある原因 直し方
Downcast$Cat → Downcast$Dog(兄弟のクラス) 親の型の変数に別の子クラスが入っていた instanceof のパターンマッチングで確かめる。共通の動作は親のメソッドにする
java.lang.Integer → java.lang.Long Map<String, Object> などから取り出した数値の型が想定と違う Number にキャストして longValue()
java.lang.Double → java.lang.Integer 小数として入っていた値を整数にキャスト Number 経由で intValue() や longValue()
java.lang.String → java.lang.Integer 文字列をキャストで数値にしようとした Integer.parseInt() で変換
java.lang.Integer → java.lang.String raw型のリストに別の型が混ざっていた 宣言に型引数を付ける。数値を文字列にするなら String.valueOf()
[Ljava.lang.Object; → [Ljava.lang.String; list.toArray() の戻り値を String[] にキャスト toArray(new String[0])
自作クラス → java.lang.Comparable TreeSet や TreeMap のキー、sort に、比較方法のないクラスを入れた Comparable を実装するか Comparator を渡す
Plugin → Plugin(同じ名前どうし) 同じクラスが別々のクラスローダーから読み込まれている 共通のクラスを片方のローダーだけから読み込む構成にする
Cannot cast java.lang.Integer to java.lang.String Class.cast() での型違い isInstance() で確かめてから cast()
Attempt to insert class ... element into collection with element type ... Collections.checkedList などに違う型を入れた 入れている行(スタックトレースの行)を直す

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java ClassCastExceptionの原因と対処|エラー文別の直し方」

ClassCastExceptionとは何か

ClassCastException は java.lang パッケージの例外で、RuntimeException の子クラスです。Java 21 の API ドキュメントでは「オブジェクトを、そのインスタンスではないサブクラスにキャストしようとしたことを示すためにスローされる」と説明されています(出典は記事末尾)。

ポイントは次の3つです。

  • 非検査例外(RuntimeException の子)なので、try-catch や throws を書かなくてもコンパイルは通ります
  • コンパイル時には見つからず、実行して初めて出る例外です。変数の型(Object や親クラス)からは中身の型がわからないためです
  • 変換ではなく「見方を変える」のがキャストです。(Long) と書いても Integer が Long に作り替えられることはありません

エラー文の読み方

実際のエラー文を分解します。

Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.Long (java.lang.Integer and java.lang.Long are in module java.base of loader 'bootstrap')
    at IntegerToLong.main(IntegerToLong.java:9)
部分 意味
class java.lang.Integer 実際に入っていたオブジェクトの型
cannot be cast to class java.lang.Long キャストしようとした先の型
かっこの中 are in module java.base of loader 'bootstrap' それぞれのクラスが属するモジュールと、読み込んだクラスローダー
at IntegerToLong.main(IntegerToLong.java:9) キャストが実行された行(ファイル名:行番号)

かっこの中の loader は、標準ライブラリのクラスなら 'bootstrap'、自分で書いたクラスを java コマンドで実行した場合は 'app' になります。java Downcast.java のようにソースファイルを直接実行した場合は、ここが loader com.sun.tools.javac.launcher.Main$MemoryClassLoader @4148db48 のような表示に変わります。

クラス名の $ は「内側に書いたクラス」の区切りで、Downcast$Cat は Downcast クラスの中の Cat という意味です。

パターン別:再現コードとエラー文全文

どのコードも、ファイル名をクラス名に合わせて保存し、javac ファイル名.java のあと java クラス名 で実行できます。

パターン1:ダウンキャスト(親の型から子の型へ)

親クラスの変数を子クラスの型にキャストするのがダウンキャストです。中身が本当にその子クラスなら通り、別の子クラスなら例外になります。

public class Downcast {
    static class Animal {}
    static class Dog extends Animal { void bark() { System.out.println("ワン"); } }
    static class Cat extends Animal {}

    public static void main(String[] args) {
        Animal a1 = new Dog();
        Dog d1 = (Dog) a1;      // 中身が Dog なので通る
        d1.bark();

        Animal a2 = new Cat();
        Dog d2 = (Dog) a2;      // 中身は Cat。ここで ClassCastException
        d2.bark();
    }
}
ワン
Exception in thread "main" java.lang.ClassCastException: class Downcast$Cat cannot be cast to class Downcast$Dog (Downcast$Cat and Downcast$Dog are in unnamed module of loader 'app')
    at Downcast.main(Downcast.java:12)

1つ目のキャストは通って「ワン」と表示され、2つ目で止まっています。同じ書き方でも、変数に入っている中身しだいで結果が変わるのがダウンキャストの特徴です。

直した後のコードです。共通の動作は親クラスに寄せ、子クラスだけの動作は型を確かめてから呼びます。

import java.util.List;

public class DowncastFix {
    static class Animal {
        String voice() { return "……"; }
    }
    static class Dog extends Animal {
        @Override String voice() { return "ワン"; }
        void fetch() { System.out.println("ボールを取ってくる"); }
    }
    static class Cat extends Animal {
        @Override String voice() { return "ニャー"; }
    }

    public static void main(String[] args) {
        List<Animal> animals = List.of(new Dog(), new Cat());
        for (Animal a : animals) {
            // 直し方1:共通の動作は親クラスのメソッドにして、キャスト自体をなくす
            System.out.println(a.voice());

            // 直し方2:子クラスだけの動作は、型を確かめてから呼ぶ
            if (a instanceof Dog d) {
                d.fetch();
            }
        }
    }
}
ワン
ボールを取ってくる
ニャー

パターン2:Integer を Long にキャスト

Map<String, Object> や JSON を読み込んだ結果などでよく出ます。小さい整数は Integer、大きい整数は Long で入っているため、値によって通ったり止まったりします。

import java.util.HashMap;
import java.util.Map;

public class IntegerToLong {
    public static void main(String[] args) {
        Map<String, Object> row = new HashMap<>();
        row.put("id", 100);              // int は Integer に包まれて入る

        Long id = (Long) row.get("id");  // Integer を Long にキャスト
        System.out.println(id);
    }
}
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.Long (java.lang.Integer and java.lang.Long are in module java.base of loader 'bootstrap')
    at IntegerToLong.main(IntegerToLong.java:9)

int から long へは自動で広がるので、同じ感覚で書きたくなる箇所です。けれども Integer と Long はどちらも Number の子クラスで、互いに親子ではありません。兄弟どうしなので、パターン1の Cat と Dog と同じ関係です。直し方は後半の「数値は Number 経由で変換する」にまとめました。

パターン3:Object 配列を String 配列にキャスト

import java.util.List;

public class ObjectArray {
    public static void main(String[] args) {
        List<String> names = List.of("sato", "suzuki");
        String[] arr = (String[]) names.toArray();   // toArray() の戻りは Object[]
        System.out.println(arr.length);
    }
}
Exception in thread "main" java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.String; ([Ljava.lang.Object; and [Ljava.lang.String; are in module java.base of loader 'bootstrap')
    at ObjectArray.main(ObjectArray.java:6)

[Ljava.lang.Object; は「Object の配列」を表す内部の名前です。先頭の [ が配列、L〜; がクラス名を示します。引数なしの toArray() は、中身がすべて文字列でも Object 配列を作って返すため、String 配列にはキャストできません。

配列の内部名は次のとおりです(後半の ArrayFix.java の出力で確認できます)。

エラー文の表記 意味
[Ljava.lang.String; String[]
[Ljava.lang.Object; Object[]
[I int[]
[J long[]
[D double[]
[B byte[]
[[I int[][]

パターン4:raw型のリストから取り出したとき

raw型とは、List のように型引数を付けずに使うジェネリクス型のことです。何でも入れられる代わりに、取り出す側で型が合わなくなります。

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

public class RawList {
    public static void main(String[] args) {
        List raw = new ArrayList();          // raw型(型引数なし)
        raw.add("apple");
        raw.add(123);                        // 何でも入ってしまう

        List<String> names = raw;            // 警告は出るがコンパイルは通る
        for (String s : names) {             // 取り出すときに見えないキャストが入る
            System.out.println(s.length());
        }
    }
}
5
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
    at RawList.main(RawList.java:11)

ソースのどこにも (String) と書いていないのに ClassCastException が出ています。ジェネリクスの型引数は実行時には残らず、コンパイラが取り出す箇所に見えないキャストを入れているためです。そのため例外の行番号は「入れた行(8行目)」ではなく「取り出した行(11行目)」を指します。

このコードは、コンパイル時に次のメッセージが出ます。

Note: RawList.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

この Note が出たら、ClassCastException の種がある合図です。詳しくは後半の「unchecked 警告との対応」で扱います。

パターン5:文字列を Integer にキャスト

Object value = "123"; を (Integer) value とキャストすると、class java.lang.String cannot be cast to class java.lang.Integer になります(全文は後半の CastBasics.java の出力にあります)。文字列と数値の行き来はキャストではなく変換メソッドで行います。Integer.parseInt("123")、String.valueOf(123) です。なお、変数の型が最初から String だとわかっている場合は、実行する前にコンパイルエラーになります。

public class Impossible {
    public static void main(String[] args) {
        String s = "123";
        Integer n = (Integer) s;
        Integer i = 100;
        Long l = (Long) i;
    }
}
Impossible.java:4: error: incompatible types: String cannot be converted to Integer
        Integer n = (Integer) s;
                              ^
Impossible.java:6: error: incompatible types: Integer cannot be converted to Long
        Long l = (Long) i;
                        ^
2 errors

コンパイラが止めてくれるのは、変数の型だけで「絶対に通らない」と判断できるキャストです。変数の型が Object や親クラスだと判断できないので、実行時の ClassCastException になります。

パターン6:型引数を戻り値にしたメソッド

import java.util.HashMap;
import java.util.Map;

public class GenericReturn {
    static Map<String, Object> store = new HashMap<>();

    static <T> T get(String key) {
        return (T) store.get(key);     // 実行時には何も確かめないキャスト
    }

    public static void main(String[] args) {
        store.put("count", "5");
        Integer count = get("count");  // 例外はメソッドの中ではなく、この行で出る
        System.out.println(count);
    }
}
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
    at GenericReturn.main(GenericReturn.java:13)

(T) と書いた8行目ではなく、呼び出し側の13行目で例外が出ています。(T) のキャストは実行時には何も確かめず、受け取った側で Integer として使おうとした瞬間に確かめられるためです。直すときは Class<T> を引数に受け取り、後述の Class.cast() を使います。

パターン7:Comparable を実装していないクラスを TreeSet に入れる

import java.util.TreeSet;

public class NotComparable {
    record Item(String name) {}

    public static void main(String[] args) {
        TreeSet<Item> set = new TreeSet<>();
        set.add(new Item("pen"));      // Item は Comparable を実装していない
    }
}
Exception in thread "main" java.lang.ClassCastException: class NotComparable$Item cannot be cast to class java.lang.Comparable (NotComparable$Item is in unnamed module of loader 'app'; java.lang.Comparable is in module java.base of loader 'bootstrap')
    at java.base/java.util.TreeMap.compare(TreeMap.java:1604)
    at java.base/java.util.TreeMap.addEntryToEmptyMap(TreeMap.java:811)
    at java.base/java.util.TreeMap.put(TreeMap.java:820)
    at java.base/java.util.TreeMap.put(TreeMap.java:569)
    at java.base/java.util.TreeSet.add(TreeSet.java:259)
    at NotComparable.main(NotComparable.java:8)

スタックトレースの一番上が java.base/java.util.TreeMap.compare になっています。自分のコードにキャストがなくても、ライブラリの中でキャストされて出る例です。この場合は一番下の NotComparable.main(NotComparable.java:8) が自分のコードの入口です。直し方は、record Item(String name) implements Comparable<Item> として compareTo を書くか、new TreeSet<>(Comparator.comparing(Item::name)) のように比較方法を渡します。

パターン8:同じクラス名なのにキャストできない

エラー文の A と B が同じ名前のときは、クラスローダーが原因です。Java では、クラス名が同じでも、読み込んだクラスローダーが違えば別の型として扱われます。

public class Plugin {
    public String toString() { return "Plugin"; }
}
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;

public class TwoLoaders {
    public static void main(String[] args) throws Exception {
        URL[] urls = { Path.of("plugin/").toUri().toURL() };
        // 親を持たない別のクラスローダーで Plugin を読み込む
        try (URLClassLoader loader = new URLClassLoader(urls, null)) {
            Object obj = loader.loadClass("Plugin").getDeclaredConstructor().newInstance();
            System.out.println(obj.getClass().getName());
            Plugin p = (Plugin) obj;   // 名前は同じでも別のクラスとして扱われる
            System.out.println(p);
        }
    }
}
javac Plugin.java TwoLoaders.java
mkdir plugin && cp Plugin.class plugin/
java TwoLoaders
Plugin
Exception in thread "main" java.lang.ClassCastException: class Plugin cannot be cast to class Plugin (Plugin is in unnamed module of loader java.net.URLClassLoader @70dea4e; Plugin is in unnamed module of loader 'app')
    at TwoLoaders.main(TwoLoaders.java:12)

かっこの中に loader java.net.URLClassLoader @70dea4e と loader 'app' の2つが並んでいます。同じ名前どうしのエラーでは、かっこの中のローダー名を見比べるのが近道です。アプリケーションサーバーやプラグインの仕組みで、同じ jar が2か所に置かれているときに起こります。共通で使うクラスは片方(親側)のローダーだけから読み込まれるように配置を見直します。

直し方1:instanceof のパターンマッチングで確かめてからキャスト

Java 16 から、instanceof で型の確認・キャスト・変数宣言を1行で書けるようになりました(JEP 394。JDK 14 と 15 ではプレビュー機能でした)。

public class OldStyle {
    // Java 15 までの書き方:確認とキャストを別々に書く
    static int lengthBefore(Object o) {
        if (o instanceof String) {
            String s = (String) o;
            return s.length();
        }
        return -1;
    }

    // Java 16 以降:確認・キャスト・変数宣言を1行で
    static int lengthAfter(Object o) {
        if (o instanceof String s) {
            return s.length();
        }
        return -1;
    }

    public static void main(String[] args) {
        System.out.println(lengthBefore("hello") + " " + lengthBefore(123));
        System.out.println(lengthAfter("hello") + " " + lengthAfter(123));
    }
}
5 -1
5 -1

型を書くのが1回になるので、確かめる型とキャストする型が食い違うことがなくなります。

コピペ用:instanceof と switch のパターンマッチング

import java.util.List;

public class PatternMatch {
    sealed interface Shape permits Circle, Square {}
    record Circle(double r) implements Shape {}
    record Square(double side) implements Shape {}

    // instanceof のパターンマッチング(Java 16以降)
    static String describe(Object o) {
        if (o instanceof String s && !s.isEmpty()) {     // && で条件を続けられる
            return "文字列 長さ=" + s.length();
        }
        if (!(o instanceof Number n)) {                  // 否定して早めに戻る
            return "対象外: " + (o == null ? "null" : o.getClass().getName());
        }
        return "数値 long=" + n.longValue();             // ここでも n を使える
    }

    // switch のパターンマッチング(Java 21以降)
    static String kind(Object o) {
        return switch (o) {
            case null -> "null";
            case Integer i when i < 0 -> "負の整数 " + i;
            case Integer i -> "整数 " + i;
            case Long l -> "long " + l;
            case String s -> "文字列 " + s;
            case int[] arr -> "int配列 長さ" + arr.length;
            default -> "その他 " + o.getClass().getSimpleName();
        };
    }

    // sealed インターフェースなら default なしで全種類を網羅できる
    static double area(Shape shape) {
        return switch (shape) {
            case Circle c -> Math.PI * c.r() * c.r();
            case Square q -> q.side() * q.side();
        };
    }

    public static void main(String[] args) {
        System.out.println(describe("hello"));
        System.out.println(describe(100));
        System.out.println(describe(List.of()));
        System.out.println(describe(null));

        System.out.println(kind(-5));
        System.out.println(kind(5));
        System.out.println(kind(5L));
        System.out.println(kind("abc"));
        System.out.println(kind(new int[3]));
        System.out.println(kind(1.5));
        System.out.println(kind(null));

        System.out.printf("%.2f%n", area(new Circle(1)));
        System.out.println(area(new Square(3)));
    }
}
文字列 長さ=5
数値 long=100
対象外: java.util.ImmutableCollections$ListN
対象外: null
負の整数 -5
整数 5
long 5
文字列 abc
int配列 長さ3
その他 Double
null
3.14
9.0

使い分けの目安です。

書き方 使える版 向いている場面
if (o instanceof String s) Java 16以降 確かめる型が1〜2種類
if (!(o instanceof Number n)) return ...; Java 16以降 対象外を先に返して、後の処理を平らに書く
switch (o) { case Integer i -> ... } Java 21以降 3種類以上の型で分ける
case Integer i when i < 0 Java 21以降 型に加えて値でも分ける
case null -> Java 21以降 null も switch の中で扱う
sealed インターフェース + switch Java 21以降 種類が決まっている型を、もれなく分ける

switch のパターンマッチングは Java 21 で正式な機能になりました(JEP 441)。JEP 441 には、case null がない switch に null を渡すと従来どおり NullPointerException になること、switch は取りうる値をすべて扱う必要があること、sealed な型なら default を省けることが書かれています。

switch で case を書く順番

型の switch は上から順に当てはめるので、広い型を先に書くと、後ろの狭い型に届かなくなります。この場合はコンパイルエラーで知らせてくれます。

public class SwitchOrder {
    static String kind(Object o) {
        return switch (o) {
            case Number n -> "数値";
            case Integer i -> "整数";
            default -> "その他";
        };
    }
}
SwitchOrder.java:5: error: this case label is dominated by a preceding case label
            case Integer i -> "整数";
                 ^
1 error

case Integer i を case Number n より先に書けば解消します。

直し方2:数値は Number 経由で変換する

Integer、Long、Double、Short、Byte、Float、BigDecimal、BigInteger はすべて Number の子クラスです。どの数値型で入っているかわからない値は、いったん Number として受け取り、longValue() や intValue() で取り出すと、型の違いで止まらなくなります。

import java.util.List;
import java.util.Map;

public class NumberConvert {
    // どの数値型で入っていても long にそろえる
    static long toLong(Object o) {
        return switch (o) {
            case Number n -> n.longValue();
            case String s -> Long.parseLong(s.trim());
            case null -> throw new IllegalArgumentException("値が null です");
            default -> throw new IllegalArgumentException("数値にできない型: " + o.getClass().getName());
        };
    }

    public static void main(String[] args) {
        Map<String, Object> row = Map.of("id", 100, "total", 2500000000L, "rate", 0.9, "qty", "12");

        System.out.println(toLong(row.get("id")));      // Integer
        System.out.println(toLong(row.get("total")));   // Long
        System.out.println(toLong(row.get("rate")));    // Double(小数点以下は切り捨て)
        System.out.println(toLong(row.get("qty")));     // String

        try {
            toLong(List.of());
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
    }
}
100
2500000000
0
12
数値にできない型: java.util.ImmutableCollections$ListN
欲しい型 書き方 補足
long ((Number) o).longValue() 小数は切り捨て(0.9 は 0)
int ((Number) o).intValue() int の範囲を超える値は上位のビットが落ちる
int(範囲も確かめる) Math.toIntExact(n.longValue()) 収まらないときは ArithmeticException
double ((Number) o).doubleValue()
文字列で入っている数値 Long.parseLong(s.trim()) 数字でないときは NumberFormatException

キャストで書ける変換と書けない変換

public class CastBasics {
    public static void main(String[] args) {
        Object nothing = null;
        String s = (String) nothing;                   // null はどの型にもキャストできる
        System.out.println(s);
        System.out.println(nothing instanceof String); // instanceof は null に false を返す

        Object boxed = 100;                            // Integer
        long a = (Integer) boxed;                      // Integer → int → long は自動で広がる
        long b = (int) boxed;                          // Object → int(アンボクシング)も書ける
        Long c = ((Integer) boxed).longValue();        // Long が欲しいときはメソッドで変換
        System.out.println(a + " " + b + " " + c);

        try {
            Integer n = (Integer) (Object) "123";
            System.out.println(n);
        } catch (ClassCastException e) {
            System.out.println("getMessage: " + e.getMessage());
        }
        System.out.println(Integer.parseInt("123") + 1); // 文字列 → 数値は parse で変換
        System.out.println(String.valueOf(123).length()); // 数値 → 文字列は valueOf
    }
}
null
false
100 100 100
getMessage: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
124
3

直し方3:配列は型つきの toArray で作る

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

public class ArrayFix {
    public static void main(String[] args) {
        List<String> names = List.of("sato", "suzuki");

        String[] a1 = names.toArray(new String[0]);      // 型つきの配列を渡す
        String[] a2 = names.toArray(String[]::new);      // Java 11以降
        System.out.println(Arrays.toString(a1) + " " + Arrays.toString(a2));

        // すでに Object[] になっているものは、要素を写して作り直す
        Object[] objs = names.toArray();
        String[] a3 = Arrays.copyOf(objs, objs.length, String[].class);
        System.out.println(Arrays.toString(a3));

        // 中身が本当に String[] なら、Object[] からのキャストは通る
        Object[] really = new String[] {"a", "b"};
        String[] ok = (String[]) really;
        System.out.println(ok.length);

        // 配列のクラス名の読み方
        System.out.println(new String[0].getClass().getName());
        System.out.println(new Object[0].getClass().getName());
        System.out.println(new int[0].getClass().getName());
        System.out.println(new long[0].getClass().getName());
        System.out.println(new double[0].getClass().getName());
        System.out.println(new byte[0].getClass().getName());
        System.out.println(new int[0][0].getClass().getName());
    }
}
[sato, suzuki] [sato, suzuki]
[sato, suzuki]
2
[Ljava.lang.String;
[Ljava.lang.Object;
[I
[J
[D
[B
[[I
  • リストから配列にするときは toArray(new String[0])。引数の配列の型に合わせた配列が返ります
  • Java 11 以降は toArray(String[]::new) とも書けます
  • すでに Object[] として受け取ってしまった場合は、Arrays.copyOf(objs, objs.length, String[].class) で作り直します
  • 4つ目の例のように、作ったときの型が String 配列であれば、Object 配列の変数に入れた後でも String 配列へキャストできます。キャストできるかどうかは、変数の型ではなく作られたときの型で決まります

直し方4:ジェネリクスの型引数を付ける

raw型をやめて型引数を付けると、違う型を入れた時点でコンパイルエラーになり、実行時の ClassCastException が起こらなくなります。

たとえば List<String> names に names.add(123) と書くと、javac が error: incompatible types: int cannot be converted to String と知らせてくれます。

外から受け取ったリストなど、自分では宣言を直せないものは、確かめながら詰め替えます。

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

public class RawFix {
    // 型のわからないリストを受け取り、String だけを取り出す
    static List<String> onlyStrings(List<?> source) {
        List<String> result = new ArrayList<>();
        for (Object o : source) {
            if (o instanceof String s) {
                result.add(s);
            } else {
                System.out.println("String 以外を検出: " + o + " (" + o.getClass().getName() + ")");
            }
        }
        return result;
    }

    public static void main(String[] args) {
        // 直し方1:宣言に型引数を付ける。これで raw.add(123) はコンパイルエラーになる
        List<String> names = new ArrayList<>();
        names.add("apple");

        // 直し方2:外から来た型のわからないリストは、確かめながら詰め替える
        List<Object> mixed = new ArrayList<>();
        mixed.add("apple");
        mixed.add(123);
        System.out.println(onlyStrings(mixed));
    }
}
String 以外を検出: 123 (java.lang.Integer)
[apple]

Class.cast と checkedList(型を変数で持つとき・入れた場所を探すとき)

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

public class CheckedDemo {
    static <T> T castOrNull(Object o, Class<T> type) {
        return type.isInstance(o) ? type.cast(o) : null;
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    public static void main(String[] args) {
        // Class.cast / isInstance:型を変数で受け取るときのキャスト
        System.out.println(castOrNull("abc", String.class));
        System.out.println(castOrNull(123, String.class));
        try {
            String.class.cast(123);
        } catch (ClassCastException e) {
            System.out.println(e.getMessage());
        }

        // Collections.checkedList:入れた瞬間に例外にする
        List<String> names = Collections.checkedList(new ArrayList<>(), String.class);
        List raw = names;
        raw.add("apple");
        raw.add(123);      // 取り出すときではなく、この行で止まる
    }
}
abc
null
Cannot cast java.lang.Integer to java.lang.String
Exception in thread "main" java.lang.ClassCastException: Attempt to insert class java.lang.Integer element into collection with element type class java.lang.String
    at java.base/java.util.Collections$CheckedCollection.typeCheck(Collections.java:3356)
    at java.base/java.util.Collections$CheckedCollection.add(Collections.java:3404)
    at CheckedDemo.main(CheckedDemo.java:25)
  • type.isInstance(o) と type.cast(o) は、instanceof とキャストを「型を引数で受け取る形」にしたものです。パターン6のような「型引数を戻り値にしたメソッド」は、Class<T> を引数に足してこの形にすると、メソッドの中で型を確かめられます
  • Collections.checkedList で包んだリストは、違う型を入れた瞬間に ClassCastException を出します。スタックトレースが「入れた行」(ここでは25行目)を指すので、どこで違う型が混ざったのかを探すときに役立ちます。checkedMap、checkedSet もあります

コンパイル時の unchecked 警告との対応

ジェネリクスが関わる ClassCastException は、ほとんどの場合、コンパイル時に警告として先に知らされています。javac -Xlint:unchecked を付けると詳細が表示されます。パターン4の RawList.java では、7・8行目の add に unchecked call、10行目の代入に unchecked conversion の警告が出ます。パターン6の GenericReturn.java では次の警告が出ます。

GenericReturn.java:8: warning: [unchecked] unchecked cast
        return (T) store.get(key);     // 実行時には何も確かめないキャスト
                            ^
  required: T
  found:    Object
  where T is a type-variable:
    T extends Object declared in method <T>get(String)
1 warning

警告と、実行時に起こることの対応です。

警告(javac の表示) 出る書き方 実行時に起こること 直し方
[rawtypes] found raw type: List List raw = new ArrayList(); これだけでは何も起こらない List<String> のように型引数を付ける
[unchecked] unchecked call to add(E) as a member of the raw type List raw型の変数に add 違う型が入る 同上
[unchecked] unchecked conversion raw型を List<String> に代入 取り出した行で ClassCastException 詰め替える(RawFix.java)
[unchecked] unchecked cast (T) obj や (List<String>) obj キャストの行では確かめず、使う行で ClassCastException Class.cast()、または要素ごとに確かめる

[rawtypes] の警告は -Xlint:rawtypes を付けると表示されます。-Xlint:unchecked,rawtypes と並べて指定できます。

@SuppressWarnings("unchecked") は、型が合っていることを自分で確かめた箇所にだけ、できるだけ狭い範囲(変数宣言や小さなメソッド)に付けます。警告を消しても実行時の動きは変わらないためです。

原因を探す手順

  1. エラー文の A(実際の型)と B(キャスト先)を読む。早見表で当てはまる行を探す
  2. スタックトレースで自分のコードの行を見つける。一番上がライブラリの行なら、下へたどって最初に出てくる自分のクラスの行を見る
  3. その行にキャストが見当たらなければ、ジェネリクスを疑う。拡張 for 文、list.get() の代入、型引数を戻り値にしたメソッドの受け取りが候補
  4. 「入れた場所」を探す。例外の行は取り出した場所なので、同じリストやマップに値を入れている箇所を探す。見つけにくいときは Collections.checkedList で包む
  5. A と B が同じ名前なら、かっこの中のローダー名を比べる

よくある質問

Q. ClassCastException は try-catch で受ければよいですか? A. 受けることはできますが、先に instanceof で確かめる書き方のほうが、意図が読み取りやすく、想定外の型が来たときの処理も書きやすくなります。catch (ClassCastException e) は、手を入れられないライブラリの呼び出しを囲む場合などに限って使うとよいでしょう。

Q. Java 8 や 11 ではどう書きますか? A. パターンマッチングは Java 16 以降の機能なので、OldStyle.java の lengthBefore のように instanceof で確かめた次の行でキャストします。数値を Number 経由で変換する方法、toArray(new String[0])、ジェネリクスの型引数を付ける方法は、そのまま使えます。

まとめ

  • エラー文は「class A cannot be cast to class B」。A が実際の型、B がキャスト先。この組み合わせで原因がわかる
  • Integer と Long、Cat と Dog のような兄弟どうしはキャストできない。数値は Number で受けて longValue() などで取り出す
  • [Ljava.lang.Object; は Object 配列。リストから配列にするときは toArray(new String[0])
  • キャストを書いていない行で出たらジェネリクス。raw型や unchecked 警告のある箇所を直し、型引数を付ける
  • 確かめてからキャストするなら instanceof のパターンマッチング(Java 16以降)。型が3種類以上なら switch(Java 21以降)
  • 同じ名前どうしのエラーは、かっこの中のクラスローダー名を見比べる
  • コンパイル時の「uses unchecked or unsafe operations」は事前の合図。-Xlint:unchecked で場所を確認する

参考・出典

  • Java SE 21 API ドキュメント「ClassCastException」 https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ClassCastException.html
  • JEP 394: Pattern Matching for instanceof https://openjdk.org/jeps/394
  • JEP 441: Pattern Matching for switch https://openjdk.org/jeps/441

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java ClassCastExceptionの原因と対処|エラー文別の直し方」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?