この記事を読んだ後、私はJavaリフレクションが何であるかを知っています(1日1つの質問、一緒に大きな工場に入る)

一緒に書く習慣をつけましょう!「ナゲッツデイリーニュープラン・4月アップデートチャレンジ」に参加した初日です。クリックしてイベントの詳細をご覧ください

一日一問、一緒に大きな工場に入り、クリックして読んでください。「ビッグファクトリートゥギャザーシリーズへの参入」は、主にビッグファクトリーの面接の質問を徹底的に研究するために、私が準備し始めたばかりのものです。継続的に更新されるこのコラムに、どなたでもご注目ください。一緒に来て、一緒に大きな工場に入ります。それ以上の苦労なしに、今日のトピックに入りましょう。

image.png

最初の反射

インターネットでのJavaリフレクションを見るのは非常に曖昧ですが、今日は私の理解を通して詳細に話します。この記事はあなたが学習への自信を取り戻すのに役立つと信じています。

一言で言えば:リフレクションはJVMコンパイルフェーズをバイパスできます。コードは動的に追加できます。たとえば、オブジェクトが決定されていない場合、実行中のプロセス中に動的に決定できます。オブジェクトのメソッドは完全には実装されていません。メソッドを(部分的に)呼び出すこともできます。

非常に抽象的な?以下の例について話し終えたら、この概念をもう一度見て、力を与えられたと感じてください。

反射がある場合は、オルソフォトがあります。オルソフォトとは、コード内に新しいobject()を事前に作成することです。

Studentオブジェクトをインスタンス化する必要がある場合、コードは次のようになります。

Students user = new Students();
复制代码

ある日、そのようなシナリオ、つまり教師と学校の2つのオブジェクトをインスタンス化する必要があるという突然の要求がありました。コードのコンパイル段階では、オブジェクトを作成することが不確実であるため、動的に作成する必要があります。コードは次のようになります。


//动态创建实体
public <T> T getClass(String param) {
    Object T = null;
    if (param.equals("student")) {
        T = new Students();
    } else if (param.equals("teacher")) {
        T = new Teacher();
    } else if (param.equals("school")) {
        T = new School();
    }
    return (T) T;
}
复制代码

パラメータparamを渡して使用するエンティティを決定することで、プロジェクトの実行時にパラメータを動的に渡すことで、使用するエンティティを決定できます。これがデザインモードのファクトリモードに少し似ているかどうかを確認してください。シーンの1つであるアプリケーションでもあります。

したがって、この例で簡単に要約します

簡単な要約

コードを実行する前に、将来どのデータ構造が使用されるかはわかりません。プログラムの実行中にのみ、使用するデータクラスを決定でき、実行中にクラス情報を動的に取得してクラスメソッドを呼び出す反射ことができます。プログラムリフレクションを介してクラスインスタンスを構築することにより、コードは最終的に次のように進化します。

public <T> T getPoJo(String className) throws Exception {
    Class clazz = Class.forName(className);
    return (T) clazz.newInstance();
}
复制代码
  • 反射的思想:在程序运行过程中确定和解析数据类的类型

  • 反射的作用:对于在编译期无法确定使用哪个数据类的场景,通过反射可以在程序运行时构造出不同的数据类实例。

接下来我们再进行深入理解,看看如何使用!

反射的基本使用

Java 反射的主要组成部分有4个:

  • Class:任何运行在内存中的所有类都是该 Class 类的实例对象,每个 Class 类对象内部都包含了本来的所有信息。记着一句话,通过反射干任何事,先找 Class 准没错!

  • Field:描述一个类的属性,内部包含了该属性的所有信息,例如数据类型,属性名,访问修饰符······

  • Constructor:描述一个类的构造方法,内部包含了构造方法的所有信息,例如参数类型,参数名字,访问修饰符······

  • Method:描述一个类的所有方法(包括抽象方法),内部包含了该方法的所有信息,与Constructor类似,不同之处是 Method 拥有返回值类型信息,因为构造方法是没有返回值的。

在网上找到一张图,放在了下面,如果用到了反射,离不开这核心的4个类,只有去了解它们内部提供了哪些信息,有什么作用,运用它们的时候才能易如反掌。

image.png

