一直很好奇
final的类字段在class文件是怎么表示的,所以用javap看看怎么回事,也顺便复习下字节码指令👿
没有final修饰的类的静态字段
定义一个类
1 | public class test{ |
编译查看class文件
1 | javac test.java |
你会发现public static String str="严";直接翻译成一个静态块
1 | static {}; |
上面ldc的指令就是把常量从常量池读到操作数栈,putstatic指令从栈顶赋值给类的静态字段str,这个字段之后在main函数会读出来。
1 | public static void main(java.lang.String[]); |
getstatic是用来读取类的静态字段的,这里读出来放入操作数栈。
有final修饰的类静态字段
改一下这个类,加final修饰下
1 | public class test{ |
编译查看class文件
1 | javac test.java |
你会发现class文件没有之前的静态块了,而且也不再用getstatic指令获取字段的值,而是直接ldc指令取常量池的值。
接下来看看实例字段在加final或不加会不会有什么不同呢
不加final的实例字段
1 | public class test{ |
类文件
1 | //省略没用的了。。。 |
构造函数里面用ldc初始化了实例字段str的值,然后在main函数里面用getfield指令,获取t实例的str的值。
加final的实例字段
1 | public class test{ |
类文件
1 | //省略没用的了。。。 |
getfield指令变成了熟悉的ldc,很明显了,加了final之后就去常量池去找,就不需要用getfield指令。
上面的字段是字符串,那接下来看看数字的字段会怎么样呢
数字的非final静态字段
上代码
1 | public class test{ |
javap结果
1 | //省略常量池 |
基本差不多,只是ldc换成了bipush。bipush就是把后面的操作数(100)压入栈。如果压入的不是100,而是更大或更小,那么用的指令就会不同的了,例如iconst_1指令,就是压入1到栈。至于main函数里面,还是用getstatic指令获取字段的值。
数字的final静态字段
上代码
1 | public class test{ |
javap结果
1 |
|
没啥惊喜的,就是静态块去掉,需要获取字段的值的地方从getstatic变成了bipush。
至于实例字段是怎么样的,应该都差不多了,就不列举了,接下来看看字段类型是引用的是怎么样呢。
加final的引用类型字段
上代码
1 | public class test{ |
javap结果
1 |
|
这里直接上final的版本,是因为加不加final,其实都是一样的,都是有静态块,引用的时候不再有什么其他指令了,老老实实的用getstatic。
总结
对于字符串或者数字类型,他们都是属于字面量,编译时已经知道,他们要么存在常量池里面,要么存在指令的操作数里面,所以加final标识的情况下,获取值时不用再到关联的对象底下去获取了,因为final就是不变,直接调用相关指令去常量池,或者直接操作数取就好。
对于引用类型,这个基本要到运行时才能确认他们的引用地址,所以加不加final都是一样。
看来字节码才是最能确定java是怎么运行的,所以与其找网上的说法不如javap一下看看。