引言
在說說final之前,我們先了解下類被加載到記憶體中所需要的幾個步驟,一個類被加載到記憶體中需要經過如下幾個階段:
- 編譯: java檔案必須編譯成Class檔案(也稱為位元組碼檔案)才可以被JVM識别,JVM并不關心Class的來源是什麼語言,隻要它符合一定的結構,就可以在Java中運作。
- 裝載:查找和導入必要的Class檔案,在Java虛拟機執行過程中,隻有他需要一個類的時候,才會調用類加載器來加載這個類,并不會在開始運作時加載所有的類。
- 連結: 檢查載入Class檔案資料的正确性、靜态變量配置設定記憶體空間,并設定預設值、将符号引用轉成直接引用
- 初始化:對類的靜态變量,靜态代碼塊執行初始化操作
了解完成上面後,我們進入final,一個被final修飾的資料會有兩種情況的存在:
- 永不改變的編譯時常量
- 在運作時被初始化的值
一、編譯時的常量
對于編譯時常量這種情況就在于編譯成Class檔案後,常量的值就已經存在于編譯時常量池中了。當我們使用該常量時,類就不需要為它進行任何初始化操作,我們直接拿來用就行了。要成為編譯時的常量必須要滿足以下這點:
- 必須關鍵字final修飾
- 資料類型必須基本資料類型或String類型
- 對這個常量進行定義的時候,必須對其進行指派
下面的例子在編譯的時候就會生成編譯時的常量:
package test;
public class Test {
private final String valueOne = "aaaaaaa";
public final static String VALUE_TWO = "bbbbbb";
}
我們通過javap -c -v -p Test 對Class檔案進行反彙編,并隻列出了關鍵的部分:
public class test.Test
SourceFile: "Test.java"
minor version:
major version:
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
....
#21 = Utf8 aaaaaaa
....
#25 = Utf8 bbbbbb
{
private final java.lang.String valueOne;
flags: ACC_PRIVATE, ACC_FINAL
ConstantValue: String aaaaaaa
public static final java.lang.String VALUE_TWO;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: String bbbbbb
....
}
我們可以看出編譯後常量的值aaaaaaa 和 bbbbbb已經存在于Constant pool中。以後在類裝載的時候就不需要為這兩個變量進行初始化的操作了。
注意,我們發現這兩個常量字段被ConstantValue屬性所修飾,根據深入了解JVM這本書中所描述:
ConstantValue屬性的作用是通知虛拟機自動為靜态變量指派,隻有被static修飾的變量才可以使用這項屬性。
也就是說static修飾的變量才可以使用ConstantValue的屬性,可是我們上面valueOne變量明明是非static修飾,難道java7以後對ConstantValue屬性進行了重新定義也允許非static的final變量使用該屬性? 這點暫時還沒搞懂,後再查查資料。
編譯時的常量還有個特點:常量在編譯階段會存入調用它的類的常量池中,是以不會觸發定義常量的類的初始化。
在Test類中定義了一個常量:
public class Test {
public final static String VALUE_TWO = "bbbbbb";
}
在Main類中使用Test類的常量:
public class Main {
public static void main(String[] args) {
System.out.println(Test.VALUE_TWO);
}
}
我們知道一個類要使用另一個類時,必須要把該類裝載進來,并進行初始操作,方可以使用,但是如果調用編譯時的常量則不需要。在編譯階段會将此常量的值“bbbbbb”存儲到了調用它的類Main 的常量池中,對常量Test.VALUE_TWO 的引用實際上轉化為了Main 類對自身常量池的引用。也就是說,實際上Main 的Class檔案之中并沒有Test 類的符号引用入口,這兩個類在編譯成Class檔案後就不存在任何聯系了。
我們通過javap 反彙編Main位元組碼檔案就可以直覺的看出:
public class test.Main
SourceFile: "Main.java"
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
.....
#4 = String #25 // bbbbbb
......
#25 = Utf8 bbbbbb
{
public test.Main();
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=, locals=, args_size=
: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
: ldc #4 // String bbbbbb
: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
: return
LineNumberTable:
line :
line :
LocalVariableTable:
Start Length Slot Name Signature
args [Ljava/lang/String;
}
bbbbbb值直接存儲在了Main的常量池中了,是以跟Test 類沒有任何關系。
二、運作時被初始化
我們并不能因為某資料是final的就認為在編譯的時候可以知道它的值了,就如下面:
public class Test {
public static final int INT_1 = new Random().nextInt();
public final int i1 = new Random().nextInt();
}
我們說過要成為編譯時常量的條件類型必須基本類型或String,很明顯上面的例子不符合這個條件。上面的例子隻有當在運作時才會進行初始化。 INT_1 變量在類被裝載時已經被初始化,每次調用該變量數值都是一樣的。而i1的初始化的階段則在于建立執行個體對象時,是以每個對象的i1值都是不一樣的:
public static void main(String[] args) {
Test test=new Test();
System.out.println(test.i1);
test=new Test();
System.out.println(test.i1);
test=new Test();
System.out.println(test.i1);
}
列印後的值分别是2145916241 、 465177656 、 -680509643。
空白final
java 允許生成空白final,空白final是指被聲明為final但又未給定初始化值的域。 無論什麼情況,編譯器都要確定空白final在使用前必須被初始化,是以我們可以不必在聲明final變量時就給定值,可以在後面再進行指派,這給關鍵字final的使用上提供了更大的靈活性。
- 對于static修飾的空白final字段,我們隻能在static代碼塊中進行指派:
public class Test {
public static final String INT_1;
static {
INT_1 = "aaaaaaaa";
}
}
- 對于非static修飾的空白final字段,我們可以在動态代碼塊 或 構造器中進行指派:
public class Test {
public final String i1;
{
i1="bbbbbbbbbb";
}
}
或
public class Test {
public final String i1;
public Test(){
i1="bbbbbbbb";
}
}
- 對于局部變量,編譯器也允許先聲明後指派:
public static void main(String[] args) {
final int a;
a=;
System.out.println(a);
}
不管哪種情況編譯器總要保證被final修飾的變量,在使用前必須先進行指派操作,才可編譯通過。
參考:
《Thinking in Java》