这个问题有意思,不过我觉得讨论final字段是否有默认值没意义,你在idea上输入final double a不主动初始化它能编译通过么?
不能,直接在idea上报出错误了,连编译都不让过,不过final初始化之前是否真的有别的值呢?答案是肯定的在《java高并发艺术》一书中就有 “一个线程当前看到一个整型final域的值为0 { 还未初始化之前的默认值 }”
这句话就代表着它被初始化了,为了验证这句话,我写了一段代码
public class FinalTest {
private final double a;
{
Class aClass = FinalTest.class;
Field field = null;
try {
field = aClass.getDeclaredField("a");
field.setAccessible(true);
// field.set(this, 1000.0);Object o = field.get(this);
System.err.println(o);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
public FinalTest() {
this.a = 10.0;
}
public static void main(String[] args) throws Exception {
FinalTest test = new FinalTest();
System.out.println(test.a);
}
}
这段代码会打印:
0.0
10.0
这段代码的设计就是将手动初始化final字段的时机放置在构造方法中,然后在构造代码块中尝试获取final的值,我们还能在这之前修改final字段的值为1000.0
那么他在什么时候被初始化呢?是在jvm加载类的过程中么?
首先估计,要不就是clinit函数要不就是init函数中初始化掉了,由于前面的例子在构造函数中就被初始化了,我们需要写一个简单的例子
public class Demo {
private final int a = 10;
private static final int b = 20;
}
使用javap反编译看看
{
private final int a;
descriptor: I
flags: (0x0012) ACC_PRIVATE, ACC_FINAL
ConstantValue: int 10 ########################################
private static final int b;
descriptor: I
flags: (0x001a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL
ConstantValue: int 20
public com.zhazha.test04.Demo();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: bipush 10
7: putfield #2 // Field a:I ############################
10: return
LineNumberTable:
line 9: 0
line 11: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/zhazha/test04/Demo;
}
看上图的两个使用#号标记的地方
可以很直白的看出来
final int a 虽然存在 ConstantValue 但还是得去init方法中初始化
而static final int b 却不需要
现在结果很明显了,他在类实例化时发生的初始化
=======================2020.08.27============================
昨天赶时间没法完全,今天补一补
jdk1.5版本(好像是5)
final修饰的字段在《java高并发艺术》一书中有专门的章节讲到了oracle在JSR-133对final字段增强,其增强的方式无非是尽量保证使用者在对final字段使用前必须初始化,其做的优化主要还是在构造方法中加上了内存屏障前面的分析我们知道final字段的初始化在init方法中,而构造方法其实也被抓取到init方法中执行了只不过被放置到了最后,那么现在我们对final字段的初始化加上了内存屏障
增加内存屏障的目的其实很明白了,防止重排序,为了解释为什么他需要增加一个内存屏障我们需要下面这段例子
class FinalFieldExample {
final int x;
int y;
static FinalFieldExample f;
public FinalFieldExample() {
x = 3;
y = 4;
}
static void writer() {
f = new FinalFieldExample();
}
static void reader() {
if (f != null) {
int i = f.x; // guaranteed to see 3 int j = f.y; // could see 0 }
}
}
上面的代码分别创建两个线程分别执行 writer 和 reader 方法
就会存在这么一种情况,使用构造函数不完整构造了一个对象( this 被赋予了地址,但 x 和 y 字段还没开始初始化),如果没存内存屏障防止重排序,线程上下文切换便会切换到 reader 函数中执行,此时 f 就是 this 所以 != null 会通过 if 判断,然后你就会神奇的发现 final 字段 x = 0 的情况,等到这两个线程执行完毕后,再次使用 final 字段 x ,此时 x = 3 出现了出现了,x == 0 x == 3 两种情况在 final 修饰的变量一生只能有一个值的情况下(init执行完毕后)出现了受精卵期就开始接受教育,然后到出生几年婴儿期又接受了一次教育
那么加上了内存屏障呢?
这两个线程在执行 writer 和 reader 方法就不会出现两个结果的情况,即使方法 reader 先被执行了也会因为 this == null 不被执行
而书本中在构造函数结束前(书本里说是return前)增加了StoreStore内存屏障,在初次读取 final 前加上LoadLoad内存屏障但书本说了在x86下仅支持StoreLoad内存屏障就没后续了你敢信,当时我就下载来openJDK的源码找到了构造函数的地方看见源码吐了,各种为了提高效率的优化,压缩指针等等看不下来,最后找了StoreStore StoreLoad LoadLoad LoadStore这几个内存屏障发现其实jvm自己实现了这些内存屏障专门实现的,所以应该不会出现问题