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.获得二进制字节流。2.静态存储结构转化为运行时的数据结构。3.生成代表类的对象。
- 校验:确保字节流包含的信息符合当前虚拟机的要求。
- 准备:为类变量分配内存,初始化为默认值。
- 解析:将类型中的符号引用转化为直接引用。
- 初始化:执行类构造器
< client>
方法的过程(包括静态语句块static{}
)
详细的可以康康我的这篇文章深入理解Java虚拟机系列(三)–虚拟机类加载机制
初始化阶段对于一个类的创建而言是至关重要的,Java
类在初始化阶段,会根据我们自己制定的Java
代码去初始化类变量和其他资源 。既然ReflectionFactory
去创建实例是不会调用构造函数的,那么也就是不会经过初始化这个阶段,故对于本案例中的Test
类,其成员变量都是没有被初始化赋值的。故都是null
。
当然,有的读者可能注意到了,final String
和 final 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));
}
结果如下:
解释:
String a = "helloworld";
这段代码,变量a
就是常量,对应的字符串保存在常量池中。- 因为
final String b = "hello";
中final
的修饰,因此对于变量b
,它是常量。 - 因此对于
String c = b + "world";
这段代码,程序看来就是两个常量之间的拼接运算,因此变量c
也是常量,这段逻辑则是在编译期就完成的。其值为”helloworld"
。同时发现常量池中已经有了”helloworld“
了,因此变量c
和变量a
本质上是一个东西。因此比较返回true
。 String d = "hello";
这段代码,程序认为它是一个变量。非常量。因此后续的String e = d + "world";
这段代码,是在程序运行期间的链接阶段进行的。 本质上就不是一个东西。因为字符串拼接,运行时生成,说明用到了StringBuilder
的拼接,对象引用都不是同一个了。 因此比较返回false
。