Java中ClassCastException的解释

2023/05/30

1. 概述

在这个简短的教程中,我们将重点介绍ClassCastException,这是一种常见的Java异常

ClassCastException是一个非受检的异常,表示代码已尝试将引用强制转换为不是子类型的类型

让我们看一下导致抛出此异常的一些场景以及我们如何避免它们。

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)

向下转换为Mammal与Frog引用不兼容,因为Frog不是Mammal的子类型。在这种情况下,编译器无法帮助我们,因为Animal变量可能包含兼容类型的引用。

有趣的是,编译错误仅在我们尝试强制转换为明确不兼容的类时发生。对于接口来说,情况并非如此,因为Java支持接口多继承,而类只支持单继承。因此,编译器无法确定引用类型是否实现了特定接口。让我们举例说明:

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

我们在第二行得到一个ClassCastException而不是编译错误:

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 {
    // handle exceptional case
}

3. 堆污染

根据Java规范:“堆污染只有在程序执行某些涉及原始类型的操作时才会发生,这会导致编译时未经检查的警告”。

对于我们的示例,让我们考虑以下泛型类:

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中使用泛型时,我们必须警惕类型擦除,这在某些情况下也会导致ClassCastException。

让我们考虑以下泛型方法:

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

现在我们调用它:

String shouldBeNull = convertInstanceOfObject(123);

乍一看,我们可能合理地预期从catch块返回空引用。但是,在运行时,由于类型擦除,参数被强制转换为Object而不是String。因此,编译器面临着将Integer分配给String的任务,这会抛出ClassCastException。

5. 总结

在本文中,我们研究了一系列不适当转换的常见场景。

无论是隐式还是显式转换,将Java引用强制转换为另一种类型都可能导致ClassCastException,除非目标类型与实际类型相同或为实际类型的后代。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章