Java后端学习路线β阶段--Java反射

101 阅读8分钟

Java后端学习路线,码云仓库地址:gitee.com/qinstudy/ja…

1、什么是反射?

大榜:我们终于走完了Java后端学习路线的第一个阶段,有个很深的体会就是:要想将知识点清晰明白地讲出来,真心不容易啊。

小汪:是滴了,我们还需要好好打磨。咱们一起讨论完了第一个阶段,接下来干什么呢?

大榜:接下来咱们进入第二阶段(β阶段)的学习。按照β阶段的计划,我们讨论Java反射,如何?

小汪:反射,我经常听说,但很少使用反射。我记得Class类、java.lang.reflect包 一起对反射提供了支持。

大榜:是滴了。反射是Java的特征之一,反射允许运行中的Java程序获取自身的信息,并且可以操作类或对象的属性和方法。

说人话,就是:通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。程序中一般对象的类型都是在编译期就确定下来的,而 Java 反射机制可以动态地创建对象并调用其属性,对象的类型在编译期是未知的。

小汪:简单来说,反射的核心是 JVM 在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。反射是发生 在运行时而不是编译时。那什么时候该使用反射呢?

2、为什么要有反射?

大榜:Java语言有反射机制,其最大目的就是为了提高程序的灵活性。很多人都认为反射在实际的 Java 开发应用中并不广泛,其实不然。当我们在使用 IDE(如 Eclipse,IDEA)时,当我们输入一个对象或类并想调用它的属性或方法时,一按点号,编译器就会自动列出它的属性或方法,这里就会用到反射。

小汪:我记得,很多Java相关的框架都使用了反射,比如最流行的Spring框架,开发Spring框架的大佬们,也要用到反射。

大榜:反射最重要的用途就是开发各种通用框架。很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 Bean),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射,运行时动态加载需要加载的对象。

小汪:榜哥,能举个案例说明下吗?

大榜:那我们设想一个反射的使用场景,比如老板给了你一个需求,需求是这样的:

老板要你编写一个程序,要使用一个类的一个方法,但是这个类不是一个固定的类,可能是Dog类,也可能是任何一个类,里面的方法当然也是跟着类一起动态变化的。这个类的全类名、类的方法签名是写在配置文件中,配置文件的内容是这样的:

# 配置项,用于测试Java反射的invoke方法
preClass = com.programming.logic.p21.Dog
preMethod = say

你如何实现配置文件中这个类的加载,还有方法的调用呢?

小汪:我们平时创建类的实例对象是直接使用关键词 new 来创建的,但这是基于我们知道自己要使用的是哪个类,就是知道类的名字、类的相关信息,比如我们要使用Dog类,我们知道它的名字是Dog,直接 new 一个Dog类就好了。但现在的需求是:根据配置文件中配置的类名称来创建对象,此时我们并没有类的相关信息,那就应该使用反射,通过反射来动态创建类的实例对象、类的实例对象的方法。

大榜:思路没问题。如果我们要使用反射功能,需要使用Class类 和 java.lang.reflect 包,它们是反射的API,java.lang.reflect 包主要包含了以下三个类:

Field :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
Method :可以使用 invoke() 方法调用与 Method 对象关联的方法;
Constructor :可以用 Constructor 的 newInstance() 创建新的对象。

小汪:顾名思义,Field表示属性,包含了静态变量和实例变量;Method表示方法,包含了静态方法和实例方法。Constructor表示构造器,我们可以使用newInstance() 创建实例对象。那到底该如何使用反射,来动态创建实例对象,并调用方法呢?

大榜:首先我们读取配置文件,获取配置项的值className;然后通过反射获取Class对象,代码如下:

Properties prop = new Properties();
// 通过类加载器,将配置文件加载至内存
ClassLoader classLoader = PropertiesDemo.class.getClassLoader();
InputStream inputStream = classLoader.getResourceAsStream("config.properties");
prop.load(inputStream);
​
// 读取类名称、方法名称
String className = prop.getProperty("preClass");
String methodName = prop.getProperty("preMethod");
​
// 根据类名称,创建clazz对象
Class<?> clazz = Class.forName(className);

接着,我们根据得到的clazz对象,创建实例对象myObject,这样就实现了通过反射动态创建实例对象;进一步调用invoke方法,来执行实例对象中的方法,代码是这样的:

// 根据clazz对象,创建 实例对象myObject
Object myObject = clazz.newInstance();
// 根据clazz对象和方法名称,获取 方法对象myMethod
Method noParameterMethod = clazz.getMethod(methodName);
​
System.out.println(myObject);
System.out.println("无参的方法" + noParameterMethod);

完整的示例代码如下(读取配置文件,使用反射来动态创建实例对象,并调用方法):

package com.programming.logic.p21;
​
import com.programming.logic.p14.PropertiesDemo;
​
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Properties;
​
/**
 * Java反射入门,参考链接:https://blog.csdn.net/tblwbr520/article/details/121887396
 *
 * @author qinxubang
 * @Date 2022/6/9 8:46
 */
