由于在学习框架时,经常会遇到反射,故此篇文章用于对反射的基本学习。

一、概述

基本定义:JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

对其简单的理解就是将类的各个组成部分封装为其他对象,以便我们能够更加细化的使用。同时,我们也都知道,Java中程序是运行在虚拟机中,我们平常用文本编辑器或者是IDE编写的程序都是.java格式的文件,这是最基础的源码,但这类文件是不能直接运行的,必须经过编译成.class字节码文件进而加载进内存供JVM虚拟机执行。要想理解反射,就先需要谈起Java代码在计算机中经历的三个阶段,见下图。

java代码经历的三个阶段

第一个阶段 – 源代码阶段

定义一个了类,继而将一个类进行编译.class文件。

在第一阶段中,可以通过Class.forName("全类名")的方式获取Class类对象。

第二阶段 – 加载进内存阶段(Class对象阶段)

类加载器(ClassLoader):负责将字节码文件加载进内存。

在内存中需要由一个对象来描述加载进内存中这个Class文件 – Class类对象。

Class类对象的基本组成:

  • 成员变量 封装成Field[] fields对象
  • 构造方法 Constructor[] cons
  • 成员方法 Method[] methods

继而通过Class类对象创建真正的对象,供我们使用。

这一阶段可以通过类名.class的方式获取到Class类对象。

第三阶段 – Runtime阶段

通过类的实例化创建出对象。Runtime运行阶段 new 出类的实例化对象。

通过对象.getClass()方式获取到Class类对象。

综上上面的三个阶段对Class类对象的获取:同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个(内存中的位置同一个,本质就是一个class文件)。

好处:

  1. 可以在程序运行过程中,操作这些对象。(例如:获取一个对象的方法,将一个类进行拆解,对其各个组成部分进行操作,获取到一个类对象在内存中的状态)
  2. 可以解耦,提高程序的可扩展性(在不改变原有代码的基础上,对功能进行增强实现)。

二、具体使用

Java反射相关的类:

类名 用途
Class类 代表类的实体,在运行的Java应用程序中表示类和接口
Field类 代表类的成员变量(成员变量也称为类的属性)
Method类 代表类的方法
Constructor类 代表类的构造方法

2.1 Class对象功能

获取成员变量们 获取构造方法们 获取成员方法们 获取全类名
Field[] getFields() Constructor<?>[] getConstructors() Method[] getMethods() String getName()
Field getField(String name) Constructor getConstructor(类<?>… parameterTypes) Method getMethod(String name, 类<?>… parameterTypes)
Field[] getDeclaredFields() Constructor getDeclaredConstructor(类<?>… parameterTypes) Method[] getDeclaredMethods()
Field getDeclaredField(String name) Constructor<?>[] getDeclaredConstructors() Method getDeclaredMethod(String name, 类<?>… parameterTypes)

其中*Declared*代表忽视权限修饰符的安全检查,可以获取一切权限的(成员变量、构造方法,成员方法);而上方的只能获取到public修饰的(成员变量,构造方法,成员方法)。

在忽视权限修饰符的同时,如果想对其成员变量\构造方法\成员方法使用等,可采用暴力反射(不推荐,降低了安全机制)。

setAccessible(true):暴力反射 – 忽略访问权限修饰符的安全检查

  1. 成员变量Field

    1.1 设置值

    • void set(Object obj, Object value)

    1.2 获取值

    • get(Object obj)
  2. 构造方法Constructor

    2.1 创建对象

    • T newInstance(Object... initargs) – 非空参时,获取到Class类对象的对应的构造方法,再填入对应参数。
    • 如果使用空参数构造方法创建对象,操作可以简化:Class对象的newInstance方法。例如:Object obj = cls.newInstance
  3. 方法对象 – Method

    3.1 执行方法

    • Object invoke(Object obj, Object... args)

    3.2 获取方法名称

    • String getName:获取方法名
  4. ClassLoader

    class类对象.getClassLoader得到类加载器对象(获取到这个字节码文件对应的类加载器),负责将这个类加载进行内存。ClassLoader对象可以获取到内存中当前类路径下的文件信息。

三、代码实操

首先定义一个实体类:

Student实体类
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class Student implements Serializable {

private String name;
private String age;

public Student() {
}

public Student(String name, String age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getAge() {
return age;
}

public void setAge(String age) {
this.age = age;
}

public void eat() {
System.out.println("eat...");
}

public void eat(String food) {
System.out.println("eat.." + food + "...");
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age='" + age + '\'' +
'}';
}
}

以上的Student实体类中,包含:

  • 成员变量:nameage
  • 构造方法:
    • public Student()
    • public Student(String name, String age)
  • 成员方法:gettersetterpublic void eat()public void eat(String food)

3.1 反射基本方法使用

3.1.1 各阶段对Class对象的获取
1
2
3
4
5
6
7
// 1.通过全类名获取class对象 -- 源码阶段
Class cls = Class.forName("cn.lizhi.domain.Student");
// 2.通过类名的方式获取class对象 -- 加载进行内存后
Class cls1 = Student.class;
// 3.类的实例化对象获取class对象 -- Runtime阶段
Student student = new Student();
Class cls2 = student.getClass();

其中clscls1cls2为同一个对象,内存地址值相等。

3.1.2 Class对象功能使用 – 成员属性实例
1
2
3
4
5
6
7
8
9
10
Student student = new Student();
Field[] fields = cls.getDeclaredFields(); // 获取全部的成员属性
for (Field field : fields) {
System.out.println(field);
}
Field name = cls.getDeclaredField("name"); // 获取指定的成员属性
name.setAccessible(true); // 暴力反射
name.set(student, "Tom"); // 设置属性值(可以不改变原代码)
String value_name = (String) name1.get(student); // Tom
System.out.println(student);
3.1.3 Class对象功能使用 – 创建对象
1
2
3
4
5
6
// 有参构造方法
Constructor cs = cls.getDeclaredConstructor(String.class, String.class);
Object o = cs.newInstance("张三", "6"); // 通过获取构造方法创建对象
System.out.println(o);
// 空参构造方法
Object o1 = cls.newInstance();
3.1.4 Class对象功能使用 – 方法调用
1
2
Method eat = cls.getMethod("eat", String.class); // 确定带参的eat()方法
eat.invoke(o, "food"); // 传入对象与参数,执行方法

3.2 实例

需求:不改变该类的任何代码的前提下,可以帮我们创建任意类的对象,并且执行其中任意方法。

  1. 定义一个配置文件,配置文件中配置类名、方法名

    1
    2
    className=cn.lizhi.domain.Student
    methodName=eat
  2. 代码编写

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public static void main(String[] args) throws Exception {
    Properties pro = new Properties();
    // 类加载器 ClassLoader负责将这个类加载进内存
    InputStream is = InflectDemo01.class.getClassLoader().getResourceAsStream("pro.properties");
    pro.load(is);
    String className = pro.getProperty("className");
    String methodName = pro.getProperty("methodName");
    Class cls = Class.forName(className); // 获取class对象
    Object obj = cls.newInstance(); // 创建对象
    Method method = cls.getMethod(methodName,String.class); // 加载重载的方法
    method.invoke(obj,"fish"); // 执行方法
    }

    当我们需要创建其它类的对象和执行它的方法时,我们只需要修改配置文件即可,方便我们的解耦开发。

参考文献

[1] Java高级特性——反射

[2] 百度百科

[3] 黑马讲义


Comment