String 和它的 intern()

先做道題

Q1:定義了幾個對象?

public class Q1 {
    public static void main(String[] args) {
        String s = new String("HelloWorld"); // 2 個(堆中 String 對象,常量池中字符串 "HelloWorld" 對象)
    }
}

Q2:判斷下面代碼輸出

public class Q2 {
    public static void main(String[] args) {
        String s1 = "HelloWorld";
        String s2 = "Hello" + "World";
        String s3 = new String("HelloWorld");
        String s4 = s3.intern();
        
        System.out.println(s1 == s2); // true,都指向常量池中字符串 "HelloWorld" 對象
        System.out.println(s1 == s3); // false,符號引用 s3 指向堆中 String 對象
        System.out.println(s1 == s4); // true,都指向常量池中字符串 "HelloWorld" 對象
    }
}

一、String 字符串

String 表示字符串,不屬於基本類型,但它可以像基本類型一樣,直接通過字面量賦值

編譯期

在編譯期,符號引用和字面量會被加入到 Class 文件的常量池中,然後在類加載階段,會進入常量池(常量池中已存在的字符串字面量不會重複添加)

常量池

Java 在內存分配中有 3種常量池:Class 常量池(靜態常量池)、字符串常量池(存在於運行時常量池)、運行時常量池

  • 常量池屬於線程共享數據區
  • 常量池可分爲兩大類:靜態常量池(Class 常量池,存在於 Class 文件中)和運行時常量池
  • 運行時常量池是方法區的一部分,存放一些運行時常量數據

字符串常量池

字符串常量池存在運行時常量池之中(JDK 6 及以前,存在方法區中;從 JDK 7 開始,存在於堆中)

二、intern() 方法(JDK 1.8)

/**
 * ...
 * When the intern method is invoked, if the pool already contains a
 * string equal to this String object as determined by
 * the equals(Object) method, then the string from the pool is
 * returned. Otherwise, this String object is added to the
 * pool and a reference to this String object is returned.
 * 調用 intern 方法時,如果池中已包含由 equals 方法確定的與此 string 對象相等的字符串,則返回池中的字符串
 * 否則,此字符串對象將添加到池中,並返回對此字符串對象的引用
 * ...
 *
 * @return  a string that has the same contents as this string, but is
 *          guaranteed to be from a pool of unique strings.
 * 返回 一個字符串,其內容與此字符串相同,但保證來自唯一字符串池
 */
public native String intern();

上面 String#intern() 方法的註釋中,其中一段所要表達的意思很明確:“調用 intern 方法時,如果池中已包含由 equals 方法確定的與此 string 對象相等的字符串,則返回池中的字符串。否則,此字符串對象將添加到池中,並返回對此字符串對象的引用”,就是說當 String 實例調用 intern() 方法時,JVM 會在字符串常量池中查詢當前字符串是否存在,若存在,返回當前字符串;若不存在,則先將當前字符串放入常量池中,之後再返回

在 JDK 6 及以前版本中,字符串常量池裏放的都是字符串常量;從 JDK 7 開始,字符串常量池從方法區移到了堆中,字符串常量池中也可以存放放於堆內的字符串對象的引用

三、字符串分析(JDK 1.8)

先用雙引號創建字符串,再用一個 new 創建字符串

public class Test1 {
    public static void main(String[] args) {
        String str1 = "Test1";
        String str2 = new String("Test1");
        String str3 = str2.intern();
        
        System.out.println(str1 == str2); // false
        System.out.println(str1 == str3); // true
        System.out.println(str2 == str3); // false
    }
 }

String str1 = “Test1”;
       編譯期:將字面量 Test1 會被加入到 Class 文件的常量池中
       類加載階段:字面量 Test1 會進入字符串常量池(注意:進入的條件是該常量此時不在字符串常量池,即不會重複添加相同常量)
       str1 指向字符串常量池中字符串 “Test1” 對象
String str2 = new String(“Test1”);
       編譯期:在此之前字符串常量池中已存在 “Test1”,不會重複添加到字符串常量池
       運行期:在堆中創建一個 String 對象
       符號引用 str2 指向堆中創建的 String 對象
String str3 = str2.intern();
       字符串常量池中已存在 “Test1”,調用 intern() 方法,不會重複添加到字符串常量池,直接返回字符串常量池中 “Test1” 對象
       str3 指向字符串常量池中 “Test1” 對象
