Java中final和static修饰的变量是在什么时候赋值的?

开始

一位朋友在群里问了这样一个问题:

本着乐于助人的想法,我当时给出的回答:

后来我总觉得哪里不对劲。

于是我仔细翻阅了《Java虚拟机规范》和《深入理解Java虚拟机》关于这一部分的内容。

害!发现自己理解的有问题。

因为自己的理解出错而误导了别人,实在是让我万分羞愧!

于是我加了这位朋友的好友,向这位朋友表达了歉意。

这位朋友也非常随和,对此表示理解。

今天讨论的问题就是从这个故事开始的。

final修饰的实例变量


我们先分析一下这个问题:

深入Java虚拟机有一句是ConstantValue属性的作用是通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量才可以使用这项属性。但为什么private final a = 10也可以被赋值

我翻阅了《深入理解Java虚拟机》第二版,在第191页,确实有前面那句话

书中说的很清楚,ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。

那就意味着只有static修饰的类变量才会在class文件中对应的字段表加上ConstantValue属性吗?

答案是否定的。

用final修饰的实例变量,编译成class文件的时候,对应的字段表也有可能会加上ConstantValue属性。

注意,我这里用了“可能”这两个字,因为这是有条件的。

哪些情况会有ConstantValue属性呢?

我们写一段代码,列举一下用final修饰的实例变量的几种情况。

然后用javap -verbose命令反编译查看字节码:

我们可以看到,在字段表集合里面有四个字段表:

分表对应着a,b,c,d,e五个实例属性。

他们都带有ACC_PUBLIC(public)ACC_FINAL(final)的访问标志。

但只有a和b对应的字段表带有ConstantValue属性。

不难得出:

用final修饰不是在构造方法赋值的String类型或者基本类型成员变量,编译成字节码文件时,对应的字段表也会带有ConstantValue属性。

这个结论不和《深入理解Java虚拟机》冲突吗?

于是我翻阅了官网的JVM规范,在4.7.2部分我找到了这样一句话:

书中说的很清楚:

如果field_info(字段表)表示的非静态字段包含了ConstantValue属性,那么这个ConstantValue属性会被JVM虚拟机所忽略。

也就是说,对于非静态字段,就算你编译器加上了ConstantValue属性,JVM也会忽略掉,你加不加结果是一样的。

看完JVM规范里面的说明,再回来看《深入理解Java虚拟机》里面的这句话:

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值,只有被static关键字修饰的类变量才可以使用这项属性。

作者的这句话的前半句没有什么争议,但我觉得后半句的表述的不太明确,容易造成误解。

以我的理解,应该是“只有被static关键字修饰的类变量才可以使用ConstantValue这项属性来进行初始化,否则使用这项属性也会被JVM忽略掉

好了,我们再回到那位朋友问的问题:

为什么private final a = 10也可以被赋值?

首先,这个问题的本身就问的不太准确。

我理解这位朋友真正想问的是“为什么private final a = 10也可以通过ConstantValue属性的形式赋值?”

我觉得这是一个很好的问题。

这位朋友通过实验发现用final修饰的实例变量对应的字段表有ConstantValue属性。

结合《深入理解Java虚拟机》,他认为a是通过ConstantValue属性让虚拟机知道然后为其赋值的。

最后他发现和书中冲突,于是提出了上文的这个问题。

这位朋友的思路有问题吗?我觉得是没有问题的。

不过这样的理解是对的吗?显然是不对的。

因为虚拟机规范是这样规范的。对于非静态字段,ConstantValue属性是不会生效的。

至于为什么要这样设计,功力不够的我暂时无法理解设计者的想法。

那单独用final修饰的实例变量到底是在什么时候赋值的呢?

这个问题也不难回答,看一下字节码就清楚了。

图片可点击放大

通过查看字节码,我们可以看到生成了一个<init>方法,右边是它的字节码指令。

什么是<init>方法?我们看看JVM规范上的解释:

我们温习一下这个英语四级短语:appear as

然后,我们一起翻译一下:

在JVM层面上,每一个用Java写的构造方法都表现为实例初始方法,这个方法就是<init>方法。记住,这个方法会在实例初始化的时候被调用。

我们再来看一下putfield这个字节码指令的含义:

为指定的类的实例域赋值的,也就是为实例变量赋值的指令。

知道了<init>方法是什么和putfield的含义后,结合上面的字节码,不难得出:

