"类加载器决定了类的身份"是个抽象结论。真实用两个独立的 URLClassLoader 加载同一份字节码,把这句抽象的话变成一个真实能复现的 ClassCastException。
上一篇:int 溢出真实静默环绕:Integer.MAX_VALUE + 1 真实变成负数,没有任何报错
JVM 判断"两个类是不是同一个类",靠的不只是全限定名,还要看是谁加载的它。
准备一个最简单的 Foo 类,编译成 Foo.class 放进一个目录;然后创建两个独立的 URLClassLoader 实例,都指向这同一个目录,父加载器都设成 null(不委派给系统类加载器),让每个实例都真实自己去定义这个类,而不是从别处拿现成的:
ClassLoaderDemo.java(核心逻辑) URL classesDir = new File("classes").toURI().toURL(); URLClassLoader loader1 = new URLClassLoader(new URL[]{classesDir}, null); URLClassLoader loader2 = new URLClassLoader(new URL[]{classesDir}, null); Class<?> foo1 = loader1.loadClass("Foo"); Class<?> foo2 = loader2.loadClass("Foo");
先确认两个类加载器确实是独立的,再看它们各自加载出来的 Foo 是不是同一个 Class 对象。
foo1 的类加载器 = java.net.URLClassLoader@251a69d7 foo2 的类加载器 = java.net.URLClassLoader@7344699f foo1 == foo2 (同一个 Class 对象吗) = false foo1.equals(foo2) = false obj1 instanceof foo1 的类型 = true obj1 instanceof foo2 的类型 = false
字节码内容完全相同、全限定名也完全相同(都叫 Foo),但真实测出来这是两个不同的 Class 对象——obj1 是 foo1 类型的实例,却真实不是 foo2 类型的实例。这直接验证了 JVM 判定类身份的真实规则:一个类在 JVM 里的唯一标识是(全限定类名,加载它的 ClassLoader 实例)这个二元组,不是单看类名。
反射调用不受这个限制(反射靠的是 Method 对象本身,不做编译期类型检查),但真实的强制类型转换会。
反射调用 obj1.hello() = hello from Foo
真实抛出 ClassCastException: class Foo cannot be cast to class Foo (Foo is in unnamed module of loader java.net.URLClassLoader @251a69d7; Foo is in unnamed module of loader 'app')
这条真实报错信息很有意思:字面上写的是"class Foo cannot be cast to class Foo"——两边名字一模一样,乍看像是同义反复。真正的区分信息藏在括号里:一个 Foo 来自 URLClassLoader@251a69d7,另一个来自应用类加载器 'app'。这也是这类问题在真实项目里最容易让人摸不着头脑的地方——报错信息表面上"类型对得上",但 JVM 坚持认为它们不是同一个类型。
用 identityHashCode 给每个 ClassLoader、Class、实例对象编号,串成动画:从加载到 instanceof 检查,再到最后真实抛出的 ClassCastException。