外观
15 反射机制
3637 字约 12 分钟
2024-09-01
从代码/编译阶段到类加载阶段
是jvm底层实现的,而类加载到运行阶段
是程序员应用阶段完成的。
class.forName( )方法中一定要填写完整类名(即包名+类名),即便该类与调用forName方法的类在同一包下,也需填写完整类名!
- 反射机制(Reflection)允许程序在执行期借助于 Reflection API 取得任何类的内部信息(如成员变量、成员方法等),并能操作对象的属性及方法。反射在设计模式和框架底层都会用到。
- 加载完类之后,在堆中就产生了一个
Class
类型的对象(一个类只有一个Class
对象),这个对象包含了类的完整结构信息。通过这个对象得到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构。所以,形象地称之为:反射ocp 原则(开闭原则):不修改源码来扩展功能
计算机的三个阶段
代码阶段 / 编译阶段
编写代码 ——(Javac 编译)——> .class 字节码文件
Class 类阶段 / 加载阶段
字节码文件 ——(ClassLoader 类加载器)——>
Class
类对象(堆中)· 字节码二进制数据 / 元数据(方法区)Class
类对象包含:成员变量Field[] fields
、构造器Constructor[] cons
、成员方法Methord[] ms
Runtime 运行阶段
创建对象,该对象知道其属于哪个
Class
对象
反射机制可以完成
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时得到任意一个类所具有的成员变量和方法
- 在运行时调用任意一个对象的成员变量和方法
- 生成动态代理
21.1 反射相关的常用类
java.lang.Class
:代表一个类。Class
对象表示某个类加载后在堆中的对象Class cls = Class.forName(classFullPath); //[1] Object o = cls.newInstance(); //[2]
- 通过完整类名得到一个类的 Class 对象
- 通过该 Class 对象创建一个该类的 对象实例
java.lang.reflect.Method
:代表类的方法。Method
对象表示某个类的某个方法Method method = cls.getMethod(methodName); //[1] method.invoke(o); //[2]
- 通过该 Class 对象得到一个 方法对象
- 方法对象.invoke:调用该方法
java.lang.reflect.Field
:代表类的成员变量Field field = cls.getField(fieldName); //[1]
- 该方法只能得到非私有对象
java.lang.reflect.Constructor
:代表类的构造方法Constructor constructor = cls.getConstructor(); //[1] Constructor constructor2 = cls.getConstructor(String.class) //[2]
- 得到一个无参构造器
- 得到一个形参是
(String str)
的构造器
反射的优点和缺点
- 优点:可以动态地创建和使用对象(也是框架底层核心),使用灵活。没有反射机制,框架技术就失去底层支撑
- 缺点:使用反射基本是解释执行。这对执行速度有影响。
反射调用优化 - 关闭访问检查
Method
和Field
、Constructor
对象都有setAccessible()
方法setAccessible()
作用是启动和禁用访问安全检查的开关,在需要访问私有域(会破坏封装性)时,传一个true。参数值为 true,表示反射对象在使用时取消访问检查,这样能提高反射效率。
为 false 表示执行访问检查
21.2 Class
类
Class
也是类,因此也继承Object
类Class
类不是 new 出来的,而是系统创建的- 对于某个类的
Class
类对象,在内存中只有一份,因为类只加载一次 - 每个类的实例都会记得自己是由哪个
Class
实例生成 - 通过
Class
可以完整地得到一个类的完整结构,通过一系列 API Class
对象是存放在堆的- 类的字节码二进制数据,是放在方法区的。有的地方称为类的元数据(包括 方法代码、变量名、方法名、访问权限 等)
21.2.1 Class
类的常用方法
Class.forName(String)
:返回指定类名的Class
对象newInstance()
:返回一个无参构造器创建的实例getName()
:返回该Class
对象表示的实体的全类名getClass()
:返回该Class
对象的运行类型java.lang.Class
getPackage()
:返回该Class
对象所在的包getSuperClass()
:返回该Class
对象的父类Class
对象getInterface()
:返回该Class
对象的接口(数组)getAnnotations()
:返回注解信息(Annotation[]
)getClassLoader()
:返回该Class
对象的加载器(ClassLoader
类型)getSuperclass()
:返回该Class
对象实体的超类的Class
getConstructors()
:返回本类所有包含public
修饰的构造器的Constructor
对象数组该方法返回的构造器不含父类构造器!
getDeclaredConstructer()
:返回本类所有构造器的Constructor
对象数组getFileds()
:返回一个包含public
修饰的属性的Field
对象的数组getFiled(String name)
:返回指定的Field
getDeclaredFields()
:获取本类中所有属性field.get(instance)
:返回指定实例的指定属性field.set(instance, ..)
:给指定实例的指定属性赋值getMethod()
:获得所有public
修饰的方法的Method
对象==
getMethod(String name, Class paramTypes, ...)
:==返回一个Method
对象,其形参类型为 paramTypegetDeclaredMethod()
:获取本类中所有方法isAssignableFrom();在其中传入一个类对象,用来判断调用这个方法的类对象是否为传入类对象的类型,根instanceof的作用相似。
21.2.2 获取 Class
对象
- (编译阶段)已知一个类的全类名,且该类在类路径下:
Class cls1 = Class.forName("com.melody.note.Test");
会调用类的静态方法,相较于classloader是重量级的,classloader返回的是部分信息,而这个是返回类的所有信息
应用场景:配置文件,读取类全路径,加载类。
可能抛出 `ClassNotFoundExcption`
- (加载阶段)已知具体的类:
Class cls2 = Test.class;
应用场景:参数传递。
例如:
ConverBeanUtils.convert(userMapper.selectById(userId),UserDTO.class);//这段代码封装了spring的BeanUtils类copyProperties方法,将第一个参数的类型转换为第二个参数的类型(实质上是将第一个类型的部分属性的值转到一个新的对象的同名属性中)
该方法最为安全
- (运行阶段)已知某个类的实例:
Class cls3 = new Test().getClass();
应用场景:通过创建好的对象获取 `Class` 对象
- 通过类加载器:
ClassLoader cll = new Test().getClass().getClassLoader();
Class cls4 = cll.loadClass("com.melody.note.Test");
不会调用类的静态方法,轻量级的
应用场景:适用于对一个类进行判断的场景(比如是否包含注解)
- 基本数据类型:
Class clsB1 = int.class;
Class<Boolean> clsB2 = boolean.class;
- 基本数据类型包装类:
Class clsB3 = Character.TYPE;
Class<Long> clsB4 = Long.TYPE;
21.2.3 哪些类有 Class
对象
- 外部类、成员内部类、静态内部类、局部内部类、匿名内部类
- 接口(interface)
- 数组
- 枚举(enum)
- 注解
- 基本数据类型
- void
21.3 类的加载
基本说明
反射机制是 Java 实现动态语言的关键,也就是通过反射实现类动态加载,java是静态强类型语言
- 静态加载:编译时加载相关的类(不管有没有执行到代码块),如果没有则报错。依赖性强
- 动态加载:运行时加载需要的类(执行到相关的代码块时),如果运行时不用该类,则不报错。降低了依赖性
类加载时机
- 创建对象时(new) [静态加载]
- 子类被加载时,父类也被加载 [静态加载]
- 调用类中的静态成员 [静态加载]
- 通过反射 [动态加载]
(类加载图_21.3)
加载(Loading):
将类的 .class 文件读入内存,并为之创建一个 java.lang.Class 对象。此过程由类加载器完成
连接(Linking):
将类的二进制数据合并进 JRE 中
初始化(initialization):
JVM 负责对类进行初始化。这里主要是静态成员
21.3.1 类加载的五个阶段
加载阶段
JVM 在该阶段的主要目的是将字节码从不同数据源(.class 文件、jar 包、网络等)转化为二进制字节流加载到内存中,并生成一个代表该类的
java.lang.Class
对象连接阶段 - 验证
目的是确保
Class
文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。包括:文件格式验证(是否以魔数 0xcafebabe 开头)、元数据验证、字节码验证、符号引用验证
可以考虑使用
-Xverify:none
参数来关闭大部分的类验证措施,以缩短虚拟机加载的时间连接阶段 - 准备
JVM 会在该阶段对 静态变量 分配内存并执行默认初始化。这些变量使用的内存都将在方法区中进行分配
public int n1 = 1; //实例属性,非静态变量,此阶段不分配内存 public static int n2 = 2; //静态变量,默认初始化为 0,后面初始化的时候才会真正赋为2. public static final int n3 = 3; //static final 常量,静态初始化为 3(一开始就是3,后面就不变了)
连接阶段 - 解析
JVM 将常量池内符号引用替换为直接引用的过程
初始化
到初始化阶段,才真正开始执行类中定义的 Java 程序代码。此阶段是执行
<clinit>()
方法的过程<clinit>()
方法是由编译器按语句在文件中出现的顺序,依次自动收集类中所有静态变量的赋值动作和静态代码块中的语句,并进行合并JVM 会保证一个类的
<clinit>()
方法在多线程环境中被正确地加锁、同步。如果多个线程去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()
方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()
方法完毕
21.4 通过反射获取类的结构信息
java.lang.Class
类(与前面的的重复)
getSuperClass()
:返回该Class
对象的父类Class
对象
getInterface()
:返回该Class
对象的接口(数组)
getAnnotations()
:返回注解信息(Annotation[]
)
getClassLoader()
:返回该Class
对象的加载器(ClassLoader
类型)
getSuperclass()
:返回该Class
对象实体的超类的Class
getConstructors()
:返回本类所有包含public
修饰的构造器的Constructor
对象数组该方法返回的构造器不含父类构造器!
getDeclaredConstructer()
:返回本类所有构造器的Constructor
对象数组
getFileds()
:返回一个包含public
修饰的属性的Field
对象的数组
getFiled(String name)
:返回指定的Field
getDeclaredFields()
:获取本类中所有属性
field.get(instance)
:返回指定实例的指定属性
field.set(instance, ..)
:给指定实例的指定属性赋值
getMethod()
:获得所有public
修饰的方法的Method
对象
getMethod(String name, Class paramTypes, ...)
:返回一个Method
对象,其形参类型为 paramType
getDeclaredMethod()
:获取本类中所有方法
java.lang.reflect.Field
类
getModifiers()
:以 int 形式返回修饰符默认修饰符 [0]、public [1]、private [2]、protected [4]、static [8]、final [16]
示例:
public static final int n = 0;
这个变量的修饰符的 int 表示 =
1 + 8 + 16 = 25
getType()
:以Class
形式返回类型上例变量的
getType()
等同于Integer.getClass()
getName()
:返回属性名
java.lang.reflect.Method
类
getModifiers()
:以 int 形式返回修饰符(同上)getName()
:返回方法名getReturnType()
:以Class
形式返回返回类型getParameterTypes()
:以Class[]
形式返回形参类型数组
java.lang.reflect.Constructer
类
getModifiers()
:以 int 形式返回修饰符getName()
:返回构造器名(和全类名相等)getParameterTypes()
:以Class[]
形式返回形参类型数组
21.5 通过反射创建对象
调用类中的 public 修饰的无参构造器
Object obj1 = cls.newInstance();
调用类中指定的构造器
Constructer cons = cls.getConstructer(int.class, String.class, ...); Object obj2 = cons.newInstance(1, "nnn", ...);
setAccessible(true)
:爆破(暴力破解)。使用反射可以访问 private 构造器,这样做会破坏封装性。Constructer cons2 = cls.getDeclaredConstructer(boolean.class ...); cons2.setAccessible(true);//下面就可以调用私有构造器创建对象了 Object obj3 = cons.newInstance(false, ...);
21.6 通过反射访问成员
Field field = cla.getDeclaredField("name"); field.setAccessible(true); field.set(o, "111"); //[1]
- o 表示一个类的实例
如果该属性是静态属性(属于类的),则对象 o 可以是 null
Method method = cls.getDeclaredMethod("m1"); method.setAccessible(true); Object returnObj = method.invoke(o, 'c', ...); //[1]
- o 表示一个类的实例,后面是实参列表
同理,静态方法的场合,对象 o 可以是 null。
在反射中调用方法时,如果方法有返回值,例如String,调用会返回Object类型(编译类型),但是实际运行类型还是String。
反射的应用场景
本节参考反射的应用场景
类型判断
创建对象
- 1、使用 Class.newInstance(),适用于类拥有无参构造方法
Class<?> classType = Class.forName("java.lang.String");
String str= (String) classType.newInstance();
- 2、Constructor.newInstance(),适用于使用带参数的构造方法
Class<?> classType = Class.forName("java.lang.String");
Constructor<?> constructor = classType.getConstructor(new Class[]{String.class});
constructor.setAccessible(true);
String employee3 = (String) constructor.newInstance(new Object[]{"123"});
创建数组
创建数组需要元素的 Class 对象作为 ComponentType:
- 1、创建一维数组
Class<?> classType = Class.forName("java.lang.String");
String[] array = (String[]) Array.newInstance(classType, 5); 长度为5
Array.set(array, 3, "abc"); 设置元素
String string = (String) Array.get(array,3); 读取元素
- 2、创建多维数组
Class[] dimens = {3, 3};
Class[][] array = (Class[][]) Array.newInstance(int.class, dimens);
访问字段、方法
Editting...
获取泛型信息
我们知道,编译期会进行类型擦除,Code 属性中的类型信息会被擦除,但是在类常量池属性(Signature属性、LocalVariableTypeTable属性)中还保留着泛型信息,因此我们可以通过反射来获取这部分信息。在这篇文章里,我们详细讨论:《Java | 关于泛型能问的都在这里了(含Kotlin)》,请关注!
获取运行时注解信息
注解是一种添加到声明上的元数据,而RUNTIME
注解在类加载后会保存在 Class 对象,可以反射获取。在这篇文章里,我们详细讨论:《Java | 这是一篇全面的注解使用攻略(含 Kotlin)》,请关注!