public class ReflectionTest {
​
    public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException,
            InstantiationException, NoSuchMethodException, InvocationTargetException {
​
        Properties prop = new Properties();
        // 通过类加载器,将配置文件加载至内存
        ClassLoader classLoader = PropertiesDemo.class.getClassLoader();
        InputStream inputStream = classLoader.getResourceAsStream("config.properties");
        prop.load(inputStream);
​
        // 读取类名称、方法名称
        String className = prop.getProperty("preClass");
        String methodName = prop.getProperty("preMethod");
​
        // 根据类名称,创建clazz对象
        Class<?> clazz = Class.forName(className);
​
        // 根据clazz对象,创建 实例对象myObject
        Object myObject = clazz.newInstance();
        // 根据clazz对象和方法名称,获取 方法对象myMethod
        Method noParameterMethod = clazz.getMethod(methodName);
​
        System.out.println(myObject);
        System.out.println("无参的方法" + noParameterMethod);
​
        noParameterMethod.invoke(myObject);
​
        Method oneParameterMethod = clazz.getMethod(methodName, String.class);
        System.out.println("一个参数的方法" + oneParameterMethod);
        // invoke方法中,第一个参数是 执行的对象,第二个参数是执行方法的入参,可以有多个入参。
        // oneParameterMethod.invoke(myObject, "A"); 具体来说,是执行该方法:dog.say("A");
        oneParameterMethod.invoke(myObject, "A");
    }
}
​
/**
 * 定义Dog类
 */
class Dog {
    private String name = "哮天犬";
​
    public Dog() {
    }
​
    public Dog(String name) {
        this.name = name;
    }
​
    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + ''' +
                '}';
    }
​
    public void say(){
        System.out.println("汪,汪汪~~~");
    }
​
    public void say(int num) {
        for (int i = 1; i <= num; i++) {
            System.out.println("汪,汪汪~~~" + i);
        }
    }
​
    public void say(String numHexStr) {
        // 将十六进制的numHexStr,转成十进制的数字num
        int num = Integer.parseInt(numHexStr, 16);
​
        for (int i = 1; i <= num; i++) {
            System.out.println("汪,汪汪~~~" + i);
        }
    }
​
    public void eat(){
        System.out.println("吃饭~~~");
    }
}

小汪:我测试了下,上面程序的输出结果为:

Dog{name='哮天犬'}
无参的方法public void com.programming.logic.p21.Dog.say()
汪,汪汪~~~
一个参数的方法public void com.programming.logic.p21.Dog.say(java.lang.String)
汪,汪汪~~~1
汪,汪汪~~~2
汪,汪汪~~~3
汪,汪汪~~~4
汪,汪汪~~~5
汪,汪汪~~~6
汪,汪汪~~~7
汪,汪汪~~~8
汪,汪汪~~~9
汪,汪汪~~~10

如果我们将配置文件的配置项,修改为Cat类和对应的eat方法,如下:

# 配置项,用于测试Java反射的invoke方法
preClass = com.programming.logic.p21.Cat
preMethod = eat

上面的代码完全不需要改动,这么一想,反射确实很灵活啊!

3、反射的实战

大榜:上面介绍了反射中的Method类,还有Field、Constructor的用法,可以参考下面这篇文章:获取 Class 对象的四种方式、反射的一些基本操作

小汪:我动手敲了下这篇文章中案例,挺入门的。

4、反射的踩坑点

大榜:我感觉反射有点抽象,我们需要多练习才能掌握反射的基础。前几天练习时,遇到了调用invoke方法的一个坑,代码是这样的:

这是Dog类中的实例方法:

class Dog {
  public void say(int num) {
        for (int i = 1; i <= num; i++) {
            System.out.println("汪,汪汪~~~" + i);
        }
    }
}

这里使用反射,调用Dog类中的say(Integer num)方法,如下所示:

Method otherParameterMethod = clazz.getMethod(methodName, Integer.class);
System.out.println("另一个参数的方法 " + otherParameterMethod);
// invoke方法中,第一个参数是 执行的对象,第二个参数是执行方法的入参,可以有多个入参。
otherParameterMethod.invoke(myObject, 3);

运行上面的代码,输出为:

java.lang.NoSuchMethodException: com.programming.logic.p21.Dog.say(java.lang.Integer)

你看,执行invoke方法,抛出了NoSuchMethodException异常。

小汪:我帮你看看哈,你创建method对象,传入的参数是Integer.class,代码是这样的:

 Method otherParameterMethod = clazz.getMethod(methodName, Integer.class);
 otherParameterMethod.invoke(myObject, 3);

上面的invoke方法,最终调用的是say(int num)方法。由于say方法的入参是int类型,而otherParameterMethod为Integer类型,所以抛出了NoSuchMethodException异常。

大榜:你分析得很对!问题搞清楚了,修复问题就很简单了,我们只需要将Integer.class修改为int.class,即告诉JVM,创建的otherParameterMethod对象,入参是int,而不是Integer.class。修改后的代码是这样的:

 Method otherParameterMethod = clazz.getMethod(methodName, int.class);
 otherParameterMethod.invoke(myObject, 3);

小汪:那我们什么情况下该使用反射,什么情况不使用反射呢?

大榜:如果程序有特别高灵活性的需求,比如从配置文件中动态创建实例对象,并调用方法,这个时候我们就考虑到使用反射了,毕竟反射可以直接根据类名称来获取Class对象,进一步创建实例对象、属性、调用方法,功能强大。

反过来说,如果没有这样的需求,我们就没必要使用反射,因为反射性能要低一点,而且反射是发生在运行期间,编译器无法帮我们做类型检查。

5、总结

通过小汪和大榜的对话,一起回顾了Java反射,以一个需求(实现配置文件中这个类的加载、方法的调用)为切入点,讨论了为什么要有反射,然后以一个示例讲解了反射的基本使用,然后介绍了Method类的invoke方法的踩坑点。

6、参考内容

1)Java反射入门

2)反射机制详解

3)深入解析Java反射