我们在学习反射的基本使用时,我会用一个Students类作为模板进行说明,首先我们先来熟悉这个类的基本组成:属性,构造函数和方法

public class Students {
    public String name;
    public int age;
    private double weight; // 体重只有自己知道
    
    public Students() {}
    
    public Students(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public void getInfo() {
        System.out.print("["+ name + " 的年龄是:" + age + "]");
    }
}
复制代码

反射中的用法有非常非常多,常见的功能有以下这几个:

  • 在运行时获取一个类的 Class 对象
  • 在运行时构造一个类的实例化对象
  • 在运行时获取一个类的所有信息:变量、方法、构造器、注解

获取类的 Class 对象

在 Java 中,每一个类都会有专属于自己的 Class 对象,当我们编写完.java文件后,使用javac编译后,就会产生一个字节码文件.class,在字节码文件中包含类的所有信息,如属性,构造方法,方法······当字节码文件被装载进虚拟机执行时,会在内存中生成 Class 对象,它包含了该类内部的所有信息,在程序运行时可以获取这些信息。

获取 Class 对象的方法有3种:

  • 类名.class:这种获取方式只有在编译前已经声明了该类的类型才能获取到 Class 对象
Class clazz = Students.class;
复制代码
  • 实例.getClass():通过实例化对象获取该实例的 Class 对象
Students sp = new Students();
Class clazz = sp.getClass();
复制代码
  • Class.forName(className):通过类的全限定名获取该类的 Class 对象
Class clazz = Class.forName("com.bean.Students");
复制代码

构造类的实例化对象

通过反射构造一个类的实例方式有2种:

  • Class 对象调用newInstance()方法
Class clazz = Class.forName("com.bean.Students");
Students stu = (Students) clazz.newInstance();
stu.getInfo();
// [null 的年龄是:0]
复制代码

即使 Students 已经显式定义了构造方法,通过 newInstance() 创建的实例中,所有属性值都是对应类型的初始值,因为 newInstance() 构造实例会调用默认无参构造器。

  • Constructor 构造器调用newInstance()方法
Class clazz = Class.forName("com.bean.Students");
Constructor constructor = clazz.getConstructor(String.class, int.class);
constructor.setAccessible(true);
Students stu = (SmallPineapple) constructor.newInstance("苏世", 25);
stu.getInfo();
// [苏世 的年龄是:25]
复制代码

通过Class对象调用 newInstance() 会走默认无参构造方法,如果想通过显式构造方法构造实例,需要提前从Class中调用getConstructor()方法获取对应的构造器,通过构造器去实例化对象。

这些 API 是在开发当中最常遇到的,当然还有非常多重载的方法,本文由于篇幅原因,且如果每个方法都一一讲解,我们也记不住,所以用到的时候去类里面查找就已经足够了

获取一个类的所有信息

image.png

这里就不展开细讲了,大家用到的时候 可以根据该图查询即可,因为我们主要是讲面试,面试不会问这么细的。

通过反射调用方法

通过反射获取到某个 Method 类对象后,可以通过调用invoke方法执行。

