static 和 final 關鍵字 對實例變數賦初始值的影響 最近一直在看《深入理解Java虛擬機》,在看完了對象記憶體分配、Class文件格式之後,想深扒一下實例變數是如何被賦上初始值的這個問題的細節。 在2.3.1小節中講對象創建的時候,講到記憶體分配有兩種方式:一種是指針碰撞;另一種是空閑列表。 ...
static 和 final 關鍵字 對實例變數賦初始值的影響
最近一直在看《深入理解Java虛擬機》,在看完了對象記憶體分配、Class文件格式之後,想深扒一下實例變數是如何被賦上初始值的這個問題的細節。
在2.3.1小節中講對象創建的時候,講到記憶體分配有兩種方式:一種是指針碰撞;另一種是空閑列表。
而選擇哪種分配方式是由JAVA堆是否規整決定,而JAVA堆是否規整則由虛擬機所採用的垃圾收集器是否帶壓縮整理功能決定。
我們不管記憶體分配採用何種方式,當記憶體分配完成後,虛擬機將分配到的記憶體空間都初始化為零值,這裡的零值代表各個類型的初始值。比如 int類型的實例變數的初始值為0,boolean類型的預設初始值為false。因此,這裡的初始值,是從虛擬視角看到的初始值。但是從JAVA程式視角來看,我們給變數賦上的值,是在 <init>
方法中進行的。下麵討論從JAVA程式視角看,實例變數如何被賦上初始值的。
如果一個實例變數在被final修飾後,該變數賦初始值的時候,要麼在聲明實例變數的時候賦值,如下所示:
public class VariableTest {
private final int a;//編譯期報錯:Variable a may not be initialized
}
public class VariableTest {
private final int a = 127;//在聲明實例變數的時候賦值
}
或者在構造方法中為變數賦初始值:
public class VariableTest {
private final int a ;
public VariableTest() {
a = 127;
}
}
這與 final 關鍵字的“不可變性”有關,至於加了final修飾後,在編譯或者運行的時候,對這個變數的影響是什麼,我就不知道怎麼解釋了。
接下來,再來看,在實例變數上使用 static 修飾,該實例變數就變成了類變數。
在第6章中講到:
對於非 static 類型的變數的賦值是在實例構造器
<init>
方法中進行的;----這與我們前面的描述相符。對於類變數有兩種方式賦初始值:
- 在類構造器
<clinit>
方法中賦值- 使用ConstantValue屬性賦值
而對於類變數的這兩種賦值方式,選用哪一種是則編譯器來決定的:對於Sun javac編譯器,當實例變數使用static 和 final 修飾的時候,就會生成 ConstantValue屬性為之賦值。
但是,這裡需要註意的是:只有基本類型(int、long...)和String類型才能生成 ConstantValue屬性。如下所示:
public class VariableTest {
private static final Integer b = 1;
private static final int a = 1;
private static final String s = "test";
}
(我猜想基本類型的包裝類型,比如int類型的包裝類Integer,應該也是採用ConstantValue屬性方式來賦初始值的吧)
那這裡就有個問題:為什麼只有基本類型(int、long..)和String類型才能生成 ConstantValue屬性呢?
下麵來說下ConstantValue屬性是個啥?
從頭說起:class文件結構中有一個常量池,常量池存放 字面量 和 符號引用。字面量就是下麵的一些“字元串”
public class VariableTest {
private static final String s = "test";
}
那"test" 就是一個字面量。
那符號引用是啥呢?有這三類:
- 類和介面的全限定名稱
- 欄位的名稱和描述符
- 方法的名稱和描述符
那描述符又是什麼呢?
描述符 的作用是 用來描述欄位的數據類型、方法的參數列表(包括數量、類型、參數順序)和返回值
描述符 就是按某種規則 來描述欄位或者方法。欄位就是我們寫代碼時定義的某個實例變數。
舉例來說:
public class VariableTest {
public void inc() {
}
}
上面那個inc方法的描述符就是:()V
,為什麼是這樣呢?那就是書上177頁解釋嘛。
感覺有點跑偏了。
class文件格式裡面,有一種存儲結構叫做 方法表,方法表裡面有一個 attribute_info 類型的 屬性(暫且叫屬性吧,參考書上表6-11)。
這個attribute_info 類型的 屬性 是一個屬性集合,裡面又定義了若幹屬性(參考表6-13),其中就有一個名為ConstantValue的屬性,而ConstantValue屬性的 值是一個常量池的索引號,而根據:表6-3常量池的項目類型來看,只有一些:CONSTANT_Utf8_info、CONSTANT_Integer_info、CONSTANT_Long_info…類型的字面量。(CONSTANT_Utf8_info 對應String類型 字面量)
因此,:只有基本類型(int、long..)和String類型才能生成 ConstantValue屬性了。
既然常量池是存儲字面量和符號引用的,而ConstantValue屬性值 就指向了常量池中的索引號,這就是一個被static final 修飾的實例變數的 初始值 的來源了(或者說:一個被static final修飾的 基本類型/String類型的 變數的初始值,其實是存儲在Class文件的常量池中的)。
介紹到這裡,那如果一個實例變數沒有使用final修飾,而只使用了static修飾,那它就應該在類構造器clinit
方法中賦初始值了。
最後總結一下:
根據實例變數的類型來分:一類是 基本類型和String類型;另一類是 引用類型(比如自定義的類)
如果一個實例變數被static修飾,那它就是一個類變數。
對於 基本類型和String類型的實例變數:
- 該實例變數被 static 修飾了,則在
clinit
方法中賦初始值(類變數嘛) - 該實例變數被 final 修飾了,在
init
方法中賦初始值(實例變數嘛) - 該實例變數同時被 static 和 final 修飾了,通過ConstantValue屬性方式賦初始值(有相應的字面量類型支持嘛)
對於 其他引用類型的實例變數:
- 該實例變數被 static 修飾了,則在
clinit
方法中賦初始值(類變數嘛) - 該實例變數被 final 修飾了,在
init
方法中賦初始值(實例變數嘛) - 該實例變數同時被 static 和 final 修飾了,在
clinit
方法中賦初始值
說了這麼多,final關鍵字如何影響 類變數的初始化呢?
在7.2節中說的:類載入的時機,給出了一張類的生命周期的示意圖:
上面有一個準備階段,還有一個初始化的階段。
如果一個類變數沒有被final修飾,如下:
public class VariableTest {
private static int a = 123;
}
類變數a 在準備階段,被賦值為0,在初始化階段被賦值為123
如果該類變數被final修飾了,如下:
public class VariableTest {
private static final int a = 123;
}
類變數a 在準備階段就被賦值為123。這是因為 a 是一個基本類型的變數,被static 和 final 修飾後,能夠生成ConstantValue屬性。
那麼我想對於下麵這個 mylist 這個變數:在準備階段應該被賦值為null,在初始化階段被賦值為 指向一個ArrayList對象吧。
public class VariableTest {
private static final List<String> mylist = new ArrayList<>();
}
參考:《深入理解Java虛擬機》