反射机制

基本介绍

  • 反射机制允许程序在执行期借助⼦Reflection API 取得任何类的内部信息(比如成员变量,构造器,成员⽅法等),并能操作对象的属性及⽅法
  • 加载完类之后,在堆中就产⽣了⼀个Class类型的对象(⼀个类只有⼀个Class对象),这个对象包含了类的完整结构信息。通过这个对象得到类的结构。这个对象就像⼀⾯镜⼦,透过这个镜⼦看到类的结构,所以,形象的称之为:反射

反射相关类

说明
java.lang.Class 代表⼀个类,Class对象表示某个类加载后在堆中的对象
java.lang.reflect.Method 代表该类的⽅法
java.lang.reflect.Field 代表类的成员变量
java.lang.reflect.Constructor 代表类的构造⽅法

案例

读写配置文件

1
2
3
4
5
6
7
Properties properties = new Properties();
properties.load(new FileInputStream("src\\re.properties"));

String classfullpath =
properties.get("classfullpath").toString();

String methodName = properties.get("method").toString();

使⽤反射机制解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 加载类, 返回Class类型的对象cls
Class cls = Class.forName(classfullpath);

// 通过cls得到加载类的实例
Object o = cls.newInstance();
// 运⾏类型
System.out.println("o的运⾏类型=" + o.getClass());

// 通过 cls 得到加载的类的 methodName的⽅法对象
Method method1 = cls.getMethod(methodName);

// 传统⽅法 对象.⽅法() , 反射机制 ⽅法.invoke(对象)
method1.invoke(o);

// 传统写法 对象.成员变量 , 反射 : 成员变量对象.get(对象)
Field nameField = cls.getField("age");
System.out.println(nameField.get(o));

// java.lang.reflect.Constructor: 代表类的构造⽅法, Constructor对象表示构造器
// ()中可以指定构造器参数类型, 返回⽆参构造器
Constructor constructor = cls.getConstructor();
System.out.println(constructor); // Cat()

// String.class 就是String类的Class对象
Constructor constructor2 = cls.getConstructor(String.class);
System.out.println(constructor2); // Cat(String name)

Class类

基本介绍

  • Class 也是类,因此也继承Object类
  • Class 类对象不是 new 出来的,⽽是系统创建的类的 Class 类对象,在内存中只有⼀份,且只加载⼀次
  • 每个类的实例都会记录它由哪个 Class 实例⽣成
  • 通过 Class 的⼀系列 API 可以完整地得到⼀个类的完整结构
  • Class 对象是存放在堆的
  • 类的字节码⼆进制数据,是放在方法区的,有的地⽅称为类的元数据(包括⽅法代码,变量名,⽅法名, 访问权限等)

Class 类对象获取 1

前提:已知⼀个类的全类名,且该类在类路径下,可通过 Class 类的静态⽅法 forName() 获取

1
Class cls1 = Class.forName("java.lang.Cat")

应⽤场景:多⽤于 配置⽂件,读取类全路径,加载类

Class 类对象获取 2

前提:若已知具体的类,通过类的 class 获取,该⽅式最为安全可靠,程序性能最⾼

1
Class cls2 = Cat.class

应⽤场景:多⽤于 参数传递 ,⽐如通过反射得到对应构造器对象

Class 类对象获取 3

前提:已知某个类的实例,调⽤该实例的 getClass() ⽅法获取 Class 对象

1
Class class = 对象.getClass(); 

应⽤场景:通过创建好的对象,获取 Class 对象

注意:这三种方式获取的 Class 对象是同一个 Class 对象,一个类在 JVM 中只会有一个 Class 对象

类加载器

1
2
ClassLoader cl = 对象.getClass().getClassLoader();
Class class4 = cl.loadClass("类的全类名");

基本数据(int,char,boolean,float,double,byte,long,short)

1
Class cls = 基本数据类型.class

基本数据类型对应的包装类,可以通过 .type 得到 Class 类对象

1
Class cls = 包装类.TYPE

有 Class 对象的类型

  • 外部类,成员内部类,静态内部类,局部内部类,匿名内部类
  • interface:接⼝
  • 数组
  • enum:枚举
  • annotation:注解
  • 基本数据类型
  • void

常用方法

方法 说明
static Class forName(String name) 返回指定类名 name 的 Class 对象
Object newInstance() 调用缺省构造函数,返回该Class对象的一个实例
getName() 返回此Class对象所表示的实体(类、接口、数组类、基本类型等)名称
Class [] getInterfaces() 获取当前Class对象的接口
ClassLoader getClassLoader() 返回该类的类加载器
Class getSuperclass() 返回表示此Class所表示的实体的超类的Class
Constructor[] getConstructors0 返回一个包含某些Constructor对象的数组
Field[] getDeclaredFields() 返回Field对象的一个数组
Method getMethod(Striing name,Class paramTypes) 返回一个Method对象,此对象的形参类型为paramType

