1. 程式人生 > >聊聊Java的常量池

聊聊Java的常量池

目錄

什麼是常量和常量值

常量是指在程式的整個執行過程中值保持不變的量。在這裡要注意常量和常量值是不同的概念,常量值是常量的具體和直觀的表現形式,常量是形式化的表現。通常在程式中既可以直接使用常量值,也可以使用常量。

通常也有把常量值和常量統稱為字面量

常量值

常量值又稱為字面常量,它是通過資料直接表示的,因此有很多種資料型別,像整型和字串型等。下面一一介紹這些常量值。

整型常量值

Java 的整型常量值主要有如下 3 種形式。

  • 十進位制數形式:如 54、-67、0。
  • 八進位制數形式:Java 中的八進位制常數的表示以 0 開頭,如 0125 表示十進位制數 85,-013 表示十進位制數 11。
  • 十六進位制數形式:Java 中的十六進位制常數的表示以 0x 或 0X 開頭,如 0x100 表示十進位制數 256,-0x16 表示十進位制數 -22。

整型(int)常量預設在記憶體中佔 32 位,是具有整數型別的值,當運算過程中所需值超過 32 位長度時,可以把它表示為長整型(long)數值。長整型型別則要在數字後面加 L 或 1, 如 697L,表示一個長整型數,它在記憶體中佔 64 位。

實型常量值

Java 的實型常量值主要有如下兩種形式。

  • 十進位制數形式:由數字和小數點組成,且必須有小數點,如 12.34、-98.0。
  • 科學記數法形式:如 1.75e5 或 32&E3,其中 e 或 E 之前必須有數字,且 e 或 E 之後的數字必須為整數。

Java 實型常量預設在記憶體中佔 64 位,是具有雙精度型(double)的值。如果考慮到需要節省執行時的系統資源,而運算時的資料值取值範圍並不大且運算精度要求不太高的情況,可以把它表示為單精度型(float)的數值。

單精度型數值一般要在該常數後面加 F 或 f,如 69.7f,表示一個 float 型實數,它在記憶體中佔 32 位(取決於系統的版本高低)。

布林型常量值

Java 的布林型常量只有兩個值,即 false(假)和 true(真)。

字元型和字串常量值

Java 的字元型常量值是用單引號引起來的一個字元,如 ‘e’、E’。需要注意的是,Java 字串常量值中的單引號和雙引號不可混用。雙引號用來表示字串,像 “11”、“d” 等都是表示單個字元的字串。

除了以上所述形式的字元常量值之外,Java 還允許使用一種特殊形式的字元常量值來表示一些難以用一般字元表示的字元,這種特殊形式的字元是以開頭的字元序列,稱為轉義字元。

常量

常量不同於常量值,它可以在程式中用符號來代替常量值使用,因此在使用前必須先定義。

Java 語言使用 final 關鍵字來定義一個常量,其語法如下所示:

final dataType variableName

其中,final 是定義常量的關鍵字,dataType 指明常量的資料型別,variableName 是變數的名稱。

例如,以下語句使用 final 關鍵字宣告常量。

final int COUNT = 10;
final float HEIGHT = 10.2f;

在定義常量時,需要注意如下內容:

  • 在定義常量時就需要對該常量進行初始化。
  • final 關鍵字不僅可以用來修飾基本資料型別的常量,還可以用來修飾物件的引用或者方法。
  • 為了與變數區別,常量取名一般都用大寫字元。

當常量被設定後,一般情況下不允許再進行更改,如果更改其值將提示錯誤。例如,以下語句定義常量 AGE 並賦予初值,如果更改 AGE 的值,那麼在編譯時將提示錯誤。

final int AGE = 10;
AGE = 11;

常量池

常量池在Java用於儲存在編譯期已確定的,已編譯的class檔案中的一份資料。它包括了關於類,方法,介面等中的常量,也包括字串常量,如String s = "java"這種申明方式;當然也可擴充,執行器產生的常量也會放入常量池,故認為常量池是JVM的一塊特殊的記憶體空間。

常量池形態

Java中的常量池,實際上分為兩種形態:靜態常量池和執行時常量池。

靜態常量池

所謂靜態常量池,即*.class檔案中的常量池,class檔案中的常量池不僅僅包含字串(數字)字面量,還包含類、方法的資訊,佔用class檔案絕大部分空間。這種常量池主要用於存放兩大類常量:字面量(Literal)符號引用量(Symbolic References),字面量相當於Java語言層面常量的概念,如文字字串,宣告為final的常量值等,符號引用則屬於編譯原理方面的概念,包括瞭如下三種類型的常量:

  • 類和介面的全限定名
  • 欄位名稱和描述符
  • 方法名稱和描述符

執行時常量池

而執行時常量池,則是JVM虛擬機器在完成類裝載操作後,將class檔案中的常量池載入到記憶體中,並儲存在方法區中,我們常說的常量池,就是指方法區中的執行時常量池。

執行時常量池相對於class檔案常量池的另外一個重要特徵是具備動態性,Java語言並不要求常量一定只有編譯期才能產生,也就是並非預置入class檔案中常量池的內容才能進入方法區執行時常量池,執行期間也可能將新的常量放入池中,這種特性被開發人員利用比較多的就是String類的intern()方法。
String的intern()方法會查詢在常量池中是否存在一份equal相等的字串,如果有則返回該字串的引用,如果沒有則新增自己的字串進入常量池。

常量池的好處

