问题
如何判断JVM中类和其他类是不是同一个类?
答案
核心结论
JVM判断两个类是否相同,必须同时满足两个条件:
类相同 = 类的全限定名相同 + 加载它的类加载器(ClassLoader)相同
只有这两个条件都满足,JVM才认为它们是同一个类。
判定标准详解
1. 类的全限定名相同
// 全限定名格式:包名.类名
com.example.User
java.lang.String
org.springframework.context.ApplicationContext
注意:内部类、匿名类有特殊的全限定名格式:
com.example.Outer$Inner // 内部类
com.example.Test$1 // 匿名类
com.example.Test$1LocalClass // 局部类
2. 类加载器(ClassLoader)相同
即使类的全限定名完全相同,由不同类加载器加载的类也是不同的类。
类加载器命名空间
每个类加载器都有自己的命名空间(Namespace),同一个类可以被不同的类加载器加载多次,形成多个不同的Class对象。
ClassLoader loader1 = new CustomClassLoader();
ClassLoader loader2 = new CustomClassLoader();
// 加载同一个类文件
Class<?> class1 = loader1.loadClass("com.example.User");
Class<?> class2 = loader2.loadClass("com.example.User");
// 结果:class1 != class2(不同的类)
System.out.println(class1 == class2); // false
实战验证
示例1:不同类加载器加载同一个类
public class ClassIdentityDemo {
public static void main(String[] args) throws Exception {
// 创建两个自定义类加载器
MyClassLoader loader1 = new MyClassLoader("loader1");
MyClassLoader loader2 = new MyClassLoader("loader2");
// 加载同一个类
Class<?> class1 = loader1.loadClass("com.example.User");
Class<?> class2 = loader2.loadClass("com.example.User");
// 1. 类名相同
System.out.println("类名相同: " + class1.getName().equals(class2.getName()));
// 输出:true
// 2. 但不是同一个类
System.out.println("是同一个类: " + (class1 == class2));
// 输出:false
// 3. 创建实例
Object obj1 = class1.newInstance();
Object obj2 = class2.newInstance();
// 4. instanceof 判断
System.out.println("obj1 instanceof class2: " + class2.isInstance(obj1));
// 输出:false(不同类,无法通过类型检查)
// 5. 类型转换
try {
Object temp = class2.cast(obj1); // ClassCastException
} catch (ClassCastException e) {
System.out.println("类型转换失败: " + e.getMessage());
}
}
}
// 自定义类加载器
class MyClassLoader extends ClassLoader {
private String name;
public MyClassLoader(String name) {
this.name = name;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 破坏双亲委派,自己加载(仅用于演示)
if (name.startsWith("com.example")) {
byte[] classData = loadClassData(name);
return defineClass(name, classData, 0, classData.length);
}
return super.loadClass(name);
}
private byte[] loadClassData(String className) {
// 从文件系统读取 .class 文件
// ...
}
}
示例2:相同类加载器加载的类
public class SameLoaderDemo {
public static void main(String[] args) throws Exception {
ClassLoader loader = ClassLoader.getSystemClassLoader();
// 同一个类加载器多次加载同一个类
Class<?> class1 = loader.loadClass("com.example.User");
Class<?> class2 = loader.loadClass("com.example.User");
// 结果:是同一个类(类加载器会缓存)
System.out.println(class1 == class2); // true
}
}
类的唯一性检查方式
方式1:使用 == 比较(推荐)
Class<?> class1 = ...;
Class<?> class2 = ...;
if (class1 == class2) {
System.out.println("是同一个类");
}
方式2:同时检查类名和类加载器
public boolean isSameClass(Class<?> class1, Class<?> class2) {
return class1.getName().equals(class2.getName()) &&
class1.getClassLoader() == class2.getClassLoader();
}
方式3:instanceof 运算符
Object obj = ...;
Class<?> targetClass = ...;
// 检查对象是否是目标类的实例
if (targetClass.isInstance(obj)) {
System.out.println("obj 是 targetClass 的实例");
}
注意:instanceof会受类加载器影响:
MyClassLoader loader = new MyClassLoader();
Class<?> userClass = loader.loadClass("com.example.User");
Object user = userClass.newInstance();
// 失败:不同类加载器加载的类
if (user instanceof com.example.User) { // 编译通过,运行时为false
// 不会执行
}
类加载器命名空间隔离的应用
1. Web容器的应用隔离(Tomcat)
Bootstrap ClassLoader
↓
System ClassLoader
↓
Common ClassLoader(Tomcat共享类库)
↓
├─ WebApp1 ClassLoader(应用1的类)
└─ WebApp2 ClassLoader(应用2的类)
效果:
// 应用1和应用2可以有同名的类
// app1: com.example.UserService (由 WebApp1 ClassLoader 加载)
// app2: com.example.UserService (由 WebApp2 ClassLoader 加载)
// 两者是完全不同的类,互不影响
2. OSGi的模块化隔离
// 不同Bundle可以使用不同版本的同一个库
Bundle1: com.google.guava:18.0 (ClassLoader1)
Bundle2: com.google.guava:28.0 (ClassLoader2)
// 两个版本的Guava类是不同的类,可以共存
3. 热部署实现原理
// 热部署步骤
public void hotDeploy() {
// 1. 创建新的类加载器
MyClassLoader newLoader = new MyClassLoader();
// 2. 加载新版本的类
Class<?> newClass = newLoader.loadClass("com.example.Service");
// 3. 创建新实例
Object newInstance = newClass.newInstance();
// 4. 替换旧实例
replaceOldInstance(newInstance);
// 5. 卸载旧的类加载器(GC回收旧类)
oldLoader = null;
}
类唯一性的常见陷阱
陷阱1:序列化/反序列化
// 对象序列化后,在不同类加载器环境反序列化
MyClassLoader loader1 = new MyClassLoader();
Class<?> userClass = loader1.loadClass("com.example.User");
Object user = userClass.newInstance();
// 序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(user);
// 使用新的类加载器反序列化
MyClassLoader loader2 = new MyClassLoader();
Thread.currentThread().setContextClassLoader(loader2);
ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(baos.toByteArray()));
Object newUser = ois.readObject();
// 问题:newUser的类型与原始user不兼容
System.out.println(newUser.getClass() == user.getClass()); // false
陷阱2:单例模式失效
// 单例类
public class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
// 不同类加载器会创建不同的单例实例
MyClassLoader loader1 = new MyClassLoader();
MyClassLoader loader2 = new MyClassLoader();
Class<?> singletonClass1 = loader1.loadClass("com.example.Singleton");
Class<?> singletonClass2 = loader2.loadClass("com.example.Singleton");
Object instance1 = singletonClass1.getMethod("getInstance").invoke(null);
Object instance2 = singletonClass2.getMethod("getInstance").invoke(null);
// 两个"单例"实例实际上不同
System.out.println(instance1 == instance2); // false
JVM内部的类标识
JVM内部使用<类加载器实例, 全限定名>作为类的唯一标识:
// JVM内部表示(概念示意)
ClassIdentifier {
ClassLoader classLoader; // 类加载器实例
String className; // 全限定名
}
// 两个类相同的条件
class1.classLoader == class2.classLoader &&
class1.className.equals(class2.className)
面试要点总结
- 判定标准:类的全限定名相同 + 类加载器相同(缺一不可)
- 命名空间隔离:不同类加载器有独立的命名空间,同名类可共存
- 比较方式:使用
==比较Class对象(推荐) - 实际应用:Tomcat应用隔离、OSGi模块化、热部署机制
- 常见陷阱:单例模式失效、instanceof失败、类型转换异常
- 核心原因:类加载器的命名空间隔离是JVM实现模块化和隔离的基础
- 安全机制:防止恶意类冒充核心类,保证系统安全