类加载

基本介绍

  • 类加载器负责将 .class 文件(存储的物理文件)加载到内存中

  • 反射机制是 java 成为动态语⾔的关键,也就是通过反射实现类动态加载

  • 静态加载:编译时加载相关的类,如果没有则报错,依赖性太强
  • 动态加载:运⾏时加载需要的类,如果运⾏时不⽤该类,即使不存在该类,则不报错,降低了依赖性

类加载时机

  • 创建类的实例
  • 调用类的类方法
  • 访问类或接口的类变量,或者为该类变量赋值
  • 使用反射方式来强制创建某个类或接口对应的 java.lang.Class 对象
  • 初始化某个类的子类
  • 直接使用 java.exe 命令来运行某个主类

创建⼀个对象时,在⼀个类的调⽤顺序:

  1. 调用静态代码块和静态属性初始化(注意:静态代码块和静态属性初始化调⽤的优先级一样,如果有多个静态代码块和多个静 态变量初始化,则按他们定义的顺序调用)
  2. 调用普通代码块和普通属性的初始化(注意:普通代码块和普通属性初始化调⽤的优先级⼀样,若果有多 个普通代码块和多个普通属性初始化,则按定义顺序调⽤)
  3. 调用构造⽅法

创建子类时的顺序

  1. 父类的静态代码块和静态属性(优先级⼀样,按定义顺序执行)
  2. 子类的静态代码块和静态属性(优先级⼀样,按定义顺序执行)
  3. 父类的普通代码块和普通属性初始化(优先级⼀样,按定义顺序执行)
  4. 父类构造⽅法
  5. 子类的普通代码块和普通属性初始化(优先级⼀样,按定义顺序执行)
  6. 子类构造⽅法

类加载过程

  • 其中验证、准备、解析这三步属于 链接 的过程

  • 加载

    1. 通过类的全限定名来获取定义此类的二进制字节流

    2. 将这个字节流所代表的静态存储结构转化为运行时数据结构(加载到内存中)

    3. 在内存中生成一个代表这个类的 java.lang.Class 对象(任何类被使用时,系统都会为它建立一个 java.lang.Class 对象)

  • 链接

    1. 验证:确保 Class 文件字节流中包含的信息符合虚拟机要求,不会危害虚拟机自身安全
    2. 准备:为类的 类变量 分配内存,并设置 默认 初始化值
    3. 解析:将二进制数据流中的 符号引用 替换为 直接引用 ,意思是找到需要用到的类
  • 初始化:根据程序员通过程序制定的主观计划去初始化 类变量 和其他资源(静态变量赋值和初始化其他资源)

类加载器的分类

  • 启动类加载器(Bootstrap ClassLoader):虚拟机内置的类加载器
  • 平台类加载器(Platform ClassLoader):负责加载JDK中一些特殊的模块
  • 系统类加载器(System ClassLoader):负责加载用户类路径上所指定的类库

双亲委派模型

  • 类加载器收到加载任务时会将加载任务逐层向上传递给父类加载器,最终到达顶层的启动类加载器
  • 当启动类加载器不能完成加载任务时,再委托给下层的子类加载器,最终完成类的加载
  • ClassLoader 对象的 getParent() 方法可以获取父类加载器

常用方法

方法名 说明
public static ClassLoader getSystemClassLoader() 获取系统类加载器
public InputStream getResourceAsStream(String name) 加载某一个资源文件

反射获取构造方法

方法名 说明
Constructor<?>[] getConstructors() 返回所有公共构造方法对象的数组
Constructor<?>[] getDeclaredConstructors() 返回所有构造方法对象的数组(包括私有)
Constructor<?> getConstructor(Class<?>…parameterTypes) 获取单个、公共的构造方法对象,如果是无参构造则该方法参数留空,如果是有参则该方法参数是参数类型的Class对象
Constructor<?> getDeclaredConstructor(Class<?>…parameterTypes) 获取单个、公共或私有的构造方法对象,如果是无参构造则该方法参数留空,如果是有参则该方法参数是参数类型的Class对象

由获取的构造方法对象创建对象

  1. 可以利用构造方法类 Constructor 中的 T newInstance(Object...initargs) 方法来创建对象

  2. 如果该构造方法是 私有 的构造方法,则创建对象之前需要使用Constructor类中的 void setAccessible(boolean b) 方法临时取消访问检查

反射获取Class对象

1
2
3
4
5
6
7
8
9
10
11
class Student{
private Student(String name){
System.out.println("私有的构造方法");
}

public Student(String name,int age){
System.out.println("公共的构造方法");
}
}

