エラーの内容
java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer
このエラーはランタイムで発生します。コンパイラが警告なしに通過させたコードでよく起きます。キャストを書き、コンパイルも通り、一見問題なさそうに見えます。ところが実際の値が流れてきた瞬間、JVMが大声で異議を唱えます。型の不一致は最初からそこにあったのに、Javaは実行時まで気づかなかっただけです。
なぜこのエラーが起きるのか
Javaはキャストの互換性をコンパイル時ではなく、ランタイムで強制します。以下の3つの状況でよく発生します:
- Raw型 / 未チェックキャスト — ジェネリクスなしの
Listを使い、要素を取り出す際にキャストする場合。 - オブジェクト型の誤った思い込み — メソッドが
Objectを返し、そこに期待する型にキャストする場合。 - デシリアライゼーション / リフレクション — JSON、YAML、データベース、設定ファイルからデータを読み込む際に、型がそのまま保持されていると思い込む場合。
問題の再現
バグの最もシンプルな例:
List list = new ArrayList(); // raw型 — ジェネリクスなし
list.add("hello");
Integer num = (Integer) list.get(0); // ここでClassCastException
コンパイラは「unchecked cast」という警告を出しますが、それでもコンパイルは通ります。ランタイムでは、JVMはIntegerを約束した場所にStringを見つけてクラッシュします。
Object値を持つMapも頻繁に問題を起こします:
Map<String, Object> config = loadConfig();
Integer timeout = (Integer) config.get("timeout"); // timeoutが"30"(String)だと例外が発生
このパターンはSpringの設定読み込みやカスタムプロパティリーダーで至る所に見られます。30と表示される値でも、整数の30ではなく文字列の"30"として格納されていることがあります。
修正方法
修正1 — ジェネリクスを使う
Raw型から適切にパラメータ化されたジェネリクスに切り替えることで、実行前にコンパイラが型の不一致を検出できるようになります。
// 修正前(raw型)
List list = new ArrayList();
list.add("hello");
Integer num = (Integer) list.get(0); // ランタイムクラッシュ
// 修正後(ジェネリック型)
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // キャスト不要、コンパイラが安全性を保証
キャストなし、爆発なし。これが目指す姿です。
修正2 — キャスト前にinstanceofで確認する
Objectの戻り値や外部データを扱う場合は、必ず先に型を確認しましょう:
Object value = config.get("timeout");
if (value instanceof Integer) {
Integer timeout = (Integer) value;
// timeoutを使用
} else if (value instanceof String) {
Integer timeout = Integer.parseInt((String) value);
// timeoutを使用
} else {
throw new IllegalArgumentException("timeoutの型が予期しない型です: " + value.getClass());
}
Java 16以降のパターンマッチングを使うとすっきり書けます:
// Java 16以降のパターンマッチングinstanceof
if (value instanceof Integer timeout) {
System.out.println("タイムアウト: " + timeout);
} else if (value instanceof String s) {
System.out.println("タイムアウト(文字列): " + Integer.parseInt(s));
}
パターンマッチングにより余分なキャスト行が不要になります。ブロック内では変数がすでに正しい型になっています。
修正3 — デシリアライゼーションとMapの型問題を修正する
JacksonとGsonは値によって異なる動作をします。JSONの数値30は、範囲内であればInteger、2,147,483,647を超えるとLong、小数点があればDoubleとしてデシリアライズされます。Map<String, Object>からどの型が返ってくるかは常に予測できません。
// Jackson: raw Mapの代わりに型付きクラスにデシリアライズする
ObjectMapper mapper = new ObjectMapper();
MyConfig config = mapper.readValue(json, MyConfig.class); // 型安全
// Map<String, Object>を使わざるを得ない場合は、Numberを橋渡しとして使う
Object raw = map.get("count");
int count = ((Number) raw).intValue(); // Integer、Long、Doubleを安全に処理
Numberはすべての数値ボックス型の共通スーパータイプです。まずNumberにキャストしてから.intValue()を呼び出すことで、型の曖昧さを完全に回避できます。
修正4 — レガシーなRawコレクションをラップする
古いAPIがRaw型のListやCollectionを返すことがあります。2つの選択肢があります:警告を抑制してキャストするか、防衛的にコピーするかです。
// 選択肢A: 警告を抑制してキャスト(APIの契約に自信がある場合のみ)
@SuppressWarnings("unchecked")
List<String> items = (List<String>) legacyApi.getItems();
// 選択肢B: 型チェックしながらコピー(APIが信頼できない場合はより安全)
List<String> items = new ArrayList<>();
for (Object obj : legacyApi.getItems()) {
if (obj instanceof String) {
items.add((String) obj);
}
// 予期しない型は静かにスキップするかログに記録する
}
選択肢Bは処理が遅くなりますが、例外は発生しません。データソースが外部または信頼できない場合に使いましょう。
スタックトレースの読み方
例外メッセージを読み飛ばさないでください。非常に正確な情報が書かれています:
java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer
at com.example.MyService.processConfig(MyService.java:42)
at com.example.MyService.init(MyService.java:18)
- MyService.javaの42行目 — キャストが失敗した場所です。ここから調査を始めましょう。
- メッセージは実際に存在した型(String)と要求した型(Integer)を教えてくれます。
- その行から遡って、値がコレクションに入った場所やメソッドから返された場所を探しましょう。そこで型が誤ってしまっています。
修正の確認方法
完了と判断する前に3つの確認を行いましょう:
javac -Xlint:unchecked MyClass.javaでコンパイルする — 未チェックキャストの警告がゼロなら問題ありません。- エッジケースの入力でユニットテストを実行する:整数が期待される場所への文字列、null値、混合型コレクションなど。
- バグがデータパイプラインに潜んでいた場合は、モックではなく実際の生データを解析パスに流すテストを書きましょう。
@Test
void testConfigParsing() {
Map<String, Object> config = Map.of("timeout", "30"); // IntegerではなくString
assertDoesNotThrow(() -> MyService.parseTimeout(config));
assertEquals(30, MyService.parseTimeout(config));
}
クイックヒント
- コンパイラ警告を有効にする:
javac -Xlint:allを使うか、IDEで未チェックキャストにフラグを立てるよう設定する。ノイズとして無視してはいけません。それぞれがランタイムクラッシュの予備軍です。 - 選択肢がない場合にのみ
Objectを返す:メソッドのシグネチャがObjectを返す場合、それは設計上の問題を示すシグナルです。ジェネリクスやシールドインターフェースで大抵はすっきり解決できます。 - 橋渡し型として
Numberを使う:数値がInteger、Long、Doubleのどれかわからない場合は、まずNumberにキャストしてから.intValue()を呼び出す。3つのケースすべてに対応できます。 - 混合マップには型付きコンテナを検討する:1つのマップに本当に異なる型の値が必要な場合、Guavaの
ClassToInstanceMapはまさにこのために作られています。各取得箇所ではなく、コンテナレベルで型安全性を強制します。

