Java 中的反射是什么

反射是一个程序语言用来自检和修改自身结构和行为的过程。

这么说可能有些晦涩,反射可以理解为在程序运行过程中,获取程序的结构信息,从而根据不同的情况来调整程序的行为。

在使用汇编语言编程的时代,汇编语言可以直接获取任何信息,可以认为汇编语言天然就支持反射。但是到后来的一些高级语言,比如 Fortran 和 C 语言等等,由于不支持反射,也就无法获取信息。

再到后续的 Java,C# 等语言中都自带了反射框架,这些语言就可以通过反射来实现通用的框架,比如在 Java 的 Spring 中就大量使用了反射。

这篇文章重点介绍 Java 中的反射。

本文基于 OpenJDK11

1. 反射能干什么

在 Java 中,反射可以获取类的定义、属性和方法,并且可以调用这些方法,或者构造对象,甚至可以在运行时修改类的定义。

先来看一个简单的例子,通过这个例子可以知道反射能做什么,例子中的代码下面会详细讲解:

1
2
3
4
5
6
7
8
9
10
11
// 利用反射来执行方法
public Object methodInvoke(Object o, Method method, Object[] params) {
try {
return method.invoke(o, params);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}

使用反射来生成 ArrayList 对象并调用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取 ArrayList 的 Class 对象和构造函数,并生成对象
Class clazz = ArrayList.class;
Constructor<ArrayList> constructor = clazz.getConstructor();
ArrayList instance = constructor.newInstance();

// 获取 add 方法,并调用 add 方法添加元素
Method add = clazz.getDeclaredMethod("add", Object.class);
Object[] params = {"reflection"};
methodInvoke(instance, add, params);

// 获取 get 方法,并调用 get 方法获取刚刚添加的元素
Method get = clazz.getDeclaredMethod("get", int.class);
Object[] params1 = {0};
System.out.println(methodInvoke(instance, get, params1));

如果不使用反射,这个代码很简单:

1
2
3
ArrayList list = new ArrayList();
list.add("reflection");
System.out.println(list.get(0));

反射这么麻烦,为什么还要使用它?

观察一下上面利用反射来执行方法的代码可以发现,执行任何类的方法,那段代码都不需要修改,而且更重要的是,具体执行的方法还可以在程序运行的过程中改变

反射用起来虽然麻烦,但是反射的动态性提高了 Java 的灵活性,很多框架都依赖反射。

2. 相关概念

在 Java 中,有很多的类,每个类都有自己的属性,方法,构造函数等等信息,这些信息都需要通过一种方式表示,在 Java 中就是通过 Class 对象来表示。

这里不要把 Class 与 Object 混在一起,Object 是所有类的父类,而 Class 类则不一样,在每个类中,都有一个 Class 对象来保存类的信息。即使对于 Java 中的原生数据类型,也有自己的 class 对象。

1
Class clazz = int.class;

每个 Class 对象中都有主要有三个主要部分:Field、Method 和 Constructor。这些和反射相关的类都在 java.lang.reflect 包下。

Method 和 Constructor 在上面的代码中都看到了,分别表示类的方法和构造函数。Field 则表示类的属性。

除了上面这些信息外,还有其他的信息,比如判断属性或者方法是否能访问,获取类、方法、属性的注解等等:

1
2
Method add = clazz.getDeclaredMethod("add", Object.class);
add.isAccessible(); // 判断是否能访问

即使对于私有方法或者属性,也可以通过下面的方式来访问:

1
2
Method add = clazz.getDeclaredMethod("add", Object.class);
add.setAccessible(true); // 通过这个设置,即使私有方法也能访问

在 Java 中,注解用的很多,实际上,注解本身并没有包含额外的内容,只是起到标记的作用,所有的注解都可以通过反射来获取,然后再进行相应的逻辑处理。

1
2
Method add = clazz.getDeclaredMethod("add", Object.class);
add.getDeclaredAnnotations(); // 获取该方法所有的注解

还可以获取方法的参数,返回值的类型等等信息。

所以总的来说,反射是一个框架,可以帮助获取程序内部的信息,至于获取这些信息来做什么,就看开发人员的发挥。

反射虽然很强大,但是性能上相比于普通方式性能上难免会有一些损失,需要注意,在程序执行的过程中,需要尽量少用反射。而且反射连私有方法都可以访问,如果滥用就可能造成程序的不安全。

3. 反射的基本用法

使用行业的第一步,就是获取目标类型的 Class 对象,如果获取不到,那就说明目标类型无法完成类加载,也就不能进行后续的反射操作。

获取 Class 对象的方法

有 4 种方法可以获取 Class 对象。

通过类的 class 属性:

1
Class clazz1 = String.class;

通过 Class.forName() 方法:

1
Class clazz2 = Class.forName("java.lang.String");

通过对象的 getClass() 方法:

1
Class clazz3 = "string".getClass();

对于基本类型的包装类,还有一种方法可以获取 Class 对象:

1
Class clazz4 = Integer.TYPE;

创建对象

在获取到 Class 对象之后就可以创建对象了,创建对象有两种方式。

通过 Class 对象直接生成:

1
2
Class clazz = String.class;
String str = (String) clazz.newInstance();

通过构造函数生成:

1
2
Constructor<String> constructor = clazz.getConstructor();
String str1 = constructor.newInstance();

如果构造函数有参数:

1
2
Constructor<String> constructor2 = clazz.getConstructor(String.class);
String str2 = constructor2.newInstance("String");

调用方法

在生成了对象之后,自然就可以调用方法,调用的方式和上面的代码一样。

首先获取 Method,如果方法没有参数,直接获取执行就可以:

1
2
Method lengthMethod = clazz.getMethod("length");
lengthMethod.invoke(str2);

如果有参数,在获取方法的时候就需要声明方法的类型和个数,而且参数的顺序不能乱:

1
2
Method substringMethod = clazz.getMethod("substring", int.class, int.class);
substringMethod.invoke(str2, 1,2);

其他工具方法

上面是反射的一些基本用法,当然反射也提供了其他的一些工具方法。

获取所有已经声明的属性、方法和构造函数,包括 private 的也会被获取出来:

1
2
3
Field[] fields = clazz.getDeclaredFields();
Method[] methods = clazz.getDeclaredMethods();
Constructor[] constructors = clazz.getDeclaredConstructors();

判断是否是内部类:

1
clazz.isMemberClass();

还有其他判断是否接口、注解,获取类的泛型信息等等方法。

文 / Rayjun

REF

[1] https://en.wikipedia.org/wiki/Reflection_(computer_programming)

© 2020 Rayjun    PowerBy Hexo