天天看點

淺談java中final資料引言一、編譯時的常量二、運作時被初始化參考:

引言

在說說final之前,我們先了解下類被加載到記憶體中所需要的幾個步驟,一個類被加載到記憶體中需要經過如下幾個階段:

  • 編譯: java檔案必須編譯成Class檔案(也稱為位元組碼檔案)才可以被JVM識别,JVM并不關心Class的來源是什麼語言,隻要它符合一定的結構,就可以在Java中運作。
  • 裝載:查找和導入必要的Class檔案,在Java虛拟機執行過程中,隻有他需要一個類的時候,才會調用類加載器來加載這個類,并不會在開始運作時加載所有的類。
  • 連結: 檢查載入Class檔案資料的正确性、靜态變量配置設定記憶體空間,并設定預設值、将符号引用轉成直接引用
  • 初始化:對類的靜态變量,靜态代碼塊執行初始化操作

了解完成上面後,我們進入final,一個被final修飾的資料會有兩種情況的存在:

  1. 永不改變的編譯時常量
  2. 在運作時被初始化的值

一、編譯時的常量

對于編譯時常量這種情況就在于編譯成Class檔案後,常量的值就已經存在于編譯時常量池中了。當我們使用該常量時,類就不需要為它進行任何初始化操作,我們直接拿來用就行了。要成為編譯時的常量必須要滿足以下這點:

  1. 必須關鍵字final修飾
  2. 資料類型必須基本資料類型或String類型
  3. 對這個常量進行定義的時候,必須對其進行指派

下面的例子在編譯的時候就會生成編譯時的常量:

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》