  • invoke(Oject obj, Object... args):参数``1指定调用该方法的对象,参数2`是方法的参数列表值。

  • 如果调用的方法是静态方法,参数1只需要传入null,因为静态方法不与某个对象有关,只与某个类有关。

可以像下面这种做法,通过反射实例化一个对象,然后获取Method方法对象,调用invoke()指定Students的getInfo()方法。

Class clazz = Class.forName("com.bean.Students");
Constructor constructor = clazz.getConstructor(String.class, int.class);
constructor.setAccessible(true);
Students st = (Students) constructor.newInstance("苏世", 25);
Method method = clazz.getMethod("getInfo");
if (method != null) {
    method.invoke(st, null);
}
// [苏世的年龄是:25]
复制代码

反射的应用场景

通过上面的讲解,我们可以回味总结下:

反射常见的应用场景这里介绍3个:

  • Spring 实例化对象:当程序启动时,Spring 会读取配置文件applicationContext.xml并解析出里面所有的标签实例化到IOC容器中。

  • 反射 + 工厂模式:通过反射消除工厂中的多个分支,如果需要生产新的类,无需关注工厂类,工厂类可以应对各种新增的类,反射可以使得程序更加健壮。

  • JDBC连接数据库:使用JDBC连接数据库时,指定连接数据库的驱动类时用到反射加载驱动类

我们再对上述场景进行一一讲解

Spring 的 IOC 容器

在 Spring 中,经常会编写一个上下文配置文件applicationContext.xml,里面就是关于bean的配置,程序启动时会读取该 xml 文件,解析出所有的 <bean>标签,并实例化对象放入IOC容器中。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="stu" class="com.bean.Students">
        <constructor-arg type="java.lang.String" value="苏世"/>
        <constructor-arg type="int" value="25"/>
    </bean>
</beans>
复制代码

在定义好上面的文件后,通过ClassPathXmlApplicationContext加载该配置文件,程序启动时,Spring 会将该配置文件中的所有bean都实例化,放入 IOC 容器中,IOC 容器本质上就是一个工厂,通过该工厂传入 < bean > 标签的id属性获取到对应的实例。

public class Main {
    public static void main(String[] args) {
        ApplicationContext ac =
                new ClassPathXmlApplicationContext("applicationContext.xml");
        Students stud = (Students) ac.getBean("stu");
        stud.getInfo(); // [苏世年龄是:25]
    }
}
复制代码

Spring 在实例化对象的过程经过简化之后,可以理解为反射实例化对象的步骤:

  • 获取Class对象的构造器
  • 通过构造器调用 newInstance() 实例化对象

反射 + 抽象工厂模式

代码实例上述已经讲过 可以回去看下增加下印象

反射 + 抽象工厂的核心思想是:

在运行时通过参数传入不同子类的全限定名获取到不同的 Class 对象,调用 newInstance() 方法返回不同的子类。细心的读者会发现提到了子类这个概念,所以反射 + 抽象工厂模式,一般会用于有继承或者接口实现关系。

JDBC 加载数据库驱动类

在导入第三方库时,JVM不会主动去加载外部导入的类,而是等到真正使用时,才去加载需要的类,正是如此,我们可以在获取数据库连接时传入驱动类的全限定名,交给 JVM 加载该类。

public class DBConnectionUtil {
    /** 指定数据库的驱动类 */
    private static final String DRIVER_CLASS_NAME = "com.mysql.jdbc.Driver";
    
    public static Connection getConnection() {
        Connection conn = null;
        // 加载驱动类
        Class.forName(DRIVER_CLASS_NAME);
        // 获取数据库连接对象
        conn = DriverManager.getConnection("jdbc:mysql://···", "root", "root");
        return conn;
    }
}
复制代码

在我们开发 SpringBoot 项目时,会经常遇到这个类,但是可能习惯成自然了,就没多大在乎,我在这里给你们看看常见的application.yml中的数据库配置,我想你应该会恍然大悟吧。

image.png

这里的 driver-class-name,和我们一开始加载的类是不是觉得很相似,这是因为MySQL版本不同引起的驱动类不同,这体现使用反射的好处:不需要修改源码,仅加载配置文件就可以完成驱动类的替换

优缺点

  • 增加程序的灵活性:面对需求变更时,可以灵活地实例化不同对象

但是,有得必有失,一项技术不可能只有优点没有缺点,反射也有两个比较隐晦的缺点

  • 破坏类的封装性:可以强制访问 private 修饰的信息
  • 性能损耗:反射相比直接实例化对象、调用方法、访问变量,中间需要非常多的检查步骤和解析步骤,JVM无法对它们优化。

反射总结

  • 反射的思想:反射就像是一面镜子一样,在运行时才看到自己是谁,可获取到自己的信息,甚至实例化对象。

  • 反射的作用:在运行时才确定实例化对象,使程序更加健壮,面对需求变更时,可以最大程度地做到不修改程序源码应对不同的场景,实例化不同类型的对象。

  • 反射的应用场景常见的有3个:Spring的 IOC 容器,反射+工厂模式 使工厂类更稳定,JDBC连接数据库时加载驱动类

  • リフレクションの3つの特性:プログラムの柔軟性の向上、クラスのカプセル化の破棄、およびパフォーマンスの低下

さて、これでJavaリフレクションの説明は終わりです。最初にいいねして収集し、引き続き注意を払うことを歓迎します。工場に突入!

おすすめ

転載: juejin.im/post/7082713148607856654