我们知道在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洪爵】