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 + '\'' +
                '}';
    }
}

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());
}

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);
    }
}

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

  • 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. 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
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zong_0915

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值