1. 程式人生 > >Java中的String與intern方法

Java中的String與intern方法

常量池

在理解Java中的String之前有一個必須要知道的概念-常量池
在java的class檔案中,有一塊常量集中存放的區域,這塊地方被稱為常量池。常量池中儲存的常量通常包括關於類,方法,介面等中的常量,以及字串常量,如String s = “java”這種申明方式;當然也可擴充,執行器產生的常量也會放入常量池。而且在JDK1.7對常量池所處的位置也做了變動。在1.7以前,常量池位於JVM執行時記憶體的方法區(永久代或叫PermGen)。而到了1.7,常量池逐漸遷移到了MetSpace(元空間)中,此空間不存在於JVM中,其使用本地記憶體。

常量池和堆中的字串

儲存在常量池中的字串通常包括以下形式
1. String a=”a”這種字串字面值類的宣告,這種宣告會先去常量池中尋找是否有字串”a”(或指向字串的引用),如果有,則直接返回此引用。如果木有,則將字串放入常量池中,並返回其引用;
2. String ab=”a”+”b”這種字串字面值類的拼接,編譯完成後,會直接返回一個”ab”字串,處理過程和a類似。
3. String ab=new String(“ab”)這種形式宣告的字串是直接在堆中生成一個字串物件。
4. String ab=a+”b”這種形式的字串拼接過程要分兩種情況討論。假如a變數是個普通變數,這種情況下,通過生成一個StringBuilder,並呼叫append方法。這種方式生成字串和使用new方式生成的字串並沒有區別。都是直接在堆上生成字串。而假如a是final型別的,在編譯時,編譯期會自動吧a變數替換成”a”字串,從而該表示式類似於第二種情況,直接在常量池中生成字串。
以一段程式碼結果來驗證以上陳述

public class StringTest {
    public static void main(String[] args) {
        String b="b";
        final String finalb="b";
        String ab="ab";
        String abNewString=new String("ab");
        String abString="a"+"b";
        String abVar="a"+b;
        String abFinalVar="a"+finalb;
        System.out
.println(abNewString==ab); System.out.println(abString==ab); System.out.println(abVar==ab); System.out.println(abFinalVar==ab); } }

上面程式的執行結果如下:

false
true
false
true

下面再來看下其反編譯後的結果:

public class com.person.blogcases.StringTest {

  public com.person.blogcases.StringTest();
    Code:
       0
: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: ldc #2 // String b 2: astore_1 3: ldc #2 // String b 5: astore_2 6: ldc #3 // String ab 8: astore_3 9: new #4 // class java/lang/String 12: dup 13: ldc #3 // String ab 15: invokespecial #5 // Method java/lang/String."<init>":(Ljava/lang/String;)V 18: astore 4 20: ldc #3 // String ab 22: astore 5 24: new #6 // class java/lang/StringBuilder 27: dup 28: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V 31: ldc #8 // String a 33: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 36: aload_1 37: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 40: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 43: astore 6 45: ldc #3 // String ab 47: astore 7 49: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream; 52: aload 4 54: aload_3 55: if_acmpne 62 58: iconst_1 59: goto 63 62: iconst_0 63: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V 66: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream; 69: aload 5 71: aload_3 72: if_acmpne 79 75: iconst_1 76: goto 80 79: iconst_0 80: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V 83: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream; 86: aload 6 88: aload_3 89: if_acmpne 96 92: iconst_1 93: goto 97 96: iconst_0 97: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V 100: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream; 103: aload 7 105: aload_3 106: if_acmpne 113 109: iconst_1 110: goto 114 113: iconst_0 114: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V 117: return }

在0、3、6行分別從常量池中將字串引用( “b”,”b”,”ab” )壓入棧,而2、5、8行則分別將入棧的String引用賦於本地變數1、2、3(b,finalb,ab)中,從第9行到第18行是整個String abNewString=new String("ab"); 的反編譯結果。而20、22兩行則是String abString="a"+"b"; 反編譯的結果,可以看到,它也是直接從常量池中獲取字串引用的。從24到43行是String abVar="a"+b; 反編譯的結果,這是Java的一個語法糖,類似這種字串變數相加的情況,Java會生成一個StringBuilder並呼叫其append進行拼接,最後呼叫toString來返回結果。而45、47兩行則是String abFinalVar="a"+finalb; 反編譯的結果,可以看到其也是從常量池中直接獲取到的字串常量。這樣的話上面程式的執行結果就比較好理解了,因為ab、abfinalVar、abString三個變數都是從常量池中獲取的字串變數的引用,因此在使用==判斷的時候會是true。而對於abNewString、abVar,它們都是在堆上創建出來的物件,因此引用是不會和ab相等的,並且abNewString和abVar也不會是同一個引用。
理解了這些,其實intern就很好理解了,對一個變數呼叫intern方法的過程就相當用字串字面值宣告一個字串變數的過程。
例如

String a=從檔案或者哪裡讀來的字串;
String b=a.intern()

a.intern方法會先拿a字串去常量池中尋找,如果找到了和其相同的字串,則直接返回其引用。如果木有找到則會將a字串的話要分兩種情況。在JDK1.7以前,JVM會複製一份a字串到常量池中,然後返回常量池中字串的引用。而1.7以後,JVM不會將字串往常量池中複製一份了,而是直接在常量池中儲存一個指向a變數的引用,然後返回常量池中的這個引用。由這個特點可以看出來,對於程式中有大量重複字串的場景,使用intern方法可以一定程度上節省記憶體消耗。

使用場景

由於常量池所在方法區,因此一般容量相較於堆空間較小。其適用場景也就比較固定。一般適用於儲存大量字串的集合類。例如要儲存一億個字串,其中不同的字串就3類,這時使用intern方法就可以顯著的減少對堆空間浪費,因為所有相同字串的引用都指向同一個字串物件。不適合大量非重複字串的場景,例如這一億個字串各不相同。這時如果都使用intern,在JDK1.7以前會出問題,因為對於呼叫intern後,jvm會複製字串至常量池,常量池大小不夠的話,就OOM了。JDK1.7以後,不再複製字串例項置常量池後,這種場景先的適用性有所緩和。
舉個面試的時候經常會被問到的問題作為例子,有一個日誌檔案,裡面記錄了各個Ip的訪問資訊,找出前十個訪問記錄最多的Ip,假設我們要先把這些Ip訪問資訊讀到記憶體裡(當然肯定有更好的方法)。這個時候使用intern方法就很合適,因為來訪問的Ip必定有大量重複的。