因此
       str1(字符串常量池中字符串 “Test1” 對象)== str2(堆中創建的 String 對象):false
       str1(字符串常量池中字符串 “Test1” 對象)== str3(字符串常量池中字符串 “Test1” 對象):true
       str2(堆中創建的 String 對象)== str3(字符串常量池中字符串 “Test1” 對象):false

小結:

  • 雙引號創建的字符串在字符串常量池中
  • 通過一個 new 方式創建字符串,不管該字符串是否已在字符串常量池中,intern() 調用前後兩個符號引用指向的不是同一個對象

JVM 啓動過程中加入常量池的字符串

上面的字符串 “Test1” 在字符串常量池中一開始並沒有,但有的字符串在 JVM 啓動過程中就已經存在,如:“Java”

public class Test2 {
    public static void main(String[] args) {
        String str1 = new String("ja") + new String("va");
        String str2 = str1.intern();
        String str3 = "java";
        
        System.out.println(str1 == str2); // false
        System.out.println(str1 == str3); // false
        System.out.println(str2 == str3); // true
    }
 }

String str1 = new String(“ja”) + new String(“va”);
       編譯期:將字面量 ja 和 va 會被加入到 Class 文件的常量池中
       類加載階段:字面量 ja 和 va 會進入字符串常量池
       運行期:在堆中創建三個 String 對象(兩個匿名 String 對象,一個符號引號 str1 指向的 String 對象)
       str1 指向堆中創建的 String 對象
String str2 = str1.intern();
       編譯期:在此之前字符串常量池中已存在 “java”,不會重複添加到字符串常量池(在 JVM 啓動的時候會調用了一些方法,在常量池中會生成 “java” 等字符串常量)
       str2 指向字符串常量池中的字符串 “java” 對象
String str3 = “java”;
       編譯期:在此之前字符串常量池中已存在 “Test1”,不會重複添加到字符串常量池
       str3 指向字符串常量池中的字符串 “java” 對象
因此
       str1(堆中創建的 String 對象)== str2(字符串常量池中的字符串 “java” 對象):false
       str1(堆中創建的 String 對象)== str3(字符串常量池中的字符串 “java” 對象):false
       str2(字符串常量池中的字符串 “java” 對象)== str3(字符串常量池中的字符串 “java” 對象):true

