Java类加载机制和反射机制

    xiaoxiao2023-11-05  152

    文章目录

    1 类加载1.1 Class1.2 类的生命周期1.3 类加载时机1.3 类加载器的种类1.3 类加载机制 2 反射机制2.1 反射机制的定义2.2 为什么要反射? 3 使用反射3.1 获取Class3.1.1 xxx.class3.1.2 Class.forName()3.1.3 obj.getClass() 3.2 通过Class获取obj3.2.1 xxx.class获取obj3.2.2 Class.forName()获取obj 3.3 成员、方法、构造访问方式和限制3.3.1 Field成员访问方式和限制3.3.2 Method方法访问和限制3.3.3 构造方法访问和限制 3.4 通过obj反射获取对象的成员、方法等3.4.1 xxx.class获取对象的成员、方法等3.4.2 Class.forName()获取对象的成员、方法等

    1 类加载

    在使用反射的时候,或许我们经常会遇到类初始化、类加载这些字眼,如果没搞明白它们是什么,我们是不能很好的在恰当的时机使用反射的。所以我们要先搞懂什么是类、类加载、类初始化。

    1.1 Class

    在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类的对象作用是运行时提供或获取某个对象的类型信息,这也是反射的需要

    1.2 类的生命周期

    上面是一个类它的生命周期走向:

    加载:将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对象

    类初始化是在类生命周期的初始化阶段

    那么我们的类是什么时候被加载的?

    1.3 类加载时机

    class Candy { static { System.out.println("Loading Candy"); } } class Gum { static { System.out.println("Loading Gum"); } } class Cookie { static { System.out.println("Loading Cookie"); } } public class Test { public static void main(String[] args) { println("inside main"); new Candy(); println("After creating Candy"); try { Class.forName("com.example.Gum"); } catch (ClassNotFoundException e) { // } println("After Class.forName() load Gum") new Cookie(); println("After creating Cookie"); } } 运行结果: inside main Loading Candy After creating Candy Loading Gum After Class.forName() load Gum Loading Cookie After creating Cookie

    可以发现,Candy、Gum、Cookie 在执行到对应的类创建时,它们的静态代码块被执行了,而静态代码块会在类第一次被加载时执行。

    所以,类加载的时机可以简单理解为代码执行到类创建代码时才开始加载。当然,这是在没有反射的情况下。

    1.3 类加载器的种类

    启动类加载器(Bootstrap ClassLoader):负责加载 jre 的核心类库,如 jre 目标下的 rt.jar、charsets.jar 等

    扩展类加载器(Extension ClassLoader):负责加载 jre 扩展目录 ext 中jar类包

    系统类加载器(Application ClassLoader):负责加载 ClassPath 路径下的类包(自己 new 创建对象的类就是在这个类加载器加载)

    用户自定义加载器(User ClassLoader):负责加载用户自定义路径下的类包

    或许你会顾名思义认为类加载器就是用来加载类的,其实并不是。类加载器并不是用来加载类的,它的作用是找到不同路径下的类而已。

    上面列出来很多个 ClassLoader 类加载器。但是我们在分类的时候只需要知道,在Java中类加载器分为两种:一种是启动类加载器(Bootstrap ClassLoader),一种是其他类加载器(扩展类加载器、系统类加载器、用于自定义加载器)。

    1.3 类加载机制

    我们常说的类加载,其实就是对应的类生命周期中的加载阶段。在加载阶段就不得不说类加载机制。

    类加载机制有两种,一种是全盘负责委托机制,一种是面试时经常会问到的双亲委派机制。

    全盘负责委托机制:当一个 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查找类字节码的机制。

    2 反射机制

    了解了类加载的机制,我们就可以开始了解反射。

    2.1 反射机制的定义

    java反射是java被视为动态(或准动态)语言的一个关键性质。反射机制允许程序在运行时(不是编译时)透过 Reflection APIs 取得任何一个已知名称的class内部信息,包括 modifiers(权限修饰符如 public、static 等)、superclass(例如 Object)、实现的 interfaces(如 Cloneable)、fields(属性)、methods(方法),并可以在运行时改变 field 和使用 methods。

    根据反射的定义,反射最主要的关键在于两点:动态、运行时,前提是获取已知名称的class。

    2.2 为什么要反射?

    为什么要使用反射?因为我们需要使用的类没有被加载过,或者不能访问,而我们需要访问到类的信息。

    结合上面的类加载机制和反射的定义,具体说明是:

    我们需要访问的类(比如 Test)还没有被执行到,为了能够访问到它的信息,所以通过反射的方式将 Test 类的 Test.class 字节码文件提前的在类加载器加载到JVM中,生成 Test 类对应的Class对象,通过访问这个Class对象我就能访问到 Test 的具体类信息了。

    3 使用反射

    在实际开发中,使用反射这项技术无非就是访问类的方法、成员等,在使用操作步骤上大概可以总结为以下几点:

    获取Class

    通过Class获取obj

    通过obj反射获取对象的成员、方法等

    3.1 获取Class

    以下是三种获取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..."); } }

    3.1.1 xxx.class

    Class<Test> clazz = Test.class; System.out.println(clazz); 运行结果: class com.example.Test

    可以发现,使用字面量 Test.class 可以获取到Class对象的类型信息,但没有执行静态代码块相关的代码。

    对应到类的生命周期,说明它已经完成了类加载阶段创建了Class对象,所以才能访问到具体类型。而没有执行静态代码块相关的代码,说明类没有完成初始化阶段。

    使用这种方式获取Class有它的局限性:

    必须要能访问到具体类型的Class调用 xxx.class,如果想要反射一些第三方库或源码内部的类是做不到的。

    3.1.2 Class.forName()

    我们看下 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

    可以发现,类的静态代码块被执行了,但是构造函数没有被调用,但也说明完成了类加载和初始化阶段。

    3.1.3 obj.getClass()

    Test test = new Test(); System.out.println(test.getClass()); 运行结果: static block initialized.... non static block initialized... constructor initialized... class com.example.Test

    可以发现,类的静态代码块、非静态代码块、构造函数都已经被调用,类已经完成加载阶段、初始化阶段,可以直接使用。

    3.2 通过Class获取obj

    同样的还是分成两种方式获取。

    3.2.1 xxx.class获取obj

    Class<Test> clazz = Test.class; Object obj = clazz.newInstance(); 或 Constructor<?> constructor = clazz.getDeclaredConstructor(); // 可传入构造参数类型 Object obj = constructor.newInstance(); // 可传入构造函数参数

    使用 clazz.newInstance() 有局限性:

    需要提供无参构造方法并且是公开可访问的。

    3.2.2 Class.forName()获取obj

    Class<?> clazz = Class.forName("com.example.Test"); Constructor<?> constructor = clazz.getDeclaredConstructor(); // 可传入构造参数类型 return constructor.newInstance(); // 可传入构造函数参数

    3.3 成员、方法、构造访问方式和限制

    3.3.1 Field成员访问方式和限制

    方法本classsuper classgetFieldpublicpublicgetDeclaredFieldpublic protected privatenogetFieldspublicpublicgetDeclaredFieldspublic protected privateno

    getField 只能获取对象中public的修饰符的属性,并且能获取父类Class的public属性;getDeclaredField 能获取对象中各种修饰符的属性,但无法获取父类的任何属性。

    获取父类属性,可以通过class对象提供的 getSuperClass 方法获取到父类对象,然后再通过 getDeclaredField 获取属性:

    Class clazz = Class.forName("com.example.Reflect"); Class superClazz = clazz.getSuperClass(); Field field = sueprclazz.getDeclaredField("field");

    3.3.2 Method方法访问和限制

    方法本classsuper classgetMethodpublicpublicgetDeclaredMethodpublic protected privatenogetMethodspublicpublicgetDeclaredMethodspublic protected privateno

    3.3.3 构造方法访问和限制

    方法本classsuper classgetConstructorpublicnogetDeclaredConstructorpublic protected privatenogetConstructorspublicnogetDeclaredConstructorspublic protected privateno

    需要注意的是,构造方法无法调用父类的任何构造方法。

    3.4 通过obj反射获取对象的成员、方法等

    下面的代码中会出现 setAccessible(true),在安全管理中会使用checkPermission方法来检查权限,而 setAccessible(true) 并不是将方法的权限改为public,而是取消java的权限控制检查。

    3.4.1 xxx.class获取对象的成员、方法等

    Class<Test> clazz = Test.class; Object obj = clazz.newInstance(); // 访问非静态成员变量 Field instanceField = clazz.getDeclaredField("instanceField"); // instanceField.setAccessible(true); // 如果成员是私有的,在访问前修改为可访问 instanceField.get(obj); // 访问静态成员变量 Field staticField = clazz.getDeclaredField("staticField"); // staticField.setAccessible(true); // 如果成员是私有的,在访问前修改为可访问 staticField.get(obj); // 访问非静态方法 Method invokeMethod = clazz.getDeclaredMethod("invokeMethod"); // invokeMethod.setAccessible(true); // 如果方法是私有的,在访问前修改为可访问 invokeMethod.invoke(obj); // 访问有参数的非静态成员方法 Method invokeMethodWithParam = clazz.getDeclaredMethod("invokeMethodWithParam", String.class); // invokeMethodWithParam.setAccessible(true); // 如果方法是私有的,在访问前修改为可访问 invokeMethodWithParam.invoke(obj, "my param"); // 访问静态方法 Method invokeStaticMethod = clazz.getDeclaredMethod("invokeStaticMethod"); // invokeStaticMethod.setAccessible(true); // 如果方法是私有的,在访问前修改为可访问 invokeStaticMethod.invoke(obj); // 访问属性中的修饰符 // getModifiers()返回的是一个int类型的返回值,代表类、成员变量、方法的修饰符 String fieldModifier = Modifier.toString(instanceField.getModifiers());

    3.4.2 Class.forName()获取对象的成员、方法等

    Class<?> clazz = Class.forName("com.example.Test"); Constructor<?> constructor = clazz.getDeclaredConstructor(); Object obj = constructor.newInstance(); // 访问非静态成员变量 Field instanceField = clazz.getDeclaredField("instanceField"); // instanceField.setAccessible(true); // 如果成员是私有的,在访问前修改为可访问 instanceField.get(obj); // 访问静态成员变量 Field staticField = clazz.getDeclaredField("staticField"); // staticField.setAccessible(true); // 如果成员是私有的,在访问前修改为可访问 staticField.get(obj); // 访问非静态方法 Method invokeMethod = clazz.getDeclaredMethod("invokeMethod"); // invokeMethod.setAccessible(true); // 如果方法是私有的,在访问前修改为可访问 invokeMethod.invoke(obj); // 访问有参数的非静态成员方法 Method invokeMethodWithParam = clazz.getDeclaredMethod("invokeMethodWithParam", String.class); // invokeMethodWithParam.setAccessible(true); // 如果方法是私有的,在访问前修改为可访问 invokeMethodWithParam.invoke(obj, "my param"); // 访问静态方法 Method invokeStaticMethod = clazz.getDeclaredMethod("invokeStaticMethod"); // invokeStaticMethod.setAccessible(true); // 如果方法是私有的,在访问前修改为可访问 invokeStaticMethod.invoke(obj); // 访问属性中的修饰符 // getModifiers()返回的是一个int类型的返回值,代表类、成员变量、方法的修饰符 String fieldModifier = Modifier.toString(instanceField.getModifiers());
    最新回复(0)