Class类是集合了所有类的属性、行为的抽象,描述类的修饰、类的构造器、类的字段以及类的方法等抽象,这里的类是指广泛的类,包括了接口、注解、数组等。简单的来说,它涵盖了所有类的共性,所以研究它时,应当从所有类的共性出发,来探讨其中的内容。而ClassLoader类则是负责了类的加载这一块,负责将Class类对象从jvm中加载出来。
虽然Class类是所有类的抽象,但是它依旧是与其它类一样具有共同性,创建一个自定义Test类,经过编译会产生一个Test.class字节码文件,该字节码文件保存着Test类的抽象,也就是保存着Test类的各种类型与方法等,jvm会通过它来创建Test类对应的Class类对象,jvm再通过该Class类对象来创建Test实例。不管创建多少个实例,字节码文件始终只有一个,且所有的实例都依赖于该Class对象。这也是所有类的hashcode都有唯一性的一个原因。
当我们new一个新对象或者引用静态成员变量时,JVM中的类加载器子系统会将对应Class类对象加载到JVM中,这个过程也就是通过类加载器将字节码文件转化为Class类对象,然后JVM再根据这个类型信息相关的Class类对象创建我们需要实例对象或者提供静态变量的引用值。但在实际中中,Class类对象是不能被外部创建的,它只能从jvm中加载其对象,因为只有私有构造方法,必须通过forName与getClass方法来获取,所以不会存在Class.class文件,但会存在其它类的字节码文件:
private Class(ClassLoader loader) { classLoader = loader; }
那么如何来加载一个Class类对象或者获取一个Class类对象的引用呢?在其内部提供了forName方法:
@CallerSensitive public static Class<?> forName(String className) throws ClassNotFoundException { Class<?> caller = Reflection.getCallerClass(); return forName0(className, true, ClassLoader.getClassLoader(caller), caller); }
该方法通过Reflection反射类的getCallerClass本地方法来获取当前类的Class对象的引用,但这个Class类对象是只有标明是属于哪一个类的,但里面并没有加载进对应类的信息。后调用forName0本地方法来对Class类对象进行初始化加载,加载对应类的静态代码块与静态成员变量的,也就是说getCallerClass获取其Class对象时并没有对类进行初始化,先用另外一个forName方法测试,这个方法可以手动开关forName0方法的初始化,如下代码所示:
class Kt{ static{ System.out.println("加载了3。。。。"); } { System.out.println("加载了。。。。"); } public Kt(){ System.out.println("加载了2。。。。"); } public void gg() throws IllegalAccessException, InstantiationException { Reflection.getCallerClass(1); } } public class Test { public static void main(String args[]) throws InstantiationException, IllegalAccessException, ClassNotFoundException { System.out.println("未开启初始化。。。。。。"); Class.forName("test.Kt",false,Kt.class.getClassLoader()); System.out.println("开启初始化。。。。。。"); System.out.println("开启初始化。。。。。。"); Class.forName("test.Kt",true,Kt.class.getClassLoader()); } }
这里用getCallerClass(int )来代替getCallerClass(),两者效果是一样的,证明了getCallerClass只是获取其对应Class类对象,但是并没有对类进行初始化加载加载。对forName0方法的解释,官方的文档写到:返回与具有给定字符串名称的类或接口关联的Class对象,使用给定的类加载器加载。给定类或接口的完全限定名称(以getName返回的相同格式)此方法尝试查找,加载和链接类或接口。指定的类加载器用于加载类或接口。如果参数加载器为null,则通过根类加载器加载该类。 仅当initialize参数为true且之前尚未初始化时,才会初始化该类。
private static native Class<?> forName0(String name, boolean initialize, ClassLoader loader, Class<?> caller) throws ClassNotFoundException;
另外一个forName方法可以让调用者手动加载静态属性与方法,也就是上面验证代码用到的,里面加入了java的安全管理器,如果是系统级别的加载器则不需要验证权限,否则就需要验证加载器是否有加载该字节码文件的权限:
/** * @param name 类的完全限定名 * @param initialize 是否加载 * @param loader 类加载器 * @return 一个Class对象 * @throws ClassNotFoundException */ @CallerSensitive public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException { Class<?> caller = null; SecurityManager sm = System.getSecurityManager(); if (sm != null) { //只有安全管理员才需要反射调用来获取调用者类的存在。否则,避免调用来产生额外的开销 caller = Reflection.getCallerClass(); if (sun.misc.VM.isSystemDomainLoader(loader)) { ClassLoader ccl = ClassLoader.getClassLoader(caller); if (!sun.misc.VM.isSystemDomainLoader(ccl)) { sm.checkPermission( SecurityConstants.GET_CLASSLOADER_PERMISSION); } } } return forName0(name, initialize, loader, caller); }
其实可以从上文看出java对类的加载策略,加载方式是动态加载的,当需要创建类的实例时,这时候创建一个对应Class类对象,加载进静态代码块、静态属性与静态方法,然后再加载进对应类的其它方法。也就是jvm在第一次创建使用该类时加载的,将类注册进jvm,不仅是Class类,ClassLoader类也存在这个方法,Object类、Class类、ClassLoader类是直接与jvm打交道的,它们的对象都由jvm产生与管理:
private static native void registerNatives(); static { registerNatives(); }
当调用类中静态的成员变量的引用时或者创建该类实例时(包含静态成员变量与方法,其中构造方法也属于静态成员方法,所以这两者可以统称静态成员变量与方法),jvm会调用类加载器加载字节码文件来创建对应的Class类对象,再通过Class类对象里面类的信息来创建对应的实例。同时为防止字节码文件被破坏而导致的问题,会加入了对字节码文件的检测。
在java中还提供了另外的方式来获取Class类对象,这种方式就是类名加.class,如下代码:
Cat cat=new Cat(); System.out.println(Cat.class==cat.getClass());//true System.out.println(int.class);//打印int
要注意的是,非引用类型以及void类型也会创建一个Class类对象。 不仅在Class类里面提供了类的对象获取,在ClassLoader类加载器类里面也存在着Class类对象的获取,也就是loadClass方法,该方法是一个同步方法,会调用父加载器来加载,该方法通过:
private native final Class<?> findLoadedClass0(String name);
findLoadedClass0本地方法来查找jvm中指定完全限定名的一个已经加载完的Class类对象:
private final ClassLoader parent; protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先,检测是否已经加载 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { //父加载器不为空则调用父加载器的loadClass c = parent.loadClass(name, false); } else { //父加载器为空则调用Bootstrap Classloader c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); //父加载器没有找到,则调用findclass,抛出未找到异常 //该方法就是用来抛异常的 c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { //调用resolveClass() resolveClass(c); } return c; } }
我们可以用以下代码来判断forName方法与loadClass方法有什么不同:
class Kt{ static{ System.out.println("加载了3。。。。"); } { System.out.println("加载了。。。。"); } public Kt(){ System.out.println("加载了2。。。。"); } } public class Test { public static void main(String args[]) throws InstantiationException, IllegalAccessException, ClassNotFoundException { System.out.println("未开启初始化的forName方法。。。。。。"); Class.forName("test.Kt",false,Kt.class.getClassLoader()); System.out.println("开启初始化的forName方法。。。。。。"); Class.forName("test.Kt",true,Kt.class.getClassLoader()); System.out.println("loadClass方法。。。。。。"); ClassLoader.getSystemClassLoader().loadClass("test.Kt"); } }
从结果来看,loadClass方法与未开启初始化操作的forName方法一样只会得到一个含有完全限定名的Class类对象,也就是说loadClass并没有加载其静态方法、静态代码块以及静态属性。
上述讲解了怎么获得一个Class类对象。下面将要讲解怎么获得类的其它组成部分,如获得实现接口的名字、构造体、方法、属性、包名、父类/接口等等。在java中,每个类的属性都定义了一个的特殊的类来描述,比如Method类就是用来描述方法的,这些类统一的定义在java.lang.reflect反射包里,这些都会在讲解反射的时候讲解到。在讨论之前,先来看看java中的AbstractRepository抽象类,这个类是用来存贮信息的,是一个抽象仓库类。GenericDeclRepository抽象类是用来存贮通用信息的,ClassRepository类是用用来存贮类的信息的,ConstructorRepository类用来存贮构造器的信息的,而MethodRepository是用来存贮方法信息的。它们之间的关系如图示:
从图中可以看出,它们之间的继承关系,以及实现的方法名,除了FiledRepository,其它都继承GenericDeclRepository仓库,在GenericDeclRepository里实现了获取类型变量的getTypeParameters函数,因为Java默认Filed不需要类型变量的,但是Filed是可以使用泛型的,从其中的getGenericType方法可以看出。而方法仓库继承构造器仓库,从另一方面说明了构造器是特殊的方法。拿获取类的父类来做例子:
public Type getGenericSuperclass() { //先得到一个仓库 ClassRepository info = getGenericInfo(); //仓库未建立,直接调用本地方法找父类 if (info == null) { return getSuperclass(); } //判定不是接口 if (isInterface()) { return null; } //调用类仓库的方法获取父类 return info.getSuperclass(); }
其实仓库的建立是起着一种缓存的机制,因为如果每次都从jvm中拿类的属性,会降低java整体的性能,为了提高性能,在第一次从jvm中拿到想要的信息后存在仓库中,后面直接从仓库中获取。
作者:
链接:https://blog.csdn.net/hackersuye/article/details/83387888
来源:CSDN
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。