小結:

  • 特殊情況:在 JVM 啓動的時候會調用了一些方法,在常量池中會生成 “java” 等字符串常量(正常情況:下方 “### 先用兩個 new 創建字符串”)
  • JVM 啓動過程中加入常量池的字符串,調用 intern() 方法,指向字符串常量池中的字符串對象

先用一個 new 創建字符串

public class Test3 {
    public static void main(String[] args) {
        // 情況一
        String s1 = new String("Test3_1");
        String s2 = s1.intern(); // intern() 在雙引號之前調用
        String s3 = "Test3_1";
        System.out.println(s1 == s2); // false
        System.out.println(s1 == s3); // false
        System.out.println(s2 == s3); // true
        
        System.out.println();
        
        // 情況二
        String s4 = new String("Test3_2");
        String s5 = "Test3_2";
        String s6 = s4.intern(); // intern() 在雙引號之後調用
        System.out.println(s4 == s5); // false
        System.out.println(s4 == s6); // false
        System.out.println(s5 == s6); // true
    }
}

情況一
       符號引號 s1 指向堆中創建的 String 對象(字符串內容是 “Test3_1”)
       符號引用 s2 指向字符串常量池中的字符串 “Test3_1” 對象
       符號引用 s3 指向字符串常量池中的字符串 “Test3_1” 對象
情況二
       符號引號 s4 指向堆中創建的 String 對象(字符串內容是 “Test3_2”)
       符號引用 s5 指向字符串常量池中的字符串 “Test3_2” 對象
       符號引用 s6 指向字符串常量池中的字符串 “Test3_2” 對象
因此
       s1(堆中創建的 String 對象,字符串內容是 “Test3_1”)== s2(字符串常量池中的字符串 “Test3_1” 對象):fasle
       s1(堆中創建的 String 對象,字符串內容是 “Test3_1”)== s3(字符串常量池中的字符串 “Test3_1” 對象):fasle
       s2(字符串常量池中的字符串 “Test3_1” 對象)== s3(字符串常量池中的字符串 “Test3_1” 對象):true

       s4(堆中創建的 String 對象,字符串內容是 “Test3_2”)== s5(字符串常量池中的字符串 “Test3_2” 對象):fasle
       s4(堆中創建的 String 對象,字符串內容是 “Test3_2”)== s6(字符串常量池中的字符串 “Test3_2” 對象):fasle
       s5(字符串常量池中的字符串 “Test3_2” 對象)== s6(字符串常量池中的字符串 “Test3_2” 對象):true

小結:

  • 通過一個 new 方式創建字符串,不管該字符串是否已在字符串常量池中,intern() 前後兩個符號引用指向的不是同一個對象

先用兩個 new 創建字符串

public class Test4 {
    public static void main(String[] args) {
        // 情況一
        String s1 = new String("Test4_1") + new String("Test4_1");
        String s2 = s1.intern(); // intern() 在雙引號之前調用
        String s3 = "Test4_1Test4_1";
        System.out.println(s1 == s2); // true
        System.out.println(s1 == s3); // true
        System.out.println(s2 == s3); // true
        
        System.out.println();
        
        // 情況二
        String s4 = new String("Test4_2") + new String("Test4_2");
        String s5 = "Test4_2Test4_2";
        String s6 = s4.intern(); // intern() 在雙引號之後調用
        System.out.println(s4 == s5); // false
        System.out.println(s4 == s6); // false
        System.out.println(s5 == s6); // true
    }
}

情況一
       符號引號 s1 指向堆中創建的 String 對象(字符串內容是 “Test4_1Test4_1”)
       符號引用 s2 指向字符串常量池中的引用,該引用指向 s1 引用的對象
       符號引用 s3 指向字符串常量池中的引用,該引用指向 s1 引用的對象
情況二
       符號引號 s4 指向堆中創建的 String 對象(字符串內容是 “Test4_2Test4_2”)
       符號引用 s5 指向字符串常量池中的字符串 “Test4_2Test4_2” 對象
       符號引用 s6 指向字符串常量池中的字符串 “Test4_2Test4_2” 對象
因此
       s1(堆中創建的 String 對象,字符串內容是 “Test4_1Test4_1”)== s2(字符串常量池中的一個引用,該引用指向 s1 引用的對象):true
       s1(堆中創建的 String 對象,字符串內容是 “Test4_1Test4_1”)== s3(字符串常量池中的一個引用,該引用指向 s1 引用的對象):true
       s2(字符串常量池中的一個指向 s1 的引用)== s3(字符串常量池中的一個指向 s1 的引用):true

       s4(堆中創建的 String 對象,字符串內容是 “Test4_2Test4_2”)== s5(字符串常量池中的字符串 “Test4_2Test4_2” 對象):fasle
       s4(堆中創建的 String 對象,字符串內容是 “Test4_2Test4_2”)== s6(字符串常量池中的字符串 “Test4_2Test4_2” 對象):fasle
       s5(字符串常量池中的字符串 “Test4_2Test4_2” 對象)== s6(字符串常量池中的字符串 “Test4_2Test4_2” 對象):true

小結:

  • JDK 1.7 及以後,多個字符串變量拼接的字符串(如:通過兩個 new 創建字符串),intern() 方法調用前,常量池中不存在拼接後的字符串
  • JDK 1.7 及以後,多個字符串變量拼接的字符串(如:通過兩個 new 創建字符串),intern() 方法調用後,常量池中不需要再存儲一份對象了,可以直接存儲堆中的引用(String#intern 方法時,如果存在堆中的對象,會直接保存對象的引用,而不會重新創建對象)

四、總結

  • JDK 1.7 及以後,字符串常量池從方法區移動到了堆中
  • 雙引號創建的字符串在字符串常量池中
  • 通過一個 new 方式創建字符串,不管該字符串是否已在字符串常量池中,intern() 調用前後兩個符號引用指向的不是同一個對象(調用 intern() 前後分別是:堆中 String 對象、字符串常量池中對應字符串對象)
  • 特殊情況:在 JVM 啓動的時候會調用了一些方法,在常量池中會生成 “java” 等字符串常量
  • JVM 啓動過程中加入常量池的字符串,調用 intern() 方法,指向字符串常量池中的字符串對象
  • 通過一個 new 方式創建字符串,不管該字符串是否已在字符串常量池中,intern() 前後兩個符號引用指向的不是同一個對象
  • JDK 1.7 及以後,多個字符串變量拼接的字符串(如:通過兩個 new 創建字符串),intern() 方法調用前,常量池中不存在拼接後的字符串
  • JDK 1.7 及以後,多個字符串變量拼接的字符串(如:通過兩個 new 創建字符串),intern() 方法調用後,常量池中不需要再存儲一份對象了,可以直接存儲堆中的引用(調用 String#intern() 方法時,如果存在堆中的對象,會直接在字符串常量池中保存堆中對象的引用,而不會重新創建字符串對象)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章