看完这篇文章才知道什么是java反射(每天一题,一起进大厂)

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

每天一题,一起进大厂,欢迎大家点击阅读。《一起进大厂系列》是我最近刚开始准备的,主要是针对大厂面试题进入针对性深入学习。欢迎大家关注该专栏,会进行不间断更新。一起加油,一起进大厂。话不多说,进入今天的正题。

image.png

反射初识

在网上看java反射讲的是很模糊,今天通过我的理解细细讲下,相信通过这篇文章让你重拾对学习的信心。

一句话概括:反射是可以绕过jvm编译阶段。可以动态加入代码。比如一个对象没有确定,可以在运行过程中动态去确定。一个对象的方法未完全实现,也可以调用该方法(部分)。

很抽象?等下面我们通过实例讲完之后,再看这个概念,觉得醍醐灌顶!!!

有反射,那就有正射,正射就是我们提前在代码中new 对象()

假如我需要实例化一个学生对象,代码就会是这样子。

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

某一天忽然来了一个这样的场景需求,就是需要实例化老师和学校这两个对象,在代码编译阶段是不确定创建对象,那么就需要动态创建了,代码即是这样的:


//动态创建实体
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决定使用哪一种实体,可以在项目运行时,通过动态传入参数决定使用哪一个实体,看到这里是不是有点像设计模式当中的工厂模式,是的,这个也算是应用场景之一。

那么通过这个例子简单总结下

简单总结

在代码运行之前,我们不确定将来会使用哪一种数据结构,只有在程序运行时才决定使用哪一个数据类,而反射可以在程序运行过程中动态获取类信息调用类方法。通过反射构造类实例,代码最终会演变成下面这样。

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