你知道Java中final和static修饰的变量是在什么时候赋值的吗?
開(kāi)始
一位朋友在群里問(wèn)了這樣一個(gè)問(wèn)題:
本著樂(lè)于助人的想法,我當(dāng)時(shí)給出的回答:
后來(lái)我總覺(jué)得哪里不對(duì)勁,仔細(xì)翻閱了《Java虛擬機(jī)規(guī)范》和《深入理解Java虛擬機(jī)》這一部分的內(nèi)容,害!發(fā)現(xiàn)自己理解的有問(wèn)題。
因?yàn)樽约旱睦斫獬鲥e(cuò)而誤導(dǎo)了別人,實(shí)在是讓我萬(wàn)分羞愧!
自己菜但是不能誤導(dǎo)別人,于是我加了這位朋友的好友,向這位朋友表達(dá)了歉意,這位朋友也非常隨和,對(duì)此表示理解。
今天討論的問(wèn)題就是從這個(gè)故事開(kāi)始的。
final修飾的實(shí)例變量
我們先分析一下這個(gè)問(wèn)題:深入Java虛擬機(jī)有一句是“ConstantValue屬性的作用是通知虛擬機(jī)自動(dòng)為靜態(tài)變量賦值,只有被static關(guān)鍵字修飾的變量才可以使用這項(xiàng)屬性。但為什么private final a = 10也可以被賦值?”
我翻閱了《深入理解Java虛擬機(jī)》第二版,在第191頁(yè),確實(shí)有前面那句話
書(shū)中說(shuō)的很清楚,ConstantValue屬性的作用是通知虛擬機(jī)自動(dòng)為靜態(tài)變量賦值。
那就意味著只有static修飾的類變量才會(huì)在class文件中對(duì)應(yīng)的字段表加上ConstantValue屬性嗎?
答案是否定的。用final修飾的實(shí)例變量,編譯成class文件的時(shí)候,對(duì)應(yīng)的字段表也有可能會(huì)加上ConstantValue屬性。
注意,我這里用了“可能”這兩個(gè)字,因?yàn)檫@是有條件的。哪些情況會(huì)有ConstantValue屬性呢?
我們寫(xiě)一段代碼,列舉一下用final修飾的實(shí)例變量的幾種情況,編譯之后,然后用javap -verbose命令查看Java編譯器為我們生成的字節(jié)碼。
我們可以看到,在字段表集合里面有四個(gè)字段表,分表對(duì)應(yīng)這a,b,c,d,e五個(gè)實(shí)例屬性,他們都帶有ACC_PUBLIC(public)和ACC_FINAL(final)的訪問(wèn)標(biāo)志。但只有a和b對(duì)應(yīng)的字段表帶有ConstantValue屬性。我們總結(jié)一下:
用final修飾不是在構(gòu)造方法賦值的String類型或者基本類型成員變量,編譯成字節(jié)碼文件時(shí),對(duì)應(yīng)的字段表也會(huì)帶有ConstantValue屬性。
這個(gè)結(jié)論不和《深入理解Java虛擬機(jī)》沖突嗎?
于是我翻閱了JVM Spec Java SE 8Edition(周志明前輩是翻譯過(guò),書(shū)名《Java虛擬機(jī)規(guī)范》,但是我手里沒(méi)有翻譯后的中文版),在4.7.2部分我找到了這樣一句話:
書(shū)中說(shuō)的很清楚,如果field_info(字段表)表示的非靜態(tài)字段包含了ConstantValue屬性,那么這個(gè)ConstantValue屬性會(huì)被Java虛擬機(jī)所忽略。也就是說(shuō),對(duì)于非靜態(tài)字段,就算你編譯器加上了ConstantValue屬性,JVM也會(huì)忽略掉,你加不加結(jié)果是一樣的。
看完《Java虛擬機(jī)規(guī)范》里面的說(shuō)明,再回來(lái)看《深入理解Java虛擬機(jī)》里面的這句話:
ConstantValue屬性的作用是通知虛擬機(jī)自動(dòng)為靜態(tài)變量賦值,只有被static關(guān)鍵字修飾的類變量才可以使用這項(xiàng)屬性。
作者的這句話的前半句沒(méi)有什么爭(zhēng)議,但我覺(jué)得后半句的表述的不太明確,容易造成誤解。
以我的理解,應(yīng)該是“只有被static關(guān)鍵字修飾的類變量才可以使用這項(xiàng)屬性來(lái)進(jìn)行初始化,否則使用這項(xiàng)屬性也會(huì)被JVM忽略掉”
好了,我們?cè)倩氐侥俏慌笥褑?wèn)的問(wèn)題:為什么private final a = 10也可以被賦值?
首先,這個(gè)問(wèn)題的本身就問(wèn)的不太準(zhǔn)確。我理解這位朋友真正想問(wèn)的是“為什么private final a = 10也可以通過(guò)ConstantValue屬性的形式賦值?”
我覺(jué)得這是一個(gè)很好的問(wèn)題,這位朋友通過(guò)實(shí)驗(yàn)發(fā)現(xiàn)用final修飾的實(shí)例變量對(duì)應(yīng)的字段表有ConstantValue屬性,結(jié)合《深入理解Java虛擬機(jī)》,他認(rèn)為a是通過(guò)ConstantValue屬性讓虛擬機(jī)知道然后為其賦值的。最后他發(fā)現(xiàn)和書(shū)中沖突,于是提出了上文的這個(gè)問(wèn)題。
這樣的思路有問(wèn)題嗎?我覺(jué)得是沒(méi)有問(wèn)題的。
不過(guò)這樣的理解是對(duì)的嗎?顯然是不對(duì)的。
因?yàn)樘摂M機(jī)規(guī)范是這樣規(guī)范的。對(duì)于非靜態(tài)字段,ConstantValue屬性是不會(huì)生效的。
至于為什么要這樣設(shè)計(jì),功力不夠的我暫時(shí)無(wú)法理解設(shè)計(jì)者的想法。
那單獨(dú)用final修飾的實(shí)例變量到底是在什么時(shí)候賦值的呢?
這個(gè)問(wèn)題也不難回答,看一下字節(jié)碼就清楚了。
通過(guò)查看字節(jié)碼,我們可以看到有一個(gè)方法,右邊是它的字節(jié)碼指令。
什么是方法?我們看看Java虛擬機(jī)規(guī)范上的解釋:
我們溫習(xí)一下這個(gè)英語(yǔ)四級(jí)短語(yǔ):appear as
然后,我們一起翻譯一下:在JVM層面上,每一個(gè)用Java寫(xiě)的構(gòu)造方法都表現(xiàn)為實(shí)例初始方法,這個(gè)方法就是方法。
記住,這個(gè)方法會(huì)在實(shí)例初始化的時(shí)候被調(diào)用。
我們?cè)賮?lái)看一下putfield這個(gè)字節(jié)碼指令的含義:putfield指令就是為指定的類的實(shí)例域賦值的,也就是為實(shí)例變量賦值的指令。
現(xiàn)在我們可以清晰的知道,這些用final修飾實(shí)例變量是在實(shí)例構(gòu)造器方法里面賦值的,也就是對(duì)象創(chuàng)建的時(shí)候賦值。
static修飾的類變量
上面講到ConstantValue屬性的作用是通知虛擬機(jī)自動(dòng)為靜態(tài)變量賦值。
我們?cè)倩剡^(guò)來(lái)講一下靜態(tài)變量,一個(gè)很關(guān)鍵的關(guān)鍵字static。
在這之前,我需要把類加載的幾個(gè)過(guò)程大致給你講一下:
類的生命周期由7個(gè)階段組成,類加載說(shuō)的是前5個(gè)階段,即加載—>驗(yàn)證—>準(zhǔn)備—>解析—>初始化。
類的生命周期圖
我們簡(jiǎn)單過(guò)一下這幾個(gè)階段:
- 加載:將字節(jié)碼所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
- 驗(yàn)證:驗(yàn)證字節(jié)碼格式,確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。
- 準(zhǔn)備:創(chuàng)建類或者接口的靜態(tài)字段,并為靜態(tài)變量設(shè)置初始值。
- 解析:將常量池內(nèi)的符號(hào)引用替換為直接引用。
- 初始化:執(zhí)行類構(gòu)造器方法。
類構(gòu)造器方法又是個(gè)什么東西呢?
JVM Spec Java SE 8Edition這樣說(shuō)道:
說(shuō)白了,編譯器會(huì)收集所有靜態(tài)變量的賦值動(dòng)作、所有靜態(tài)代碼塊,合并產(chǎn)生一個(gè)方法,即方法。這個(gè)方法在類加載的初始化階段執(zhí)行。
而對(duì)于類變量(static修飾的),則有兩種賦值方式可以選擇:
- 使用ConstantValue屬性賦值。
- 在類構(gòu)造器方法中賦值。
目前Oracle公司實(shí)現(xiàn)的Javac編譯器的選擇是:
- final+static修飾:使用ConstantValue屬性賦值。
- 僅僅使用static修飾:在方法中賦值。
需要注意點(diǎn)的是,用生成ConstantValue屬性來(lái)進(jìn)行初始化,這個(gè)變量必須是基本類型或者java.lang.String類型。
這是因?yàn)镃lass文件格式的常量類型中只有與基本屬性和字符串相對(duì)應(yīng)的字面量,所以就算ConstantValue屬性想支持別的類型也無(wú)能為力。
對(duì)于這一點(diǎn),我們也可以通過(guò)javap -verbose命令反編譯驗(yàn)證一下:
final+static修飾的常量
上面我們說(shuō)過(guò),方法是在類加載的出初始化階段賦值的。
那static+final修飾的常量是在類加載的那一階段進(jìn)行的呢?我們可以看一下JVM規(guī)范:
我們可以看到在JVM規(guī)范里面,static+final修飾的常量是在初始化階段執(zhí)行方法之前執(zhí)行的。
咦?我們平時(shí)背的不都是在類加載的準(zhǔn)備階段會(huì)對(duì)普通類屬性賦初始值,帶有ConstantValue的類屬性直接賦值嗎?
《深入理解Java虛擬機(jī)》也是這樣說(shuō)的啊?
書(shū)上是錯(cuò)的嗎?不是的,因?yàn)椤渡钊肜斫釰ava虛擬機(jī)》里面講的具體實(shí)現(xiàn),是基于HotSpot VM講的。
確確實(shí)實(shí),HotSpot VM就是這么干的,我們也可以在openJdk中找到對(duì)應(yīng)的源碼:
看起來(lái),HotSpot VM對(duì)基本類型或者字符串類型的常量的賦值確實(shí)在準(zhǔn)備階段完成了。
但一個(gè)很關(guān)鍵的點(diǎn)是,仍然在調(diào)用之前賦值了。
外界是不會(huì)觀察到HotSpot VM提前做了這個(gè)初始化賦值的,所以是沒(méi)問(wèn)題。
不過(guò)要記住的是,規(guī)范里明確說(shuō)了正確的初始化時(shí)機(jī)是在“初始化(Initialization)”階段。
總結(jié)
還有一點(diǎn),一定不要把《深入理解Java虛擬機(jī)》和《Java虛擬機(jī)規(guī)范》搞混了。
- 《Java虛擬機(jī)規(guī)范》是翻譯的官方JVM規(guī)范文檔,所有的JVM實(shí)現(xiàn)都要遵從規(guī)范,但有強(qiáng)制要求的規(guī)范和建議的規(guī)范。
- 《深入理解Java虛擬機(jī)》是作者根據(jù)自己的理解,結(jié)合HotSpot VM的具體實(shí)現(xiàn),為了讓讀者更容易理解JVM而寫(xiě)的一本書(shū)。
總結(jié)
以上是生活随笔為你收集整理的你知道Java中final和static修饰的变量是在什么时候赋值的吗?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 日期格式化时注解@DateTimeFor
- 下一篇: Java利用stream(流)对map中