Java常量池

  一.相關知識

  1.何爲常量

  第一種常量:是一個值,我們將這個值本身稱爲常量。比如:

整型常量:1024
實型常量:1.024
字符常量:'g' 'c' 'w'
字符串常量:"gcw"
邏輯常量:true false

   例如,我們可以將數字1024稱爲一個int類型的常量。
  第二種常量:不可變的變量,我們也稱爲常量。被關鍵字final修飾的變量,其值就不可以改變。可能它本身是個變量,但是被final修飾後,我們就可以認爲它是個常量。比如:

final int i=1024;

   2.常量池

  常量池分爲兩種:靜態常量池和運行時常量池。
  (1)靜態常量池也就是Class字節碼文件中的常量池。我們舉一個簡單的例子,下面是一個HelloWorld的源文件和Class文件。
  源文件:

public class HelloWorld{
    public static void main(String args[]){
        System.out.println("hello world");
    }
}

   class文件:

  我們對class文件中的標識符予以一一分析。

  ①魔數
  魔數是class字節碼文件中的前四個字節:ca fe ba be(漱壕)。它的唯一作用是確定這個文件是否可以被JVM接受。很多文件存儲標準中都使用魔數來進行身份識別。
  ②版本號
  第5和第6個字節是次版本號,第7個和第8 個是主版本號。這裏的第7和第8位是0034,即:0x0034。0x0034轉爲10進制是52。Java的版本是從45開始的然而從1.0 到1.1 是45.0到45.3, 之後就是1.2 對應46, 1.3 對應47 … 1.6 對應50,這裏的1.6.0_24對應的是52,就是0x0034;
  ③常量池的入口
  由於常量池中的常量的數量不是固定的,所以常量池的入口需要放置一項u2類型的數據,代表常量池的容量計數值。這裏的常量池容量計數值是從1開始的。如圖常量池的容量:0x001d(29)。所以共有29個常量。
  ④常量池
  常量池中主要存放兩類常量:字面量和符號引用。字面量是比較接近Java語言層面的常量概念,也就是我們提到的常量。符號引用則屬於編譯原理的方面的概念,包括三類常量:類和接口的全限定名;字段的名稱和描述符;方法的名稱和描述符。

  (2)運行時常量池:運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外還有一項信息是常量池,它用於存放編譯期生成的字面量和符號應用,這部分內容將在類加載後進入方法區的時候存到運行時常量池中。運行時常量池還有個更重要的的特徵:動態性。Java要求,編譯期的常量池的內容可以進入運行時常量池,運行時產生的常量也可以放入池中。常用的是String類的intern()方法【當調用 intern() 方法時,編譯器會將字符串添加到常量池中(stringTable維護),並返回指向該常量的引用。 】。

  3.常量池的好處
  常量池是爲了避免頻繁地創建和銷燬對象而影響系統性能,其實現了對象的共享。
  例如字符串常量池,在編譯階段就把所有的字符串文字放到一個常量池中。
  (1)節省內存空間:常量池中所有相同的字符串常量被合併,只佔用一個空間。
  (2)節省運行時間:比較字符串時,==比equals()快。對於兩個引用變量,只用==判斷引用是否相等,也就可以判斷實際值是否相等。

  4.equals和==的區別
   Java中的數據類型分兩種:基本數據類型和引用數據類型。
  (1)基本數據類型共8種:byte short int long char float double boolean。
  對於基本數據類型的比較,都是用==來比較兩者的值是不是相等。
  (2)引用數據類型。
  一般情況下,equals和==是一樣的,都是比較的兩者的地址值是不是一樣。但是也有特殊情況,比如,我們都知道所有類都是繼承自Object基類,Object中的equals方法中是使用==來實現的,即比較的是兩者的地址值。但是,Object的子類可以重寫equals方法,比如Date、String、Integer等類都是重寫了equals()方法,比較的是值是否相等。例如,在String類的equals()源碼中,先比較是不是指向同一個地址,如果不是再比較兩者是不是值相等。這個時候,equals和==所表達的含義顯然就不一樣了。  

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String) anObject;
            int n = count;
            if (n == anotherString.count) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = offset;
                int j = anotherString.offset;
                while (n-- != 0) {
                    if (v1[i++] != v2[j++])
                        return false;
                }
                return true;
            }
        }
        return false;
    }

   二.8種基本數據類型

  8種基本數據類型都有自己的包裝類,其中有6個包裝類(Byte,Short,Integer,Long,Character,Boolean)實現了常量池技術。舉例來說,通過查看Integer的源碼會發現,它有個內部靜態類IntegerCache,這個內部靜態類進行了緩存,範圍是[-128,127],只要是這個範圍內的數字都會緩存到裏面,從而做成常量池進行管理。我們來看一個實例:

package com.itszt.test5;
/**
 * Integer常量池
 */
public class IntegerTest {
    public static void main(String[] args) {
        Integer i1=10;
        Integer i2=10;//在[-128-127之間,存入常量池]
        System.out.println("i1 ==i2 ---> " + (i1==i2));

        Integer i3=1000;
        Integer i4=1000;//超出常量池範圍,各自創建新的對象
        System.out.println("i3 ==i4 ---> " + (i3==i4));
    }
}

   控制檯打印結果:

i1 ==i2 ---> true
i3 ==i4 ---> false

   上述代碼中,第一次把i1的值緩存進去了,當創建i2的時候,它其實是指向了第一次緩存進去的那個10,所以i1和i2指向了同一個地址;由於i3和i4均超出了常量池範圍,故在堆內存中重新創建了兩個對象,它們在堆內存中的地址不相等。

  如果使用new關鍵字,意味着在堆內存中開闢了新的一塊內存孔家。每次new一個對象都是在堆內存中開闢一塊新的空間,所以每一個new出的對象的地址都不一樣。

  Float和Double沒有實現常量池。代碼演示如下:

        Float f1=10.0f;
        Float f2=10.0f;
        System.out.println("f1 =f2 ---> " + (f1==f2));

        Double d1=12.0;//默認爲double類型
        Double d2=12.0d;
        System.out.println("d1 =d2 ---> " + (d1==d2));    

   上述代碼在main()主函數中執行後,控制檯打印如下:

f1 = f2 ---> false
d1 =d2 ---> false

   三.String類

  1.new String都是在堆上創建字符串對象。

   

  2.當調用 intern() 方法時,編譯器會將字符串添加到常量池中(stringTable維護),並返回指向該常量的引用。

  

  3.通過字面量賦值創建字符串(如:String str=”twm”)時,會先在常量池中查找是否存在相同的字符串,若存在,則將棧中的引用直接指向該字符串;若不存在,則在常量池中生成一個字符串,再將棧中的引用指向該字符串。

  4.常量字符串的“+”操作,編譯階段直接會合成爲一個字符串。如string str=”JA”+”VA”,在編譯階段會直接合併成語句String str=”JAVA”,於是會去常量池中查找是否存在”JAVA”,從而進行創建或引用。

  5.對於final字段,編譯期直接進行了常量替換(而對於非final字段則是在運行期進行賦值處理的)。
  final String str1=”ja”;
  final String str2=”va”;
  String str3=str1+str2;
  在編譯時,直接替換成了String str3=”ja”+”va”,然後再次替換成String str3=”JAVA”。

  6.常量字符串和變量拼接時(如:String str3=baseStr + “01”;)會調用stringBuilder.append()在堆內存上創建新的對象。

  7.在JDK 1.7後,intern()方法還是會先去查詢常量池中是否有已經存在,如果存在,則返回常量池中的引用,這一點與之前沒有區別,區別在於,如果在常量池中找不到對應的字符串,則不會再將字符串拷貝到常量池,而只是在常量池中生成一個對原字符串的引用。簡單的說,就是往常量池放的東西變了:原來在常量池中找不到時,複製一個副本放到常量池,1.7後則是將在堆上的地址引用複製到常量池。

  

  四.常見試題解答:

Q:下列程序的輸出結果:
String s1= “abc”;
String s2= “abc”;
System.out.println(s1==s2);
A:true,均指向常量池中對象。

Q:下列程序的輸出結果:
String s1=new String(“abc”);
String s2=new String(“abc”);
System.out.println(s1==s2);
A:false,兩個引用指向堆中的不同對象。

Q:下列程序的輸出結果:
String s1 = “abc”;
String s2 = “a”;
String s3 = “bc”;
String s4 = s2 + s3;
System.out.println(s1 == s4);
A:false,因爲s2+s3實際上是使用StringBuilder.append來完成,
        會生成不同的對象。

Q:下列程序的輸出結果:
String s1 = “abc”;
final String s2 = “a”;
final String s3 = “bc”;
String s4 = s2 + s3;
System.out.println(s1 == s4);
A:true,因爲final變量在編譯後會直接替換成對應的值,
    所以實際上等於s4=”a”+”bc”,而這種情況下,
    編譯器會直接合併爲s4=”abc”,所以最終s1==s4。

Q:下列程序的輸出結果:
String s = new String(“abc”);
String s1 = “abc”;
String s2 = new String(“abc”);
System.out.println(s == s1.intern());
System.out.println(s == s2.intern());
System.out.println(s1 == s2.intern());
A:false,false,true。 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章