我们知道在Java中类全限定名由两部分组成,包名和类名,当然网上也有说法是由三部分组成,包名、子包名以及类名,这里我把包相关的统称为包名。
比如说在某个Java项目中com.knight包下有一个类A,那么这个类A的类全限定名就为:com.knight.A。我们如果在相同包路径有相同的类名,往往编译是通过不了的。
那么是否有可能在同一个Java项目中加载类全限定名完全相同但实现上不同的两个类?如果不可以,是否就代表代表类的唯一路径就是类全限定名?如果可以,那类的全限定名在java中真的唯一标识了一个类吗?
以下内容涉及到的知识点:Java的反射机制和双亲委派模型。如果不太了解的同学,建议先了解后再来看本篇文章哦。
洪爵准备先进行实操,把最终的结果先落地,然后再根据结果来讨论。
首先我们创建一个Java项目,洪爵命名为Main,然后创建包路径com.knight,在该包路径下创建一个A类,在这个A类里,我们创建一个public方法,返回一个String,方法名为getVersion()。
项目树状图:
A.java代码:
public class A {
public String getVersion() {
return "1.0";
}
}
我们运行javac编译A.java,会在com.knight包下生成A.class文件。
javac ./src/com/knight/A.java
然后我们把A.class文件移到项目根目录下(连同包名文件夹一起)。
然后修改A.java的代码,让getVersion的方法返回值为"2.0"。
现在洪爵想创建一个Main.java文件,在这个Java文件里,洪爵会尝试导入这两个类全限定名相同的A类。首先如果洪爵什么都不做,直接去import这两个类,无疑是不行的,大家肯定都尝试过,那这里的解法就需要提到双亲委派模型了。
我们知道除了应用类加载器、拓展类加载器和启动类加载器外,还有一种自定义类加载器,洪爵尝试使用自定义类加载器看是否能把这两个A类都加载进来。
class MyClassLoader extends ClassLoader {
private final String classPath;
// 自定义前缀
private static final String PREFIX = "prefix.";
public static String getPrefix() {
return PREFIX;
}
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
// 读取文件
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fileInputStream = new FileInputStream(classPath + "/" + name + ".class");
int len = fileInputStream.available();
byte[] data = new byte[len];
fileInputStream.read(data);
fileInputStream.close();
return data;
}
@Override
protected Class<?> findClass(String name) {
try {
name = name.substring(PREFIX.length());
// 查找指定名称的类文件
byte[] data = loadByte(name);
// 将字节数组形式的类文件数据转换为一个Class对象
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 检查该类是否已经被加载过 如果已加载则返回对应的Class对象
Class<?> c = findLoadedClass(name);
if (c == null) {
// 如果没有加载过 先让父类进行加载
if (!name.startsWith(PREFIX)) {
c = super.loadClass(name, resolve);
} else {
// 父类不加载 则自己加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
然后我们尝试开始加载这两个A类,本项目中的A.java自然不必多说,正常new一个就行,另外一个A.class我们需要使用反射去生成对象,并调用getVersion方法:
public class Main {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
MyClassLoader myClassLoader = new MyClassLoader("./");
Class<?> clazz = myClassLoader.loadClass(MyClassLoader.getPrefix() + "com.knight.A");
System.out.println(clazz);
Method method = clazz.getMethod("getVersion");
System.out.println(method.invoke(clazz.newInstance()));
System.out.println(A.class);
A a2 = new A();
System.out.println(a2.getVersion());
}
}
这是对应的输出:
class com.knight.A
1.0
class com.knight.A
2.0
天!同一个项目中,竟然让洪爵成功加载了两个类全限定名完全相同的A类!
这到底是怎么做到的?让洪爵来稍微解释一下,首先你得知道加载器其中3个比较常用的方法的含义:loadClass、findClass和defineClass。
loadClass方法会检查该类是否已经被加载过,如果已加载则直接返回对应的Class对象。如果没有加载过,则调用父ClassLoader的loadClass方法,如果父ClassLoader也无法加载,则调用findClass方法来实际查找和加载类。findClass用于查找指定名称的类文件。defineClass用于将字节数组形式的类文件数据转换为一个Class对象。
如果自定义类加载器不想违背双亲委派模型,一般只需要重写findClass方法即可,如果想违背双亲委派模型,则还需要重写loadClass方法。虽然我们重写了loadClass方法,但是大体上还是按照双亲委派模型的方式,如果找不到会先去让父类加载,那么我在那里设置了MyClassLoader的父类呢?其实因为MyClassLoader是继承了ClassLoader,而ClassLoader的默认protected构造函数,会设置默认的父类为应用类加载器,源码如下图:
// ClassLoader.java
protected ClassLoader() {
this(checkCreateClassLoader(), null, getSystemClassLoader());
}
洪爵在loadClass有一行比较与众不同的代码,我会判断这个包路径是否是PREFIX开头的,如果是则走自己的加载逻辑,然后在自己的findClass方法中,再把PREFIX去掉,露出了真正的包名,这个时候去做加载。
但是核心问题是,为什么jvm允许两个类全限定名相同的A类被加载进来?我们深扒源码,发现ClassLoader的源码里有一个map,这个map的key是对应的包路径,value是对应的package对象,所以自定义类加载器、应用类加载器等都自己维护了一个包路径到package对象的映射,等同于每个加载器都有自己的命名空间。
// ClassLoader.java
// The packages defined in this class loader. Each package name is
// mapped to its corresponding NamedPackage object.
//
// The value is a Package object if ClassLoader::definePackage,
// Class::getPackage, ClassLoader::getDefinePackage(s) or
// Package::getPackage(s) method is called to define it.
// Otherwise, the value is a NamedPackage object.
private final ConcurrentHashMap<String, NamedPackage> packages = new ConcurrentHashMap<>();
因此在同一个Java项目中可以出现类全限定名相同的类,类全限定名并不能唯一标识一个类。那么在一个Java项目中,怎么唯一标识一个类呢?
除了类全限定名外,还需要加上所使用的类加载器就可以唯一定位、标识一个类了,即类加载器 + 类全限定名。
不知道你看完本篇文章,是否有收获?有需要讨论的地方也可以来找洪爵~
愿每个人都能带着怀疑的态度去阅读文章并探究其中原理。
道阻且长,往事作序,来日为章。
期待我们下一次相遇。
【b站搜Knight洪爵 微信可搜KNIGHT洪爵】