(ChildClass) objと書いたら実行時にClassCastExceptionが飛んできて、何が起きたのかわからなかった経験はないでしょうか。
私も最初はアップキャストとダウンキャストの違いを理解しないまま、なんとなくキャストを書いていました。
この記事を読めば、キャストの基本からinstanceofによる安全な変換、ClassCastExceptionの原因と対策までがわかります。
正直、最初は例外メッセージを見ても何が起きているのかさっぱりでした。
Javaのキャストとは?型変換の基本
キャストとは、ある型のデータを別の型として扱えるように変換することです。
Javaのキャストには、プリミティブ型同士の変換と、クラスの継承関係を利用した参照型同士の変換の2種類があります。
プリミティブ型変換と参照型変換は仕組みがまったく違うため、混同すると理解が混乱します。
プリミティブ型キャストと参照型キャストの違い
プリミティブ型のキャストは、int型をdouble型に変換するような、数値の表現形式を変える処理です。
一方、参照型のキャストは、実体(インスタンス)は変えずに「どの型として扱うか」というラベルを変える処理です。
参照型キャストではインスタンスの中身は一切変わらず、コンパイラに対して「この変数をこの型として扱ってよい」と伝えているだけになります。
暗黙的キャストと明示的キャストの違い
int型からlong型への変換のように、情報が失われない変換は自動的に行われます。これを暗黙的キャストと呼びます。
逆に、double型からint型への変換のように情報が失われる可能性がある変換は、(int) 3.14のように明示的にキャストを書く必要があります。
参照型のアップキャストも暗黙的に行われ、ダウンキャストは明示的に書く必要があるという点で、この考え方は共通しています。
プリミティブ型キャストで精度が落ちる具体例
明示的なキャストは、値そのものを壊してしまうことがあります。
doubleからintへの変換では小数点以下がそのまま切り捨てられ、longからintへの変換では扱える桁数を超えてオーバーフローが起きます。
double d = 3.99;
int i = (int) d; // 3(小数点以下が切り捨てられる)
long l = 10000000000L;
int overflow = (int) l; // 桁あふれで想定外の値になる
参照型のダウンキャスト失敗が実行時例外を投げるのに対し、プリミティブ型のキャストは値が壊れても例外を投げません。
エラーにならないぶん、精度が落ちたことに気づきにくいのが実務での落とし穴です。
文字列⇔数値の変換はキャストと呼ばない
「キャスト」と検索する人の中には、文字列と数値の変換方法を探している場合もあります。
しかしString.valueOf()やInteger.parseInt()はキャスト演算子ではなく、メソッド呼び出しによる変換です。
実際、(String) objのようにint型やdouble型の値をStringへキャストしようとすると、コンパイルエラーになります。
型変換全般を「キャスト」と呼んでしまう混同はよくあるつまずきポイントなので、演算子によるキャストとメソッドによる変換は別物だと切り分けておくと理解が整理されます。
アップキャスト|サブクラスからスーパークラスへの変換
アップキャストは、サブクラスのインスタンスをスーパークラスの型として扱う変換です。安全な変換のため、Javaが自動的に行ってくれます。
アップキャストが自動で行われる仕組み
サブクラスは必ずスーパークラスの機能をすべて持っています。
そのため、サブクラスのインスタンスをスーパークラス型の変数に代入しても、情報が失われることはありません。
この安全性が保証されているからこそ、コンパイラは明示的なキャストを要求せず、自動で変換してくれます。
class Animal {}
class Dog extends Animal {
void bark() { System.out.println("わん"); }
}
Dog dog = new Dog();
Animal animal = dog; // 暗黙的にアップキャストされる
アップキャスト後に呼び出せるメソッドの制限
アップキャストした変数からは、スーパークラスで定義されているメソッドしか呼び出せません。
上の例で言えば、animal変数からbark()メソッドを呼ぶことはできません。
インスタンスの実体はDogのままですが、変数の型がAnimalになった時点で扱いが変わります。
コンパイラはAnimalが持つメソッドしか知らない扱いになるためです。
アップキャストを使うと何が便利になるか
アップキャストが真価を発揮するのは、異なるサブクラスのインスタンスをまとめて扱いたいときです。
List<Animal>という1つのリストにDogやCatのインスタンスを格納し、ループで一括処理できます。
List<Animal> animals = new ArrayList<>();
animals.add(new Dog());
animals.add(new Cat());
for (Animal animal : animals) {
animal.makeSound(); // それぞれのサブクラスの実装が呼ばれる
}
この仕組みはポリモーフィズム(多態性)と呼ばれ、呼び出す側はDogかCatかを意識せず、共通の窓口(Animal型)を通して扱えます。
アップキャストは制約ではなく、こうした柔軟な設計を可能にするための手段だと捉えると理解しやすくなります。
Java Silver試験でも、ポリモーフィズムを絡めたキャストの問題は頻出です。
ダウンキャスト|スーパークラスからサブクラスへの変換
ダウンキャストは、スーパークラス型の変数をサブクラス型に戻す変換です。
アップキャストとは逆に、情報が失われるリスクがあるため、明示的にキャストを書く必要があります。
ダウンキャストが必要になる場面
アップキャストで失われたサブクラス固有のメソッドを再び使いたいときに、ダウンキャストが必要になります。
先ほどの例でanimal変数からbark()を呼びたい場合、(Dog) animalのように明示的にキャストすれば、再びbark()を呼び出せるようになります。
ただし、ダウンキャストが頻繁に必要になるコードは、そもそもクラス設計を見直すサインであることが多いです。
ポリモーフィズムを活かした設計にできれば、ダウンキャストなしで済むケースは意外と多くあります。
Animal animal = new Dog();
Dog dog = (Dog) animal; // ダウンキャスト
dog.bark();
ダウンキャストが失敗する条件
ダウンキャストが成功するのは、変数の実体が本当にキャスト先の型である場合だけです。
実体がCatのインスタンスなのに(Dog) animalと書いてしまうと、実行時にClassCastExceptionが発生します。
コンパイル時には型チェックが通ってしまうため、この失敗は実行してみるまで気づけないのが厄介な点です。
instanceofで安全にダウンキャストする方法
ダウンキャストの失敗を防ぐには、キャストする前に実体の型を確認するinstanceof演算子を使います。
NG例
Dog dog = (Dog) animal;
instanceofで確認せずに直接キャストすると、実体が異なる型のときにClassCastExceptionが発生する。
OK例
if (animal instanceof Dog) { Dog dog = (Dog) animal; }
instanceofで型を確認してからキャストするため、ClassCastExceptionが発生しない。
instanceof演算子による事前チェック
if (animal instanceof Dog)のように書けば、animalの実体がDogかどうかを事前に確認できます。
この条件が true のときだけダウンキャストすれば、ClassCastExceptionを未然に防げます。
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.bark();
}
Java 16以降のパターンマッチングinstanceof
Java 16以降では、instanceofのチェックとキャストを1行にまとめられるパターンマッチングが使えます。
if (animal instanceof Dog d)と書くだけで、条件がtrueのブロック内では自動的にdという変数がDog型として使えるようになります。
従来のようにinstanceofでチェックしてから改めてキャストする2段階の記述が不要になり、コードが簡潔になります。
if (animal instanceof Dog d) {
d.bark(); // キャスト済みのdをそのまま使える
}
ClassCastExceptionの原因と対策
ClassCastExceptionは実務でも頻出のバグの1つです。原因を型ごとに把握しておくと、初動の切り分けが速くなります。
よくある発生パターン3つ
1つ目は、兄弟クラス間の誤ったキャストです。
DogとCatが同じAnimalを継承していても、CatのインスタンスをDogにキャストすることはできません。
2つ目は、リストの要素を誤った型で受け取るケースです。
List<Object>から取り出した要素を安易に特定の型へキャストすると、実体が違う型だった場合に例外が発生します。
3つ目は、外部から受け取ったデータの型を確認せずにキャストするケースです。
JSONのデシリアライズ結果など、実行時まで型が確定しないデータで起こりやすいバグです。
instanceof確認を省略してしまう最大の理由は、開発中は型が一致していて問題が表面化しないためです。
テストデータが偏っていると発見が遅れ、本番環境で想定外のデータが来て初めて例外に気づくことも珍しくありません。
ジェネリクスとの組み合わせで起きる注意点
ジェネリクスを使ったコレクションは、コンパイル時に型消去が行われるため、実行時には型情報が残っていません。
そのため、List<Dog>とList<Cat>を混同するようなミスは、コンパイラの警告を無視して無理にキャストした場合にだけ発生します。
ジェネリクスの型パラメータを安易にキャストで誤魔化すコードを見かけたら、設計自体を見直すサインだと捉えたほうがよいです。
よくある質問
Q. アップキャストとダウンキャストはどちらが危険ですか?
A. ダウンキャストの方が危険です。実体と異なる型へダウンキャストするとClassCastExceptionが発生するため、instanceofで事前確認するのが安全です。
Q. instanceofを使えばキャストは絶対に失敗しませんか?
A. はい。instanceofで型を確認してからキャストすれば、その時点での型不一致によるClassCastExceptionは防げます。
Q. プリミティブ型のキャストでClassCastExceptionは起きますか?
A. いいえ。ClassCastExceptionは参照型のキャストで発生する例外です。プリミティブ型の変換で情報が失われる場合は、例外ではなく値の精度が落ちる形で処理されます。
Q. Java 16のパターンマッチングinstanceofは古いJavaでも使えますか?
A. いいえ。Java 16以降でのみ使用できます。それより前のバージョンでは、従来通りinstanceofとキャストを分けて書く必要があります。
Q. 兄弟クラス同士のキャストはなぜできないのですか?
A. 継承関係にないためです。DogとCatが同じAnimalを継承していても、DogとCatの間には親子関係がないため、互いにキャストするとClassCastExceptionが発生します。
Q. ジェネリクスのキャストで警告が出るのはなぜですか?
A. コンパイル時の型消去により、実行時にはジェネリクスの型パラメータの情報が残らないためです。
List<Dog>をList<Cat>に無理にキャストすると、コンパイラは未検査キャストとして警告を出します。
まとめ|キャストはinstanceofで安全性を確保する
Javaのキャストのポイントをまとめると、次の3点に集約されます。
- アップキャストは自動・安全、ダウンキャストは明示的でリスクを伴う
- ダウンキャストの前にはinstanceofで実体の型を確認する
- Java 16以降ならパターンマッチングinstanceofでコードを簡潔にできる
ClassCastExceptionは実行してみるまで気づきにくいバグですが、instanceofで事前確認する習慣さえ身につければ、ほとんどのケースは未然に防げます。
キャストを書くときは、まず「この変換は本当に安全か」を一度立ち止まって考えてみてください。