Class clazz = Class.forName("Student");

反射获取公共的构造方法

1
2
3
4
Constructor publicConstructor = clazz.getConstructor(String.class, int.class);

// 由公共构造方法创建对象
Student student = (Student) publicConstructor.newInstance("CyanChau", 21)

反射获取私有的构造方法

1
2
3
4
5
6
7
Constructor privateConstructor = clazz.getDeclaredConstructor(String.class);

// 临时取消访问检查
privateConstructor.setAccessible(true);

// 由私有的构造方法创建对象
Student student = (Student) privateConstructor.newInstance("CyanChau");

反射获取成员变量

方法名 说明
Field[] getFields() 返回所有公共成员变量对象的数组
Field[] getDeclaredFields() 返回所有成员变量对象的数组
Field getField(String name) 根据变量名获取一个公共成员变量对象
Field getDeclaredField(String name) 根据变量名获取一个成员变量对象

由 Field 对象操作成员变量

  1. void set(Object o,Object v),设置某个成员变量的值,第一个参数是需要设置成员变量的对象,第二个参数是成员变量的值
  2. Object get(Object o),获取某个对象的当前成员变量的值
  3. 注意,如果成员变量是私有的,在使用 get 和 set 方法时也需要 使用setAccessible()方法暂时取消访问检查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1.获取类对象
Class c = Student.class;

// 2.获取成员变量
Field name = c.getDeclaredField("name");
Field age = c.getDeclaredField("age");

// 3. 赋予访问私有字段权限
age.setAccessible(true);

// 4.给字段赋值
Student student = new Student();
name.set(student, "CyanChau");
age.set(student, 20);

// 打印对象并获取字段值
System.out.println(student + "===>" + name.get(student) + "===>" + age.get(student));

反射获取成员方法

方法名 说明
Method[] getMethods() 获取所有公共成员方法对象数组
Method[] getDeclaredMethods() 获取所有成员方法对象数组
Method getMethod(String name,Class<?>…parameterTypes) 获取一个公共的成员方法对象,第一个参数是方法名,第二个参数是方法的参数的Class对象
Method getDeclaredMethod(String name,Class<?>…parameterTypes) 获取一个成员方法对象,第一个参数是方法名,第二个参数是方法的参数的Class对象

由 Method 对象操作成员方法

  • 使用 Object invoke(Object o,Object...args) 方法可以运行成员方法
  • 第一个参数是指调用该方法的对象
  • 第二个参数是指该方法的参数
  • 返回值 Object 是成员方法的返回值,如果没有则可以不用接收

关闭访问检查:

  • Method 和 Field、Constructor 对象都有 setAccessible() ⽅法
  • setAccessible 作⽤是启动和禁⽤访问安全检查的开关
  • 参数值为 true 表示 反射的对象在使⽤时取消访问检查,提⾼反射的效率。
  • 参数值为 false 则表示反射的对象执⾏访问检查

泛型擦除

反射是作用在 运行时 的技术,而泛型 只在编译阶段约束只能操作某种数据类型,使用反射的话泛型将不能产生对我们产生约束,此时就相当于泛型被擦除了。

编写如下代码时,往集合添加 Integer 类型数据不会产生报错,而添加其他类型数据将无法通过编译。

1
2
3
4
ArrayList<Integer> list = new ArrayList<>();
list.add(100); // 合法
// list.add(Cyan"); // 产生编译错误
list.add(99); // 合法

而使用反射操作,此时集合的泛型将不能产生约束,可以为集合存入其他任意类型的元素的。泛型只是在编译阶段可以约束集合只能操作某种数据类型,在编译成 Class 文件进入运行阶段时其真实类型都是 ArrayList。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1.创建对象
ArrayList<Integer> list = new ArrayList<>();
// 尝试添加
list.add(666);
// list.add("Cyan"); // 报错
// 2.获取类对象
Class listClass = list.getClass();
// 3.获取add方法
Method add = listClass.getDeclaredMethod("add", Object.class);
// 4.添加其他类型元素
add.invoke(list,"Cyan");
add.invoke(list,999);
// 5.打印查看数据是否添加
System.out.println(list);

// 打印结果
// [666, Cyan, 999]

可以看到外面不单单存在 Integer 类型数据,还存在它所不允许的 String 类型数据,跳过了泛型对我们的约束作用。

通用框架的底层原理

给你任意一个对象,在不清楚对象字段的情况,把对象的字段名和对应值存储到文件

  1. 接受任意对象
  2. 使用反射获取对象的Class类对象,获取全部成员变量信息
  3. 遍历成员变量得到它们在对象中具体值,然后存入文件