这些用final修饰实例变量是在实例构造器方法里面赋值的,也就是对象创建的时候赋值。

static修饰的类变量


上面讲到ConstantValue属性的作用是通知虚拟机为静态变量赋值。

什么是静态变量?static修饰的变量!

那static修饰的变量是什么时候加载的呢?

在这之前,我需要给你把类加载的几个过程大致讲一下:

类的生命周期由7个阶段组成,类加载说的是前5个阶段:

即加载—>验证—>准备—>解析—>初始化。

类的生命周期图

我们简单过一下这几个阶段:

  • 加载:将字节码所代表的静态存储结构转化为方法区的运行时数据结构。

  • 验证:验证字节码格式,确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  • 准备:创建类或者接口的静态字段,并为静态变量设置初始值。

  • 解析:将常量池内的符号引用替换为直接引用。

  • 初始化:执行类构造器<clinit>方法。

上面出现类构造器<clinit>方法,这个方法又是个什么东西呢?

JVM 规范这样写道:

说白了,编译器会收集所有静态变量的赋值动作、所有静态代码块,合并产生一个方法,即<clinit>方法。

<cliit>方法在上面那张类加载图中的的初始化阶段执行。

现在你应该对类加载过程有一个大致的了解了。

回到static修饰的变量(类变量),类变量有两种赋值方式可以选择:

  • 使用ConstantValue属性赋值。

  • 在类构造器<clinit>方法中赋值。

目前Oracle公司实现的Javac编译器的选择是:

  • final+static修饰:使用ConstantValue属性赋值。

  • 仅使用static修饰:在<clinit>方法中赋值。这个方法在类加载的初始化阶段执行。

需要注意点的是,用生成ConstantValue属性来进行初始化,这个变量必须是基本类型或者java.lang.String类型。

对于这一点,我们也可以通过javap -verbose命令反编译验证一下:

为什么呢?

这是因为Class文件格式的常量类型中只有与基本属性和字符串相对应的字面量,所以就算ConstantValue属性想支持别的类型也无能为力。

final+static修饰的常量


上面我们说过,<clinit>方法是在类加载的初始化阶段赋值的。

那static+final修饰的常量是在类加载的那一阶段进行的呢?

我们可以看一下JVM规范:

我们可以看到在JVM规范里面,static+final修饰的常量是在初始化阶段执行方法之前执行的。

咦?网上的博客不都是在类加载的准备阶段会对普通类属性赋初始值,对带有ConstantValue的类属性直接赋值吗?

《深入理解Java虚拟机》也是这样说的啊?

书上是错的吗?不是的,因为《深入理解Java虚拟机》里面讲的具体实现,是基于HotSpot VM讲的。

确确实实,HotSpot VM就是这么干的,我们也可以在openJdk中找到对应的源码:

跟踪源码可以发现HotSpot VM对基本类型或者字符串类型的常量的赋值确实在准备阶段完成了。

但一个很关键的点是,虽然没在初始化阶段赋值,仍然在调用<clinit>方法之前赋值了。

外界是不会观察到HotSpot VM提前做了这个初始化赋值的,所以是没问题的。

不过要记住的是:

JVM规范里明确说了正确的初始化时机是在“初始化(Initialization)”阶段。


总结


  1. 单独用final修饰的变量也有可能在字节码找到对应的ConstantValue属性,但是会被JVM忽略掉。

  2. final修饰的实例属性,在实例创建的时候才会赋值。

  3. static修饰的类属性,在类加载的准备阶段赋初值,初始化阶段赋值。

  4. static+final修饰的String类型或者基本类型常量,JVM规范建议在初始化阶段赋值,但是HotSpot VM直接在准备阶段就赋值了。

  5. static+final修饰的其他引用类型常量,赋值步骤和第二点的流程是一样的。

还有一点,一定不要把《深入理解Java虚拟机》和《Java虚拟机规范》搞混了。

  • 《Java虚拟机规范》是官方JVM规范文档翻译而来的,所有的JVM实现都要遵从规范,但有强制要求的规范和建议的规范。

  • 《深入理解Java虚拟机》是作者根据自己的理解,结合HotSpot VM的具体实现,为了让读者更容易理解JVM而写的一本书。

写在最后


本人才疏学浅,OpenJdk源码也理解的不够透彻。

为了说清楚文中的一些知识,我翻阅了很多资料。

