在使用反射的时候,或许我们经常会遇到类初始化、类加载这些字眼,如果没搞明白它们是什么,我们是不能很好的在恰当的时机使用反射的。所以我们要先搞懂什么是类、类加载、类初始化。
在Java中用来表示运行时类型信息的对应类就是 Class,Class也是一个类。
Class类被创建后的对象就是Class对象。需要注意的是,Class对象表示的是自己手动编写类的类型信息。比如创建一个 Shapes 类,JVM就会创建一个 Shapes 对应Class类的Class对象,该Class对象保存了 Shapes 类相关的类型信息。
实际上,在Java中每个类都有一个Class对象,每当我们编写并且编译一个新创建的类就会产生一个对应Class对象,并且这个Class对象会被保存在同名为 .class 文件里(编译后的 .class 字节码文件就是Class对象)。
为什么需要Class对象呢?
当我们 new 一个新对象或者引用静态成员变量时,JVM的类加载器会将对应Class对象加载到JVM中,然后再根据这个类型信息相关的Class对象创建我们需要实例对象或者提供静态变量的引用值。
在JVM中,Class有且只有一个,这需要结合上面的类加载机制说明,就是类已经被加载过了,下一次将会是从缓存获取。
总结下上面的说明:
Class 也是类的一种,与 class 关键字不一样
手动编写的类被编译后会产生一个Class对象,Class对象表示的是创建的类的类型信息,而且这个Class对象保存在同名的 .class 文件中。比如创建一个 Shapes 类,编译 Shapes 类后就会创建包含 Shapes 类型信息的Class对象,并保存在 Shapes.class 字节码文件中
每个通过关键字 class 标识的类,在内存中有且只有一个与之对应的Class对象描述其类型信息,无论创建多少个实例对象,其依据都是用一个Class对象
Class类的对象作用是运行时提供或获取某个对象的类型信息,这也是反射的需要
上面是一个类它的生命周期走向:
加载:将java文件编译为 .class 文件后,将 .class 文件从磁盘读取到内存,最后在堆中生成这个类的 java.lang.Class 对象
连接:连接阶段又可以具体分为三个步骤:
验证:验证字节码文件的正确性。使用全限定类名验证,比如 com.example.Test
准备:给类的静态变量分配内存,并赋予默认值。比如类中 static int count 变量默认赋值为0
解析:类装载器装入类所引用的其他所有类。比如 String str = "aaa",转化为 str 的地址指向 aaa 的地址)
初始化:为类的静态变量赋予正确的初始值。比如类中 static int count = 10;赋值为10。连接准备阶段为静态变量赋予的是虚拟机默认的初始值,此处赋予的才是程序编写者为变量分配的真正的初始值,执行静态代码块
使用
卸载
根据上面的说明,可以知道:
类加载是在类生命周期的加载阶段,此时类的 .class 文件已经被加载到JVM内存中,也创建了对应类的类型的Class对象
类初始化是在类生命周期的初始化阶段
那么我们的类是什么时候被加载的?
可以发现,Candy、Gum、Cookie 在执行到对应的类创建时,它们的静态代码块被执行了,而静态代码块会在类第一次被加载时执行。
所以,类加载的时机可以简单理解为代码执行到类创建代码时才开始加载。当然,这是在没有反射的情况下。
启动类加载器(Bootstrap ClassLoader):负责加载 jre 的核心类库,如 jre 目标下的 rt.jar、charsets.jar 等
扩展类加载器(Extension ClassLoader):负责加载 jre 扩展目录 ext 中jar类包
系统类加载器(Application ClassLoader):负责加载 ClassPath 路径下的类包(自己 new 创建对象的类就是在这个类加载器加载)
用户自定义加载器(User ClassLoader):负责加载用户自定义路径下的类包
或许你会顾名思义认为类加载器就是用来加载类的,其实并不是。类加载器并不是用来加载类的,它的作用是找到不同路径下的类而已。
上面列出来很多个 ClassLoader 类加载器。但是我们在分类的时候只需要知道,在Java中类加载器分为两种:一种是启动类加载器(Bootstrap ClassLoader),一种是其他类加载器(扩展类加载器、系统类加载器、用于自定义加载器)。
我们常说的类加载,其实就是对应的类生命周期中的加载阶段。在加载阶段就不得不说类加载机制。
类加载机制有两种,一种是全盘负责委托机制,一种是面试时经常会问到的双亲委派机制。
全盘负责委托机制:当一个 ClassLoader 加载一个类的时候,除非显示的使用另一个ClassLoader,该类所依赖和引用的类也由这个ClassLoader载入
双亲委派机制:指先委托父类加载器寻找目标类,在找不到的情况下,在自己的路径中查找并载入目标类
我们主要说一下双亲委派机制。
体现双亲委派机制的代码在于 ClassLoader.loadClass():
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); } else { 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. c = findClass(name); } } return c; }findLoadedClass() 从自己缓存查找是否已经加载了类,有则直接取出来返回
没有缓存,找父ClassLoader要 parent.loacClass()
父ClassLoader都没有,自己加载 findClass()
将上面的操作步骤结合类加载器种类图进行说明的话,步骤如下(加载我们 new 创建的类对象):
AppClassLoader.findLoadedClass() 没有找到缓存,ExtensionClassLoader.loadClass() -> ExtensionClassLoader.findLoadedClass() 没有找到缓存,BootstrapClassLoader.loadClass() -> BootstrapClassLoader.findLoadedClass() 没有找到缓存且 parent==null,自己查找 findClass(),下一次加载这个类就会从自己的缓存 findLoadedClass() 查找到不会再执行这个流程
上面的步骤就是双亲委托机制的本质:ClassLoader是带缓存的、从上到下加载的过程。
双亲委派机制其实在翻译为中文的时候是有误导的,在英文中双亲为 Parent,其实它只是一种往上委托父ClassLoader查找类字节码的机制。
了解了类加载的机制,我们就可以开始了解反射。
java反射是java被视为动态(或准动态)语言的一个关键性质。反射机制允许程序在运行时(不是编译时)透过 Reflection APIs 取得任何一个已知名称的class内部信息,包括 modifiers(权限修饰符如 public、static 等)、superclass(例如 Object)、实现的 interfaces(如 Cloneable)、fields(属性)、methods(方法),并可以在运行时改变 field 和使用 methods。
根据反射的定义,反射最主要的关键在于两点:动态、运行时,前提是获取已知名称的class。
为什么要使用反射?因为我们需要使用的类没有被加载过,或者不能访问,而我们需要访问到类的信息。
结合上面的类加载机制和反射的定义,具体说明是:
我们需要访问的类(比如 Test)还没有被执行到,为了能够访问到它的信息,所以通过反射的方式将 Test 类的 Test.class 字节码文件提前的在类加载器加载到JVM中,生成 Test 类对应的Class对象,通过访问这个Class对象我就能访问到 Test 的具体类信息了。
在实际开发中,使用反射这项技术无非就是访问类的方法、成员等,在使用操作步骤上大概可以总结为以下几点:
获取Class
通过Class获取obj
通过obj反射获取对象的成员、方法等
以下是三种获取Class的方式:
// ClassLoader将类装载入内存,但不进行类的初始化 // 即类完成了加载阶段被加载进JVM内存创建了Class Class clazz = Test.class; // ClassLoader将类装载入内存,并进行类的初始化 // 即类完成了加载阶段并加载进JVM内存创建了Class,并且完成了类初始化阶段 Class clazz = Class.forName("com.example.Test"); // 返回类对象运行时真正所指的对象、所属类型的Class对象 Class clazz = new Test().getClass();这三种加载方式有什么区别?在说明之前,提供一个测试用例用来验证三种方式:
public class Test { static { System.out.println("static block initialized...."); } { System.out.println("non static block initialized..."); } public Test() { System.out.println("constructor initialized..."); } }可以发现,使用字面量 Test.class 可以获取到Class对象的类型信息,但没有执行静态代码块相关的代码。
对应到类的生命周期,说明它已经完成了类加载阶段创建了Class对象,所以才能访问到具体类型。而没有执行静态代码块相关的代码,说明类没有完成初始化阶段。
使用这种方式获取Class有它的局限性:
必须要能访问到具体类型的Class调用 xxx.class,如果想要反射一些第三方库或源码内部的类是做不到的。
我们看下 Class.forName() 的源码:
@CallerSensitive public static Class<?> forName(String className) throws ClassNotFoundException { return forName(className, true, VMStack.getCallingClassLoader()); } /** * @Param initialize if {@code true} the class will be initialized. */ @CallerSensitive public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException { if (loader == null) { loader = BootClassLoader.getInstance(); } Class<?> result; try { // native获取Class result = classForName(name, initialize, loader); } catch (ClassNotFoundException e) { Throwable cause = e.getCause(); if (cause instanceof LinkageError) { throw (LinkageError) cause; } throw e; } return result; } @FastNative static native Class<?> classForName(String className, boolean shouldInitialize, ClassLoader classLoader) throws ClassNotFoundException;我们调用 Class.forName() 是一个重载方法,其中有一个参数 initialized,这个参数代表是否对加载的类进行初始化,设置为true时会类进行初始化,代表会执行类中的静态代码块以及对静态变量的赋值等操作。我们验证一下。
try { Class<?> clazz = Class.forName("com.example.Test"); System.out.println(clazz); } catch (ClassNotFoundException e) { e.printStackTrace(); } 运行结果: static block initialized.... class com.example.Test可以发现,类的静态代码块被执行了,但是构造函数没有被调用,但也说明完成了类加载和初始化阶段。
可以发现,类的静态代码块、非静态代码块、构造函数都已经被调用,类已经完成加载阶段、初始化阶段,可以直接使用。
同样的还是分成两种方式获取。
使用 clazz.newInstance() 有局限性:
需要提供无参构造方法并且是公开可访问的。
getField 只能获取对象中public的修饰符的属性,并且能获取父类Class的public属性;getDeclaredField 能获取对象中各种修饰符的属性,但无法获取父类的任何属性。
获取父类属性,可以通过class对象提供的 getSuperClass 方法获取到父类对象,然后再通过 getDeclaredField 获取属性:
Class clazz = Class.forName("com.example.Reflect"); Class superClazz = clazz.getSuperClass(); Field field = sueprclazz.getDeclaredField("field");需要注意的是,构造方法无法调用父类的任何构造方法。
下面的代码中会出现 setAccessible(true),在安全管理中会使用checkPermission方法来检查权限,而 setAccessible(true) 并不是将方法的权限改为public,而是取消java的权限控制检查。