likes
comments
collection
share

【再学一次系列】new对象不行吗?为什么要用反射?

作者站长头像
站长
· 阅读数 20

「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战

前言

哈喽大家好,我是卡诺,一名致力于成为全栈的全粘工程师!

记得第一次在学校接触反射的时候就对自己三连问:“这玩意重要吗?这玩意有啥用?为啥不直接new对象?”。直到后来出来工作,接触了一些三方框架,再加上自己也参与一些公司基础框架开发,才意识到反射不可谓不是Java框架开发的神兵利器。

今天我们便来重温Java的反射机制,本章内容主要针对反射的前置知识进行讲解,其他反射相关操作将在【再学一次Java】专栏中持续更新,如果大家感兴趣可以关注这个专栏哦!

简介

官方描述👉访问地址:Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions. The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control.

谷歌翻译一下:反射允许 Java 代码能够发现有关已加载类的字段、方法和构造函数的信息,并在安全限制内使用反射字段、方法和构造函数对其底层对应物进行操作。该API 适用于需要访问目标对象的公共成员(基于其运行时类)或给定类声明的成员的应用程序。它还允许程序抑制默认反射访问控制。

简单来说,在程序运行时,反射允许我们可以以编程的方式动态操作任意一个已经加载的类/对象的所有的属性和方法。(概念懵逼没关系,铁子们请继续向下看)

JVM装载类的过程

在反射正文开始前,我们要先聊一聊JVM装载类的过程,看如下代码:

/**
 * person类
 * @author : uu
 * @version : v1.0
 * @Date 2022/1/24
 */
@Data
public class Person {
    private String name;
}

// 测试类
@Test
public void test(){
    System.out.println("new 之前");
	Person person = new Person();
	System.out.println("new 之后");
}

启动测试时请在VM配置参数:-XX:+TraceClassLoading打印类的加载日志,启动后控制台打印如下数据:

...
new 之前
[Loaded com.uucoding.advance.entity.Person from file:/Users/uu/IdeaProjects/uu-study/uu-java-study/thinking-java/advance-java-example/target/classes/]
new 之后
...

该行打印的内容表示从编译的class文件中加载Person类。我们来画个简单的图理解理解上面这些代码的流程:

【再学一次系列】new对象不行吗?为什么要用反射?

  1. 首先程序启动,Person.java文件编译成Person.class文件;
  2. 程序执行到new Person(),开辟内存空间
  3. 类加载器将Person.class加载到JVM内存中;
  4. 将Person.class加载到JVM的同时也会将person对象,以及Person类的Class对象加载到JVM内存中。

不知道大家看完上述流程会不会出现如下问题:

  • 没有new对象,启动会不会加载该类?

  • new了对象,不调用,启动会不会加载该类?

  • 类真的是只会被加载一次吗?

问题实操答疑

上面的三个问题,我也不知道,但是我们可以有效的利用我们手头的idea来测试一下。

问题一:没有new对象,启动会不会加载该类?

  • 验证步骤

    1. 去掉测试方法中的 new Person()
    2. 启动测试类,VM参数需要配置为-XX:+TraceClassLoading
    3. 查看类的加载日志是否包含Person类
  • 验证代码如下:

@Test
public void testEmpty(){
    // VM 配置 -XX:+TraceClassLoading
    // 不new对象,查看Person类是否被加载
}
  • 验证结果

    • 日志中无Person类,没有new对象,启动不会加载该类

问题二:new了对象,不调用,启动会不会加载该类?

  • 验证步骤

    1. 引入spring-boot-starter-web依赖
    2. 构建SpringBoot启动入口
    3. 构建一个测试接口,方法体增加 new Person()
    4. 启动SpringBoot程序,VM参数需要配置为-XX:+TraceClassLoading
    5. 查看类的加载日志是否包含Person类
  • 验证代码如下:

// 引入依赖
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
	<version>2.6.2</version>
</dependency>
/**
 * 启动类
 * @author : uu
 * @version : v1.0
 * @Date 2022/1/24
 */
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
/**
 * 验证反射controller
 * @author : uu
 * @version : v1.0
 * @Date 2022/1/24
 */
@RestController
@RequestMapping("/test")
public class ReflectController {
    /**
     * 验证请求才会加载class
     * @return
     * @throws Exception
     */
    @GetMapping("/lazyNew")
    public void lazyNew() throws Exception {
        new Person();
    }
}
  • 验证结果

    • 没有Person类,如果new了对象,不调用,启动不会加载该类
  • 继续验证(如果请求该接口,类会不会被加载?)

    1. 浏览器/Postman请求该接口
    2. 查看类的加载日志是否包含Person类
  • 继续验证结果

    • 请求后控制台打印出加载Person类
  • 结论

    • 通过验证其实可以看的出来,只有被系统使用的类才会被调用。

问题三:类的真的只会被加载一次吗?

基于问题二的代码,我们重复调用相同的接口,Person类不会被重复加载!

结论

只有被系统使用的类才会被JVM加载,且每个类只会被加载一次,类加载后会生成一个Class对象,该Class对象中包含这个类的所有信息!

为什么要用反射?

回归主题,上面啰哩啰嗦半天,看起来好像反射也没啥特殊的呀!不如我们举个🌰,工厂模式大家应该都比较了解(或看这里工厂案例新玩法,用Lambda重构设计模式),起初我们是通过传入一个key,获取一个对象。那么如果新增或者减少类,都要对工厂进行调整,但是如果利用反射,根本就不需要考虑工厂的变化,只需要知道有哪些类,现在要用什么即可!

实际开发中,反射最重要的场景之一就是开发通用的框架,比如:Spring中数据驱动配置,仅需要将配置spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver更改成spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver,即可将数据源mysql驱动替换成Oracle驱动!

反射能够有效的降低代码耦合度,避免硬编码问题,有效提高程序的灵活性和扩展性!

反射能力

  • 可以在运行时获取类的信息
  • 可以在运行时创建任意一个类的对象
  • 可以在运行时操作任意一个类/对象的任意成员变量和方法

反射缺点

以下描述来自官方文档,详见:👉反射文档 见【Drawbacks of Reflection】部分

反射是一把双刃剑,它很强大,但不能乱用,因为使用反射会给我们带来一些困扰,比如:

  • 性能开销:反射涉及动态解析类型,因此无法执行某些Java虚拟机的优化,应该避免在性能敏感功能中使用反射

  • 安全限制:反射需要在安全管理器下运行时可能不存在的运行时权限。对于必须在受限安全上下文中运行的代码(例如在 Applet 中),这是一个重要的考虑因素。

  • 内部暴露:反射允许代码执行在非反射代码中非法的操作,例如访问private字段和方法,因此使用反射可能会导致意想不到的副作用,这可能会导致代码功能失调并可能破坏可移植性。反射代码打破了抽象,因此可能会随着平台的升级而改变行为。

源码

总结

  • 本章主要阐述反射的一些前置知识,主要包括类的加载机制、反射的优缺点;
  • Class类是反射的基础,任何类都是Class的实例对象;
  • JVM在加载的时候会把类的类名、所在包、构造函数、属性、方法打包对应的Class对象中;
  • 反射能够获取类的属性方法等,本质上就是操作这个类Class对象。

关联文章

👉【再学一次系列】

最后

  • 感谢铁子们耐心看到最后,如果大家感觉本文有所帮助,麻烦给个赞👍关注➕
  • 由于本人技术有限,文章和代码可能存在错误,希望大家评论指出,万分感激🙏;
  • 同时也欢迎大家V我(uu2coding)一起讨论学习前端、Java知识,一起卷一起进步。