你知道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
命令查看Java编译器为我们生成的字节码。
我们可以看到,在字段表集合里面有四个字段表,分表对应这a,b,c,d,e五个实例属性,他们都带有ACC_PUBLIC(public)
和ACC_FINAL(final)
的访问标志。但只有a和b对应的字段表带有ConstantValue属性。我们总结一下:
用final修饰不是在构造方法赋值的String类型或者基本类型成员变量,编译成字节码文件时,对应的字段表也会带有ConstantValue属性。
这个结论不和《深入理解Java虚拟机》冲突吗?
于是我翻阅了JVM Spec Java SE 8Edition(周志明前辈是翻译过,书名《Java虚拟机规范》,但是我手里没有翻译后的中文版),在4.7.2部分我找到了这样一句话:
书中说的很清楚,如果field_info(字段表)表示的非静态字段包含了ConstantValue属性,那么这个ConstantValue属性会被Java虚拟机所忽略。也就是说,对于非静态字段,就算你编译器加上了ConstantValue属性,JVM也会忽略掉,你加不加结果是一样的。
看完《Java虚拟机规范》里面的说明,再回来看《深入理解Java虚拟机》里面的这句话:
ConstantValue属性的作用是通知虚拟机自动为静态变量赋值,只有被static关键字修饰的类变量才可以使用这项属性。
作者的这句话的前半句没有什么争议,但我觉得后半句的表述的不太明确,容易造成误解。
以我的理解,应该是“只有被static关键字修饰的类变量才可以使用这项属性来进行初始化,否则使用这项属性也会被JVM忽略掉”
好了,我们再回到那位朋友问的问题:为什么private final a = 10也可以被赋值?
首先,这个问题的本身就问的不太准确。我理解这位朋友真正想问的是“为什么private final a = 10也可以通过ConstantValue属性的形式赋值?”
我觉得这是一个很好的问题,这位朋友通过实验发现用final修饰的实例变量对应的字段表有ConstantValue属性,结合《深入理解Java虚拟机》,他认为a是通过ConstantValue属性让虚拟机知道然后为其赋值的。最后他发现和书中冲突,于是提出了上文的这个问题。
这样的思路有问题吗?我觉得是没有问题的。
不过这样的理解是对的吗?显然是不对的。
因为虚拟机规范是这样规范的。对于非静态字段,ConstantValue属性是不会生效的。
至于为什么要这样设计,功力不够的我暂时无法理解设计者的想法。
那单独用final修饰的实例变量到底是在什么时候赋值的呢?
这个问题也不难回答,看一下字节码就清楚了。
通过查看字节码,我们可以看到有一个方法,右边是它的字节码指令。
什么是方法?我们看看Java虚拟机规范上的解释:
我们温习一下这个英语四级短语:appear as
然后,我们一起翻译一下:在JVM层面上,每一个用Java写的构造方法都表现为实例初始方法,这个方法就是方法。
记住,这个方法会在实例初始化的时候被调用。
我们再来看一下putfield
这个字节码指令的含义:putfield
指令就是为指定的类的实例域赋值的,也就是为实例变量赋值的指令。
现在我们可以清晰的知道,这些用final修饰实例变量是在实例构造器方法里面赋值的,也就是对象创建的时候赋值。
static修饰的类变量
上面讲到ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。
我们再回过来讲一下静态变量,一个很关键的关键字static。
在这之前,我需要把类加载的几个过程大致给你讲一下:
类的生命周期由7个阶段组成,类加载说的是前5个阶段,即加载—>验证—>准备—>解析—>初始化。
类的生命周期图
我们简单过一下这几个阶段:
- 加载:将字节码所代表的静态存储结构转化为方法区的运行时数据结构。
- 验证:验证字节码格式,确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 准备:创建类或者接口的静态字段,并为静态变量设置初始值。
- 解析:将常量池内的符号引用替换为直接引用。
- 初始化:执行类构造器方法。
类构造器方法又是个什么东西呢?
JVM Spec Java SE 8Edition这样说道:
说白了,编译器会收集所有静态变量的赋值动作、所有静态代码块,合并产生一个方法,即方法。这个方法在类加载的初始化阶段执行。
而对于类变量(static修饰的),则有两种赋值方式可以选择:
- 使用ConstantValue属性赋值。
- 在类构造器方法中赋值。
目前Oracle公司实现的Javac编译器的选择是:
- final+static修饰:使用ConstantValue属性赋值。
- 仅仅使用static修饰:在方法中赋值。
需要注意点的是,用生成ConstantValue属性来进行初始化,这个变量必须是基本类型或者java.lang.String类型。
这是因为Class文件格式的常量类型中只有与基本属性和字符串相对应的字面量,所以就算ConstantValue属性想支持别的类型也无能为力。
对于这一点,我们也可以通过javap -verbose
命令反编译验证一下:
final+static修饰的常量
上面我们说过,方法是在类加载的出初始化阶段赋值的。
那static+final修饰的常量是在类加载的那一阶段进行的呢?我们可以看一下JVM规范:
我们可以看到在JVM规范里面,static+final修饰的常量是在初始化阶段执行方法之前执行的。
咦?我们平时背的不都是在类加载的准备阶段会对普通类属性赋初始值,带有ConstantValue的类属性直接赋值吗?
《深入理解Java虚拟机》也是这样说的啊?
书上是错的吗?不是的,因为《深入理解Java虚拟机》里面讲的具体实现,是基于HotSpot VM讲的。
确确实实,HotSpot VM就是这么干的,我们也可以在openJdk中找到对应的源码:
看起来,HotSpot VM对基本类型或者字符串类型的常量的赋值确实在准备阶段完成了。
但一个很关键的点是,仍然在调用之前赋值了。
外界是不会观察到HotSpot VM提前做了这个初始化赋值的,所以是没问题。
不过要记住的是,规范里明确说了正确的初始化时机是在“初始化(Initialization)”阶段。
总结
- final修饰的实例属性,在实例创建的时候才会赋值。
- static修饰的类属性,在类加载的准备阶段赋初值,初始化阶段赋值。
- static+final修饰的String类型或者基本类型常量,JVM规范是在初始化阶段赋值,但是HotSpot VM直接在准备阶段就赋值了。
- static+final修饰的其他引用类型常量,赋值步骤和第二点的流程是一样的。
还有一点,一定不要把《深入理解Java虚拟机》和《Java虚拟机规范》搞混了。
- 《Java虚拟机规范》是翻译的官方JVM规范文档,所有的JVM实现都要遵从规范,但有强制要求的规范和建议的规范。
- 《深入理解Java虚拟机》是作者根据自己的理解,结合HotSpot VM的具体实现,为了让读者更容易理解JVM而写的一本书。
你知道Java中final和static修饰的变量是在什么时候赋值的吗?相关推荐
- Java中final和static修饰的变量是在什么时候赋值的?
开始 一位朋友在群里问了这样一个问题: 本着乐于助人的想法,我当时给出的回答: 后来我总觉得哪里不对劲. 于是我仔细翻阅了<Java虚拟机规范>和<深入理解Java虚拟机>关于 ...
- Java中final和static对修饰类、方法、属性的总结
一.final 根据程序上下文环境,Java关键字final有"这是无法改变的"或者"终态的"含义,它可以修饰非抽象类.非抽象类成员方法和变量.你 ...
- java中final,static,this,supper关键字的异同
final可以修饰变量.方法及类: 1.当定义一个final变量时,jvm会将其分配到常量池中,其所修饰的对象只能赋值一次,对基本类型来说是其值不可变,引用类型(包括作为函数形参或类变量):引用不可以 ...
- java中final关键字,修饰变量、引用、类、方法、实例变量的问题
一.final在英语中的意思 final在英语中表示 最终的.不可变更的. 二.final修饰的变量 局部变量: final修饰的局部变量一旦赋值就不能再次赋值,只能赋值一次.若是再次赋值会报错: 实 ...
- Java中final、static关键字的作用
final关键字可用于三个地方: 用于修饰类.类属性和类方法. 被final修饰的类不能被继承,被final修饰的类属性和类方法不能被覆盖(重写): 一.final 1. final修饰变量: fin ...
- [Java基础]final和static修饰符
final: final修饰局部变量时: static: static访问特点:
- JAVA基础--final、static区别以及类加载顺序
一.JAVA中final 与 static 总结 final static 修饰类 该类不可被继承 只能修饰内部类,该类不需要new,是静态加载(嵌套顶层类) 修饰接口 × × 修饰构造函数 × × ...
- java中final关键字的使用
final 中文翻译为 最终的,在java中也是较为常用的关键字之一. 在java 中 final 关键字可以修饰 类.方法.变量 final 修饰在类上,则表示该类不能被继承,如果里面的成员变量没 ...
- Java中的public static final来修饰数组与接口变量
Java中的public static final来修饰数组与接口变量 public static final来修饰数组 final对数组的作用 怎样实现对数组的public static final ...
最新文章
- 逆向-攻防世界-maze
- Django博客系统(首页用户名展示)
- AssetBundle——外部加载资源Asset
- WINDOWS基础 ---- 系统目录
- 玩转12306之查询、订票
- 中科罗伯特工业机器人_「聚焦中日韩产业博览会」丈夫看机器人,妻子忙采购...
- gc频繁的暂停启动_减少主要GC暂停的频率
- easyui Combotree 怎么加载数据 支持多选
- 【2016年第2期】大数据时代下中国社会调查的科学新观
- IT也要健康:帮助你保持健康的几个重要因素
- 云计算将逐渐成为支撑SaaS应用的基础
- 面试技巧(一)〔参加笔试、面试的技巧〕
- IBM 存储管理软件IBM DS Linux Storage Manager安装(Linux)
- 基于Python的开源人脸识别库:离线识别率高达99.38%(转)
- oppo小布机器人_看这一篇就够了,1分钟带你了解OPPO小布的隐藏玩法!
- 【C++入门学习】——MainWindow
- RPA 软件技术是什么意思
- 1000人 规模园区网设计
- 关于求字体识别不出来
- aria2简单下载脚本
热门文章
- find 命令示例_数组find()方法以及JavaScript中的示例
- 利用永恒之蓝入侵服务器复制文件,msf利用永恒之蓝进行漏洞攻击
- 企业办公自动化系统_OA系统的核心功能有哪些?分析当下OA系统的缺陷以及相关解决方案...
- Android view.settran,Android RecyclerView从入门到玩坏
- 大年初一,磊哥给大家发大红包啦!
- nodejs Error: request entity too large解决方案
- MATLAB使用教程
- 字符串匹配之KMP---全力解析
- devstack mysql_DevStack部署OpenStack开发环境 - 问题总结
- 计算机二级2019年9月c语言题库,(3)2019年9月计算机二级C语言试题