深入理解 Java 中的反射机制

一. 什么是反射

在理解反射这个概念之前,我们先来理解 Java 中的 “正射”。

在编写代码时,如果需要使用某一个类,必定先会去了解这是一个什么类,用来做什么,有怎么样的功能。之后才会对这个类进行实例化,以及使用对应的实例化对象进行下一步的操作。

而反射则是在代码一开始编写时不知道要初始化的类是什么,自然也无法使用 new 关键字来创建对象。

它的定义如下:

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

与编译期的静态行为相对,所有的静态型操作都在编译期完成,而反射的所有行为基本都是在运行时进行的。反射让 Java 有了动态特性,可以让程序更加灵活强大。

二. 反射的常用方法

(1)获取反射中的 class 对象

在反射中,要获取一个类或调用一个类的方法,首先必须要获取到该类的对象,在 Java API 中,获取 Class 类对象有以下三种方法:

  • Class.forName("类的路径");
Class clazz = Class.forName("com.test.domain.Person");
  • 利用已有类对象的 getClass() 方法;
Person person = new Person();
Class clazz = person.getClass();
  • 对于在编译前就已经知道的类,使用 .class 属性;
Class clazz = Person.class;

(2)通过反射创建类对象

通过反射建立类的对象,Java API 提供了两种方式:

  • 通过 class 对象的 newInstance() 方法;
Class clazz = Class.forName("com.test.domain.Person"); 
Person person = (Person) clazz.newInstance();
  • 通过 Constructor 对象的 newInstance() 方法;
Class clazz = Class.forName("com.test.domain.Person");
Constructor con = clazz.getConstructor();
Person person = (Person)con.newInstance();

(3)通过反射操作成员变量

  • 获取所有成员变量
// 获取所有成员变量(非私有)
Field[] fields = clazz.getFields();
Arrays.stream(fields).map(x -> x.getName()).forEach(System.out::println);
// 获取所有成员变量(私有)
Field[] fields1 = clazz.getDeclaredFields();
Arrays.stream(fields1).map(x -> x.getName()).forEach(System.out::println);
  • 获取单个成员变量
// 获取单个成员变量(非私有)
Field field = clazz.getField("name");
System.out.println("单个成员变量(非私有):" + field.getName());
// 获取单个成员变量(私有)
Field field1 = clazz.getDeclaredField("address");
field1.setAccessible(true);
field1.set(person, "小张");
System.out.println("单个成员变量(私有):name=" + field1.getName() + ",value = " + field1.get(person));

当成员变量为 private 时,无法直接使用 set() 方法修改它的值,此时应该使用 setAccessible() 方法取得访问权限。

(4)通过反射操作成员方法

  • 获取所有成员方法
// 获取 Class 类的成员方法(不包括私有方法)
Method[] methods = clazz2.getMethods();
Arrays.stream(methods).map(x -> x.getName()).forEach(System.out::println);
// 获取 Class 类的成员方法(包括私有方法)
Method[] methods1 = clazz2.getDeclaredMethods();
Arrays.stream(methods1).map(x -> x.getName()).forEach(System.out::println);
  • 获取单个成员方法
Class clazz = Class.forName("code.Person");
Person person = (Person)clazz.newInstance();
// 获取 Class 类的单个成员方法(非私有)
Method method = clazz.getMethod("setName", String.class);
method.invoke(person,"小王");
// 获取 Class 类的单个成员方法(私有)
Method method1 = clazz.getDeclaredMethod("printPersonInfo");
method1.setAccessible(true);
method1.invoke(person);

操作方法与操作变量相差不大,在获取到对应方法之后使用 invoke() 方法执行即可。同理,遇见私有方法时,也需要使用 setAccessible(true) 方法获取访问权限。

三. 反射的实现原理

正常的类加载过程为:

1. new 一个对象时,触发 JVM 加载对应类的 .class 文件;

2. JVM 从本地磁盘找到对应类的 .class 文件并加载到 JVM 内存中;

3. JVM 将文件加载到内存后,自动创建一个 Class 对象;

反射的实现过程如下: 

1. 通过类的全限定名,获取这个类的字节码 .class 文件,从而获取到这个类对应的 Class 对象;

2. 通过 Class 对象提供的方法,结合类 Method,Field,Constructor,获取这个类的所有相关信息.;

3. 获取到信息后,就可以对当前对象进行下一步的操作了,比如:

  • 使用 Constructor 创建对象;
  • 用 get 和 set 方法读取和修改与 Field 对象相关的字段;
  • 用 invoke 方法调用与 Method 对象关联的方法。

四. 反射的弊端

1. 性能

反射包括了一些动态类型,所以 JVM 无法对这些代码进行优化。

因此,反射操作的效率要比非反射操作低得多。我们应该避免在经常被执行的代码和对性能要求很高的程序中使用反射。

2. 安全

使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。

3. 内部暴露

由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用。

反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。

4. 缺少类型检查 

丧失了编译时类型检查的好处,包括异常检查。如果程序企图用反射去调用不存在或者不可访问方法,运行时将会失败。

5. 代码可读性差

从代码规范的角度来说,执行反射访问所需要的代码非常笨拙和冗长,可读性差。

 五. 反射的应用

核心反射机制最初是为了基于组件的应用创建工具而设计的,如 Spring。

这类工具通常需要装载类,并且用反射功能找出它们支持哪些方法和构造器,允许用户交互式地构建访问这些类的应用程序。

反射功能只是在应用程序设计阶段被用到,通常来说,普通应用程序运行时不应该以反射方式访问对象。


本篇博客参考了以下资料,非常感谢:

https://www.cnblogs.com/jojop/p/10668239.html

https://mp.weixin.qq.com/s/D94zyd6JSjITDgR2UxeyzQ

https://blog.csdn.net/a745233700/article/details/82893076

Guess you like

Origin blog.csdn.net/j1231230/article/details/119966519