但是还是有一部分的知识点无法找到权威的资料证明。

不过我尽量都基于官方文档展开分析,如果有认识有差错的地方,欢迎指出!我定会在第一时间修改,不误导别人!

最后,感谢你的阅读!

如果可以,点赞再看关注,谢谢你!

—————END—————

推荐阅读:

最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。
明天见(。・ω・。)ノ
<think>好的,我现在需要回答用户关于Javafinal修饰成员变量变量的规则及要求的问题。首先,我需要回顾用户提供的引用内容,确保答案准确并涵盖所有关键点。用户引用了多个资料,其中提到final修饰变量的不同情况。例如,引用3指出final变量一旦初始化后值不能改变,用于常量定义、保证不变性优化性能。引用4提到成员变量必须在创建对象前赋值,可以通过定义时赋值、构造器赋值或set方法(但根据Java规则,set方法可能不适用,因为final变量只能赋值一次)。引用5强调final修饰的成员变量必须显式初始化,否则编译错误。 接下来,我需要区分成员变量变量(即静态变量)。成员变量属于实例,而类变量属于类。对于final成员变量,初始化方式包括定义时赋值、构造器赋值或初始化块。而对于final变量,必须在定义时或在静态初始化块中赋值,因为它们在类加载时初始化,早于对象创建。 需要注意,用户可能混淆了成员变量变量的初始化要求。例如,类变量不能在构造器中初始化,因为构造器属于实例层面。此外,正确使用final可以确保线程安全或不可变对象,这也是用户可能关心的应用场景。需要验证每个规则是否符合Java规范,比如是否允许在方法中为final变量赋值(不允许,只能在声明、构造器或初始化块中)。同时,引用4提到三种赋值方式,但set方法可能不适用,因为final变量只能赋值一次,而set方法可能在对象生命周期中被多次调用,导致编译错误。 总结时,需分点列出成员变量变量的规则,强调初始化时机赋值方式的不同,并举例说明。此外,相关问题应引导用户深入理解final的应用,如内存模型影响、设计模式中的使用等。</think>### Javafinal修饰成员变量变量的规则及要求 #### 一、final修饰成员变量(实例变量) 1. **初始化要求** - final成员变量必须在对象创建**前**完成初始化[^4][^5]。 - 初始化方式: - **定义时直接赋值**:`private final int value = 10;` - **构造方法中赋值**:所有构造方法必须包含对该变量赋值[^4]。 - **实例初始化块中赋值**:`{ value = 10; }` ```java public class Example { private final int a = 5; // 方式1:定义时赋值 private final int b; private final int c; { c = 10; } // 方式3:实例初始化块赋值 public Example(int b) { this.b = b; // 方式2:构造方法赋值 } } ``` 2. **不可修改性** - 一旦赋值后,值不可更改,否则编译报错[^3][^4]。 3. **禁止setter方法** - 若通过普通方法(如setter)赋值,会导致编译错误,因为final变量只能被赋值一次[^4]。 --- #### 二、final修饰变量(静态变量) 1. **初始化要求** - final变量必须在**类加载时**完成初始化[^3][^5]。 - 初始化方式: - **定义时直接赋值**:`private static final int MAX = 100;` - **静态初始化块中赋值**:`static { MAX = 100; }` ```java public class Constants { private static final int MAX; // 方式1:定义时赋值 static { MAX = 100; // 方式2:静态初始化块赋值 } } ``` 2. **不可修改性** - 与成员变量相同,初始化后值不可变[^3]。 --- #### 三、特殊规则 1. **对象类型变量** - final修饰对象变量时,**引用不可变,但对象内部属性可修改**[^2][^3]。 例如: ```java final List<String> list = new ArrayList<>(); list.add("Hello"); // 允许操作对象内部 list = new ArrayList<>(); // 编译错误:引用不可更改 ``` 2. **编译时常量优化** - 若final变量是基本类型或字符串且赋值表达式为常量,编译器会直接替换值到使用位置[^3]。 --- #### 四、对比总结 | 类型 | 初始化时机 | 允许的初始化方式 | 作用范围 | |------------|--------------------|--------------------------------|--------------| | 成员变量 | 对象创建前 | 定义赋值、构造方法、实例块 | 对象实例级别 | | 类变量 | 类加载时 | 定义赋值、静态初始化块 | 类级别 | ---
评论 10
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值