• Java - 由ReflectionFactory引发的对final关键字的思考


    前言

    Spring常见问题解决 - AOP调用被拦截类的属性报NPE这篇文章里面讲到了:Spring进行AOP实现的时候,如果采取Cglib来实现,那么底层创建实例的时候,会优先使用ReflectionFactory去创建实例。而文章我也提到过,ReflectionFactory创建的实例对象,其成员属性是不会被初始化的。不过这句话从严格意义来说并不正确,那么本文我们继续来探究下。

    一. ReflectionFactory创建实例

    总的来说,ReflectionFactory的作用在于:可以不通过调用构造函数来创建一个对象的实例。 案例如下:
    我们自定义一个User类(无参构造必须要有)

    public class User {
        private String name;
    
        public User(String name) {
            this.name = name;
            System.out.println("User with name");
        }
    
        public User() {
            System.out.println("User without param");
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    2.通过ReflectionFactory来创建User实例:

    public static void main(String[] args) throws Exception {
        ReflectionFactory reflectionFactory = ReflectionFactory.getReflectionFactory();
        Constructor constructor = reflectionFactory.newConstructorForSerialization(User.class, Object.class.getDeclaredConstructor());
        constructor.setAccessible(true);
        User t = (User) constructor.newInstance();
        System.out.println(t.toString());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    3.结果如下:可见并没有调用对应的构造函数。
    在这里插入图片描述

    ReflectionFactory创建实例,底层是通过字节码来直接操作的。但是这一块的原理并不是本文想要讨论的重点。我们知道,AOP调用里面,如果调用了被代理类的成员变量,那么其可能为null。这一块才是我们要讨论的。

    1.1 成员变量为null的测试案例

    案例如下:

    public class Test {
        public final User user = new User("LJJ");
        public User user2 = new User("LJJ");
        public String name = "Hello";
        public final String str = "ssss";
        public Integer a = 12222;
        public final Integer b = 12222;
        public int aa = 1;
        public final int bb = 2;
    
        public static void main(String[] args) throws Exception {
            ReflectionFactory reflectionFactory = ReflectionFactory.getReflectionFactory();
            Constructor constructor = reflectionFactory.newConstructorForSerialization(Test.class, Object.class.getDeclaredConstructor());
            constructor.setAccessible(true);
            Test t = (Test) constructor.newInstance();
            System.out.println("final user: " + t.user);
            System.out.println("user: " + t.user2);
            System.out.println("final String: " + t.str);
            System.out.println("String: " + t.name);
            System.out.println("final Integer: " + t.b);
            System.out.println("Integer: " + t.a);
            System.out.println("final int: " + t.bb);
            System.out.println("int: " + t.aa);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    结果如下:
    在这里插入图片描述
    那么我们来讨论下,为何会这样?首先我们可以根据结果给出三类:

    • String类型(单独拿出来是因为比较特殊)。
    • Integer/User这种Object
    • int这种基础数据类型。

    1.2 为何成员变量没有被初始化?

    接下来进行分析,首先,我们知道用ReflectionFactory去创建实例是不会调用构造函数的。那么我们先来说下Java中一个类加载的流程,一共分为五个步骤:

    1. 加载(由类加载器完成):1.获得二进制字节流。2.静态存储结构转化为运行时的数据结构。3.生成代表类的对象。
    2. 校验:确保字节流包含的信息符合当前虚拟机的要求。
    3. 准备:为类变量分配内存,初始化为默认值。
    4. 解析:将类型中的符号引用转化为直接引用。
    5. 初始化:执行类构造器< client> 方法的过程(包括静态语句块static{}

    详细的可以康康我的这篇文章深入理解Java虚拟机系列(三)–虚拟机类加载机制

    初始化阶段对于一个类的创建而言是至关重要的,Java类在初始化阶段,会根据我们自己制定的Java代码去初始化类变量和其他资源 。既然ReflectionFactory去创建实例是不会调用构造函数的,那么也就是不会经过初始化这个阶段,故对于本案例中的Test类,其成员变量都是没有被初始化赋值的。故都是null

    当然,有的读者可能注意到了,final Stringfinal int的组合都是能够拿到正确的值的(int类型的默认值是0,它不存在null这一说法)。
    在这里插入图片描述

    1.3. final的用处

    • 如果用来修饰类,那么这个类不可以被继承,同时里面的所有成员和方法都会被隐式地指定为final
    • 如果用来修饰方法,那么这个方法不可以被重写。
    • 如果用来修饰变量。如果是基础数据类型,那么其值只有在初始化的时候赋值并且不可以被更改。如果是引用类型变量,那么初始化后不可以被再指向另外一个对象。但是指向的对象内容是可以改变的。

    二. 本文案例分析

    对于本文而言重点就是:

    • final修饰的变量,一定要有一个初始化的过程。可以在定义的时候直接赋值。也可以在类构造的时候传参赋值。
    • 本文案例中是在定义变量的时候直接赋值的,因此也就是在编译期间就能够知道其具体的值。
    • final变量是基本数据类型以及String类型时,如果在编译期间能知道它的确切值,则编译器会把它当做编译期常量使用。

    那么请注意,Java里面对于常量相关的概念我们要知道:

    • 基础数据类型的值都是保存在栈中的。
    • 如果String类型,在编译期就指定了起值,那么对应的字符串是保存在常量池中的。(也是栈的一部分)

    也因这两类数据的特殊性,不像User类这样,初始化还需要在堆内存中去开辟一个空间。因此对于Test类而言,User类型的user成员变量,无论是否被final修饰,其都是需要通过堆内存给他们分配空间才能够使用的,而这一阶段就发生在Test类的初始化阶段(但是本文案例中,这一阶段都被跳过了呀)。

    这里也就可以理解为何案例中输出的结果是这样的。后面再举个例子来简单理解下final的用法:

    @org.junit.Test
    public void test() {
        String a = "helloworld";
        final String b = "hello";
        String d = "hello";
        String c = b + "world";
        String e = d + "world";
        System.out.println((a == c));
        System.out.println((a == e));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    结果如下:
    在这里插入图片描述
    解释:

    1. String a = "helloworld";这段代码,变量a就是常量,对应的字符串保存在常量池中。
    2. 因为final String b = "hello";final的修饰,因此对于变量b,它是常量。
    3. 因此对于String c = b + "world";这段代码,程序看来就是两个常量之间的拼接运算,因此变量c也是常量,这段逻辑则是在编译期就完成的。其值为”helloworld"。同时发现常量池中已经有了”helloworld“了,因此变量c和变量a本质上是一个东西。因此比较返回true
    4. String d = "hello";这段代码,程序认为它是一个变量。非常量。因此后续的String e = d + "world";这段代码,是在程序运行期间的链接阶段进行的。 本质上就不是一个东西。因为字符串拼接,运行时生成,说明用到了StringBuilder的拼接,对象引用都不是同一个了。 因此比较返回false
  • 相关阅读:
    可执行文件的装载与进程
    新一代PoE接口高精度OpenCV人工智能套件OAK-D-Pro-PoE测试
    第二十二章 Ajax
    MBA管理类联考英语二题型答题时间及次序问题
    前端请求发送成功,后端收到null
    【c语言】详解文件操作(二)
    SpringAOP日志注解
    CONNMIX 开发 WebSocket 视频弹幕
    【产品运营】产品需求应该如何管理
    MySQL基础-多表查询
  • 原文地址:https://blog.csdn.net/Zong_0915/article/details/126512236