你知道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)”階段。
總結
還有一點,一定不要把《深入理解Java虛擬機》和《Java虛擬機規范》搞混了。
- 《Java虛擬機規范》是翻譯的官方JVM規范文檔,所有的JVM實現都要遵從規范,但有強制要求的規范和建議的規范。
- 《深入理解Java虛擬機》是作者根據自己的理解,結合HotSpot VM的具體實現,為了讓讀者更容易理解JVM而寫的一本書。
總結
以上是生活随笔為你收集整理的你知道Java中final和static修饰的变量是在什么时候赋值的吗?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 日期格式化时注解@DateTimeFor
- 下一篇: AI理论知识基础(19)-线性变换(1)