常量池是為了避免頻繁的建立和銷燬物件而影響系統性能,其實現了物件的共享。
例如字串常量池,在編譯階段就把所有的字串文字放到一個常量池中。

  • 節省記憶體空間:常量池中所有相同的字串常量被合併,只佔用一個空間。
  • 節省執行時間:比較字串時,== 比equals()快。對於兩個引用變數,只用==判斷引用是否相等,也就可以判斷實際值是否相等。

常量池的位置

這個問題的回答眾說紛紜,個人覺得一個比較靠譜的回答:

  • Java8之前,常量池是存放在堆中的,常量池就相當於是在永久代中,所以永久代存放在堆中。
  • Java8之後,取消了整個永久代區域,取而代之的是元空間。常量池就不存放在堆中了,而是存放在方法區裡面,與堆疊是並列關係。永久代也就不存放在堆中了。

原文:JVM中常量池存放在哪裡

通過例子瞭解常量池

網路上流行的常量池例子

@Test
public void constantPoolTest() {
    String s1 = "Hello";
    String s2 = "Hello";
    String s3 = "Hel" + "lo";
    String s4 = "Hel" + new String("lo");
    String s5 = new String("Hello");
    String s6 = s5.intern();
    String s7 = "H";
    String s8 = "ello";
    String s9 = s7 + s8;

    System.out.println(s1 == s2);  // true
    System.out.println(s1 == s3);  // true
    System.out.println(s1 == s4);  // false
    System.out.println(s1 == s9);  // false
    System.out.println(s4 == s5);  // false
    System.out.println(s1 == s6);  // true
}

首先說明一點,在Java 中物件直接使用==操作符,比較的是兩個物件的引用地址,並不是比較內容,比較內容請用equals()。

  • s1 == s2這個非常好理解,s1、s2在賦值時,均使用的字串字面量,說白話點,就是直接把字串寫死,在編譯期間,這種字面量會直接放入class檔案的常量池中,從而實現複用,載入執行時常量池後,s1、s2指向的是同一個記憶體地址,所以相等。
  • s1 == s3這個地方有個坑,s3雖然是動態拼接出來的字串,但是所有參與拼接的部分都是已知的字面量,在編譯期間,這種拼接會被優化,編譯器直接幫你拼好,因此String s3 = “Hel” + "lo"在class檔案中被優化成String s3 = “Hello”,所以s1 == s3成立。
  • s1 == s4當然不相等,s4雖然也是拼接出來的,但new String(“lo”)這部分不是已知字面量,是一個不可預料的部分,編譯器不會優化,必須等到執行時才可以確定結果,結合字串不變定理,鬼知道s4被分配到哪去了,所以地址肯定不同。
  • s1 == s9也不相等,道理差不多,雖然s7、s8在賦值的時候使用的字串字面量,但是拼接成s9的時候,s7、s8作為兩個變數,都是不可預料的,編譯器畢竟是編譯器,不可能當直譯器用,所以不做優化,等到執行時,s7、s8拼接成的新字串,在堆中地址不確定,不可能與方法區常量池中的s1地址相同。
  • s4 == s5已經不用解釋了,絕對不相等,二者都在堆中,但地址不同。
  • s1 == s6這兩個相等完全歸功於intern方法,s5在堆中,內容為Hello ,intern方法會嘗試將Hello字串新增到常量池中,並返回其在常量池中的地址,因為常量池中已經有了Hello字串,所以intern方法直接返回地址;而s1在編譯期就已經指向常量池了,因此s1和s6指向同一地址,相等。

特例1

public static final String A = "ab"; // 常量A
public static final String B = "cd"; // 常量B
public static void main(String[] args) {
	String s = A + B;  // 將兩個常量用+連線對s進行初始化 
	String t = "abcd";   
    if (s == t) {   
    	System.out.println("s等於t,它們是同一個物件"); // print   
    } else {   
        System.out.println("s不等於t,它們不是同一個物件");   
    }   
} 

A和B都是常量,值是固定的,因此s的值也是固定的,它在類被編譯時就已經確定了。
也就是說:String s = A + B; 等同於:String s = “ab” + “cd”;

特例2

public static final String A; // 常量A
public static final String B;    // 常量B
static {   
	A = "ab";   
	B = "cd";   
}   
public static void main(String[] args) {   
   // 將兩個常量用+連線對s進行初始化   
	String s = A + B;   
	String t = "abcd";   
    if (s == t) {   
    	System.out.println("s等於t,它們是同一個物件");   
    } else {   
        System.out.println("s不等於t,它們不是同一個物件"); // print   
    }   
} 

A和B雖然被定義為常量,但是它們都沒有馬上被賦值。在運算出s的值之前,他們何時被賦值,以及被賦予什麼樣的值,都是個變數。因此A和B在被賦值之前,性質類似於一個變數。那麼s就不能在編譯期被確定,而只能在執行時被建立了。

至此,我們可以得出三個非常重要的結論:

  • 必須要關注編譯期的行為,才能更好的理解常量池。
  • 執行時常量池中的常量,基本來源於各個class檔案中的常量池。
  • 程式執行時,除非手動向常量池中新增常量(比如呼叫intern方法),否則JVM不會自動新增常量到常量池。

以上所講僅涉及字串常量池,實際上還有整型常量池、浮點型常量池。Java中包裝類的大部分都實現了常量池技術,如Byte,Short,Integer,Long,Character,Boolean。

你可能感興趣:

參考: