oynix

于无声处听惊雷,于无色处见繁花

Java之反射

反射,是Java的一种高级特性。就我而言,实际开发中很少遇到用反射的需求,但这并不能否定反射的重要地位,这篇文章来简单说一说。

前言

在Java里,有一个类,名字叫Class,这个名字总是容易给人一种这是个特殊的类。其实,和其他类一样,这也是个普通的类,在java.lang包下,lang就是language的简写,里面都是和Java语言本身相关的东西。这个类里面包含了一个类的所有信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 省略了一些代码
package java.lang

public final class Class<T> {

// 这里只列举了一部分
String name; // 类名
ClassLoader classLoader; // 类加载器
String packageName; // 包名
Class<?>[] interfaces; // 所实现的接口
Constructor<?>[] contructors; // 构造方法
Field[] fields; // 字段
Method[] methods; // 方法
int modifiers; // 类修饰符

private Class(ClassLoader loader, Class<?> arrayComponentType) {
this.classLoader = loader;
this.componentType = arrayComponentType;
}
}

在JVM首次调用到一个类的时候,会先将这个类加载到JVM里,这个过程就是在为这个类创建Class实例,且JVM只会为每个类创建一个Class实例,如果存在则在直接使用。可以看到,Class只有一个构造方法,而且是私有的,只有JVM可以调用。

那么,怎么获取到JVM创建的Class实例呢?一共有3种方式:

1
2
3
4
5
6
7
8
9
10
// 方式1:通过类的静态变量class
Class clz = String.class;

// 方式2:通过实例的方法
String str = "str";
Class clz = str.getClass();

// 方式3:通过Class的静态方法Class.forName
// Class.forName有3个重载方法,可以传入不同的限定名称
Class clz = Class.forName("java.lang.String");

通过类的Class实例,我们可以创建类的实例,调用类的所有成员变量、和成员方法,包括私有的。

方法概览

通过反射,一般有三个目的:获取构造方法Constructor来创建实例、获取方法Method来调用,以及获取字段Field来set/get值。除此之外,还可以获取其他信息,如注解,来完成额外的操作,这些在这先不讨论。

构造方法 字段 方法
单个public getConstructor() getField() getMethod()
所有public getConstructors() getFields() getMethods()
类里任一 getDeclaredConstructor() getDeclaredField() getDeclaredMethod()
类里所有 getDeclaredConstructors() getDeclaredFields() getDeclaredMethods()
  • 不加s是获取单个,加s是获取所有
  • 不加declared的,是获取public的,protected都不行,必须public,可以是自己的,也可以是继承的
  • 加declared的,是获取类里面自己声明的,只能是写在类里的成员,不能是继承的,公开的、私有的都可以获取

示例类

创建了一个示例接口和一个示例类,很简单,看一眼就可以跳过。

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
// Person接口
public interface Person {
String getName();
void sayHi(String toWho);
}

// Student实现类
public class Student implements Person {

// 一个私有变量 一个公开变量
private int sex;
public int score;

// 一个无参数构造方法 一个有参数的构造方法
public Student() {
this.score = 0;
this.sex = 1;
}

public Student(int score, int sex) {
this.score = score;
this.sex = sex;
}

// 私有构造方法
private Student(int score) {
this.score = score;
this.sex = 2;
}

// 继承过来的方法
@Override
public String getName() { return "student"; }

@Override
public void sayHi(String toWho) {
System.out.println("student say hi to :" + toWho);
}

// 一个私有方法
private void setSex(int sex) { this.sex = sex; }

public int getSex() { return sex; }
}

通过反射获取构造方法

  • 获取无参构造方法
    1
    2
    3
    4
    5
    // 获取Class实例
    Class<Student> clz = Student.class;

    Constructor<Student> cst = clz.getConstructor();
    System.out.println(cst.newInstance().score); // 缺省值 0
  • 获取有参数的构造方法
    1
    2
    3
    4
    Class<Student> clz = Student.class;

    Constructor<Student> cst = clz.getConstructor(int.class, int.class);
    System.out.println(cst.newInstance(80, 1).score); // 传入值 80
  • 获取私有的构造方法
    1
    2
    3
    4
    5
    6
    Class<Student> clz = Student.class;

    // 这里必须用加declared的,因为带一个参数的构造方法是私有的
    Constructor<Student> ctr3 = clz.getDeclaredConstructor(int.class);
    ctr3.setAccessible(true); // 使用私有的成员前,需要先设置为可访问
    System.out.println(ctr3.newInstance(55).score);

通过反射获取字段

字段,Field,即类里面声明的成员变量。像上面,通过Class实例获取到类的构造函数,可以用其创建类的实例,那获取到字段有什么用呢?用字段可以获取其对应的值,所以,就需要一个类的实例,有了实例,才能获取。

  • 获取公开字段
    1
    2
    3
    4
    5
    6
    7
    Class<Student> clz = Student.class;

    Field score = clz.getField("score"); // 通过字段名字获取
    // 使用字段,需要一个Student实例
    Student student = new Student(60, 1);
    // 这句话的意思就是,获取student实例中score字段的值
    System.out.println(score.get(student)); // 60
  • 获取私有字段
    1
    2
    3
    4
    5
    6
    7
    Class<Student> clz = Student.class;
    Student student = new Student(60, 1);

    Field sex = clz.getDeclaredField("sex"); // 这里必须加declared 通过字段名字获取
    sex.setAccessible(true); // 设置可访问
    sex.set(student, 10); // 先设置为10
    System.out.println(sex.get(student)); // 10

通过反射获取方法

字段可通过名字获取,但方法不可只通过名字,因为会存在方法重载,即几个方法名字一样,但参数类型或者数量不同,所以要获取方法,就需要指定方法名称,同时和方法的参数的类型列表。和字段相同的是,方法在调用的时候,也需要一个实例,有了实例,才能在这个实例上调用其方法。

  • 获取公开方法
    1
    2
    3
    4
    5
    Class<Student> clz = Student.class;
    Student student = new Student(60, 1);

    Method sayHi = clz.getMethod("sayHi", String.class); // 指明方法名,和参数类型
    sayHi.invoke(student, "Teacher"); // student say hi to :Teacher
  • 获取私有方法
    1
    2
    3
    4
    5
    6
    7
    Class<Student> clz = Student.class;
    Student student = new Student(60, 1);

    Method setSex = clz.getDeclaredMethod("setSex", int.class); // 指明方法名,和参数类型
    setSex.setAccessible(true); // setSex是私有的,先设置为可访问
    setSex.invoke(student, 100); // 调用,设置sex的值
    System.out.println(student.getSex()); // 100

总结

通过上面的例子,可以看出:反射都是基于类的Class实例,通过里面的成员,来进行一些操作的。先获取到Class实例,再获取对应的成员变量,比如字段、方法等,然后再利用这些成员,采取一些操作来达成目的。

没有太多代码,就不创建项目了,附上所有代码:

------------- (完) -------------
  • 本文作者: oynix
  • 本文链接: https://oynix.com/2021/09/c1853a000a6e/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

欢迎关注我的其它发布渠道