1. 概述

在这篇简短的教程中,我们将聚焦于 ClassCastException,这是 Java 中一种常见的异常

ClassCastException 是一个 非受检异常(unchecked exception),它表示程序试图将一个对象引用强制转换为它并非子类型的类型

接下来我们将探讨一些导致该异常出现的典型场景,并给出避免这些问题的建议。

2. 显式类型转换

为了便于演示,我们先定义以下几个类:

public interface Animal {
    String getName();
}
public class Mammal implements Animal {
    @Override
    public String getName() {
        return "Mammal";
    }
}
public class Amphibian implements Animal {
    @Override
    public String getName() {
        return "Amphibian";
    }
}
public class Frog extends Amphibian {
    @Override
    public String getName() {
        return super.getName() + ": Frog";
    }
}

2.1. 类型转换中的类

最常见的 ClassCastException 场景,就是显式地将一个对象转换为不兼容的类型

例如,尝试将 Frog 转换为 Mammal

Frog frog = new Frog();
Mammal mammal = (Mammal) frog;

我们可能会认为这会抛出 ClassCastException,但实际上编译器会直接报错:

incompatible types: Frog cannot be converted to Mammal

但是,如果我们使用它们的共同父类进行操作:

Animal animal = new Frog();
Mammal mammal = (Mammal) animal;

此时,编译通过,但在运行时会抛出 ClassCastException:

Exception in thread "main" java.lang.ClassCastException: class Frog cannot be cast to class Mammal (Frog and Mammal are in unnamed module of loader 'app') 
at Main.main(Main.java:9)

这是因为 Frog 并不是 Mammal 的子类,因此无法向下转型。编译器无法判断 Animal 引用实际指向的是哪个子类,所以不会报错。

⚠️ 有趣的是,只有在明确不兼容的类之间才会出现编译错误;而接口类型不会触发编译错误,因为 Java 支持多接口继承。举个例子:

Animal animal = new Frog();
Serializable serial = (Serializable) animal;

虽然 Frog 没有实现 Serializable 接口,但由于是接口类型,编译器允许这种转换,最终运行时报错:

Exception in thread "main" java.lang.ClassCastException: class Frog cannot be cast to class java.io.Serializable (Frog is in unnamed module of loader 'app'; java.io.Serializable is in module java.base of loader 'bootstrap') 
at Main.main(Main.java:11)

2.2. 数组类型转换

类的转换我们已经看过了,现在来看数组。数组的类型转换规则和类一致,但容易被自动装箱或类型提升搞混。

来看下面这个例子:

Object primitives = new int[1];
Integer[] integers = (Integer[]) primitives;

第二行会抛出 ClassCastException,因为基本类型数组不支持自动装箱。

再来看类型提升的情况:

Object primitives = new int[1];
long[] longs = (long[]) primitives;

同样抛出 ClassCastException,因为整个数组不能做类型提升。

2.3. 安全的类型转换

当我们进行显式类型转换时,强烈建议在转换前使用 instanceof 判断类型是否兼容。

示例代码如下:

Mammal mammal;
if (animal instanceof Mammal) {
    mammal = (Mammal) animal;
} else {
    // 处理异常情况
}

✅ 这样可以有效避免运行时的 ClassCastException。

3. 堆污染(Heap Pollution)

根据 Java 语言规范“堆污染只会在程序使用了原始类型(raw type)并引发编译时未检查警告的操作时发生。”

我们定义一个泛型类用于实验:

public static class Box<T> {
    private T content;

    public T getContent() {
        return content;
    }

    public void setContent(T content) {
        this.content = content;
    }
}

然后尝试制造堆污染:

Box<Long> originalBox = new Box<>();
Box raw = originalBox;
raw.setContent(2.5);
Box<Long> bound = (Box<Long>) raw;
Long content = bound.getContent();

最后一行会抛出 ClassCastException,因为 Double 无法转换为 Long

4. 泛型类型擦除

在使用 Java 泛型时,必须警惕 类型擦除(Type Erasure),它也可能导致 ClassCastException。

考虑以下泛型方法:

public static <T> T convertInstanceOfObject(Object o) {
    try {
        return (T) o;
    } catch (ClassCastException e) {
        return null;
    }
}

调用方式如下:

String shouldBeNull = convertInstanceOfObject(123);

初看之下,我们可能期望返回 null,但实际上由于类型擦除,T 在运行时被替换为 Object,所以 (T) o 实际上是 (Object) o,并不会抛出异常。

但当我们试图将 Integer 赋值给 String 变量时,编译器会在后续插入强制转换,从而抛出 ClassCastException。

5. 总结

本文通过多个常见场景展示了 ClassCastException 的成因。

无论是显式还是隐式的类型转换,只有当目标类型与引用的实际类型一致或为其子类时,才不会抛出 ClassCastException

文中使用的代码示例可在 GitHub 项目 中找到。


原始标题:Explanation of ClassCastException in Java