简介
- 定义:
Java
语言中 一种 动态(运行时)访问、检测 & 修改它本身的能力 - 作用:动态(运行时)获取 类的完整结构信息 & 调用对象的方法(类的结构信息包括:变量、方法等)
特点
优点
灵活性高。因为反射属于动态编译,即只有到运行时才动态创建 &获取对象实例。
- 静态编译:在编译时确定类型 & 绑定对象。如常见的使用
new
关键字创建对象- 动态编译:运行时确定类型 & 绑定对象。动态编译体现了
Java
的灵活性、多态特性 & 降低类之间的藕合性
缺点
执行效率低
因为反射的操作 主要通过
JVM
执行,所以时间成本会 高于 直接执行相同操作
因为接口的通用性,Java的invoke方法是传object和object[]数组的。基本类型参数需要装箱和拆箱,产生大量额外的对象和内存开销,频繁促发GC。
编译器难以对动态调用的代码提前做优化,比如方法内联。
反射需要按名检索类和方法,有一定的时间开销。
- 容易破坏类结构
因为反射操作饶过了源码,容易干扰类原有的内部逻辑
应用场景
- 动态获取 类文件结构信息(如变量、方法等 & 调用对象的方法
- 常用的需求场景有:动态代理、工厂模式优化、
Java JDBC
数据库操作等
使用
反射能干啥?
反射的实现主要是通过操作 java.lang.Class 类,因此来学一下这个类
java.lang.Class 类
- 定义:
java.lang.Class
类是反射机制的基础 - 作用:存放着对应类型对象的 运行时信息
- 在
Java
程序运行时,Java
虚拟机为所有 类型 维护一个java.lang.Class
对象- 该
Class
对象存放着所有关于该对象的 运行时信息- 泛型形式为
Class<T>
看下面一段代码,对于2个String类型对象,它们的Class对象相同:
1 | Class c1 = "hello".getClass(); |
结论:每种类型的Class对象只有1个 = 地址只有1个
其他类
Java
反射机制的实现除了依靠Java.lang.Class
类,还需要依靠:Constructor
类、Field
类、Method
类,分别作用于类的各个组成部分:
使用步骤
在使用Java
反射机制时,主要步骤包括:
- 获取目标类型的
Class
对象 - 通过
Class
对象分别获取Constructor
类对象、Method
类对象 &Field
类对象 - 通过
Constructor
类对象、Method
类对象 &Field
类对象分别获取类的构造函数、方法&属性的具体信息,并进行后续操作
步骤1:获取目标类型的Class对象
获取目标类型的Class
对象的方式主要有4种:
1 |
|
步骤2:通过 Class 对象分别获取Constructor类对象、Method类对象 & Field 类对象
1 | // 即以下方法都属于`Class` 类的方法。 |
步骤3:通过 Constructor类对象、Method类对象 & Field类对象分别获取类的构造函数、方法 & 属性的具体信息 & 进行操作
这部分方法很重要,熟练了后实际操作才能感受到反射的好用
1 | // 即以下方法都分别属于`Constructor`类、`Method`类 & `Field`类的方法。 |
访问权限问题
背景
反射机制的默认行为受限于Java
的访问控制。如:无法访问(private
)私有的方法、字段冲突
Java安全机制只允许查看任意对象有哪些域,而不允许读它们的值。若强制读取,将抛出异常解决方案
脱离Java
程序中安全管理器的控制、屏蔽Java语言的访问检查,从而脱离访问控制- 具体实现手段:使用
Field类
、Method类
&Constructor
类对象的setAccessible()
1 | void setAccessible(boolean flag) |
基础实例学习
利用反射获取类的属性 & 赋值
Student 类:
1 | class Student { |
1 | try { |
上面的代码自己可以改着看看,比如将序号4的代码注释掉,就会出现我们上面提到的访问权限的问题。会提示你不能访问private修饰的属性。
1 | java.lang.IllegalAccessException: Class TestDemo can not access a member of class Student with modifiers "private" |
利用反射调用类的构造函数
1 | class Student { |
1 | try { |
利用反射调用类对象的方法
1 | class Student { |
1 | try { |
常见需求场景讲解
工厂模式优化
- 背景
采用简单工厂模式 冲突
- 操作成本高:每增加一个接口的子类,必须修改工厂类的逻辑
- 系统复杂性提高:每增加一个接口的子类,都必须向工厂类添加逻辑
解决方案
采用反射机制: 通过 传入子类名称 & 动态创建子类实例,从而使得在增加产品接口子类的情况下,也不需要修改工厂类的逻辑
- 实例演示
步骤1. 创建抽象产品类的公共接口
Product.java:
1 | abstract class Product{ |
步骤2. 创建具体产品类(继承抽象产品类),定义生产的具体产品
1 | <-- 具体产品类A:ProductA.java --> |
步骤3. 创建工厂类
Factory.java
1 | public class Factory { |
步骤4:外界通过调用工厂类的静态方法(反射原理),传入不同参数从而创建不同具体产品类的实例
TestReflect.java
1 | public class Test { |
可以明显看出,通过采用反射机制(通过传入子类名称 & 动态创建子类实例),从而使得在增加产品接口子类的情况下,也不需要修改工厂类的逻辑 & 增加系统复杂度。
工厂模式再优化
背景
在上述方案中,通过调用工厂类的静态方法(反射原理),从而动态创建产品类实例(该过程中:需传入完整的类名 & 包名)冲突
开发者 无法提前预知 接口中的子类类型 & 完整类名解决方案
通过 属性文件的形式(Properties
) 配置所要的子类信息,在使用时直接读取属性配置文件从而获取子类信息(完整类名)- 实例演示
步骤1、2、3同上
步骤4:创建属性配置文件
Product.properties
1 | //写入抽象产品接口的子类信息(即完整的类名) |
项目结构如下:
步骤5:下面引用创建的properties文件
之所以要贴出项目结构图,就是为了引用的时候别搞错路径
1 | public static void main(String args[]) throws Exception { |
结果同上
上面读取配置文件还可以采用ClassLoader
只不过,ClassLoader采用的是相对路径,上面的FileInputStream采用的是绝对路径
1 | ClassLoader classLoader = Test.class.getClassLoader(); |
使用反射前后对比
如果Factory类中我们没有采用反射,而是采用常规写法,如下:
1 | class Factory { |
可见,如果我们新增加了一款产品 ProductD,那就要去修改工厂类,如果采用反射的话,就没必要了