Java-String.intern的深入研究

今天看到String的intern()方法,有點懵,搜索了下看到這麼一篇博文,很精彩就轉發希望幫助更多的人。

原文地址:https://www.cnblogs.com/Kidezyq/p/8040338.html

後面又發現評論區有很多疑問,原博主沒有解決,又再搜索了一篇博文,將自己的疑惑得到了解決。如有問題請留言,共同探討。

 

When---什麼時候需要了解String的intern方法:

面試的時候(蜜汁尷尬)!雖然不想承認,不過面試的時候經常碰到這種高逼格的問題來考察我們是否真正理解了String的不可變性、String常量池的設計以及String.intern方法所做的事情。但其實,我們在實際的編程中也可能碰到可以利用String.intern方法來提高程序效率或者減少內存佔用的情況,這個我們等下會細說。

 

What---String.intern方法究竟做了什麼:

Returns a canonical representation for the string object. A pool of strings, initially empty, is maintained privately by the class String. 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. It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true. All literal strings and string-valued constant expressions are interned. String literals are defined in section 3.10.5 of the The Java? Language Specification.

上面是jdk源碼中對intern方法的詳細解釋。簡單來說就是intern用來返回常量池中的某字符串,如果常量池中已經存在該字符串,則直接返回常量池中該對象的引用。否則,在常量池中加入該對象,然後 返回引用。下面的一個例子詳細的解釋了intern的作用過程:

Now lets understand how Java handles these strings. When you create two string literals:

String name1 = "Ram"; 

String name2 = "Ram";

In this case, JVM searches String constant pool for value "Ram", and if it does not find it there then it allocates a new memory space and store value "Ram" and return its reference to name1. Similarly, for name2 it checks String constant pool for value "Ram" but this time it find "Ram" there so it does nothing simply return the reference to name2 variable. The way how java handles only one copy of distinct string is called String interning.

 

How---String.intern方法在jdk1.7之前和之後的區別:

簡單的說其實就一個:在jdk1.7之前,字符串常量存儲在方法區的PermGen Space。在jdk1.7之後,字符串常量重新被移到了堆中。

 

Back---重回String設計的初衷:

Java中的String被設計成不可變的,出於以下幾點考慮:

1. 字符串常量池的需要。字符串常量池的誕生是爲了提升效率和減少內存分配。可以說我們編程有百分之八十的時間在處理字符串,而處理的字符串中有很大概率會出現重複的情況。正因爲String的不可變性,常量池很容易被管理和優化。

2. 安全性考慮。正因爲使用字符串的場景如此之多,所以設計成不可變可以有效的防止字符串被有意或者無意的篡改。從java源碼中String的設計中我們不難發現,該類被final修飾,同時所有的屬性都被final修飾,在源碼中也未暴露任何成員變量的修改方法。(當然如果我們想,通過反射或者Unsafe直接操作內存的手段也可以實現對所謂不可變String的修改)。

3. 作爲HashMap、HashTable等hash型數據key的必要。因爲不可變的設計,jvm底層很容易在緩存String對象的時候緩存其hashcode,這樣在執行效率上會大大提升。

Deeper---直接來看例子:

首先來試試下面程序的運行結果是否與預想的一致:

String s1 = new String("aaa");
String s2 = "aaa";
System.out.println(s1 == s2);    // false

s1 = new String("bbb").intern();
s2 = "bbb";
System.out.println(s1 == s2);    // true

s1 = "ccc";
s2 = "ccc";
System.out.println(s1 == s2);    // true

s1 = new String("ddd").intern();
s2 = new String("ddd").intern();
System.out.println(s1 == s2);    // true

s1 = "ab" + "cd";
s2 = "abcd";    
System.out.println(s1 == s2);    // true

String temp = "hh";
s1 = "a" + temp;
// 如果調用s1.intern 則最終返回true
s2 = "ahh";
System.out.println(s1 == s2);    // false

temp = "hh".intern();
s1 = "a" + temp;
s2 = "ahh";
System.out.println(s1 == s2);    // false

temp = "hh".intern();
s1 = ("a" + temp).intern();
s2 = "ahh";
System.out.println(s1 == s2);    // true

s1 = new String("1");    // 同時會生成堆中的對象 以及常量池中1的對象,但是此時s1是指向堆中的對象的
s1.intern();            // 常量池中的已經存在
s2 = "1";
System.out.println(s1 == s2);    // false

String s3 = new String("1") + new String("1");    // 此時生成了四個對象 常量池中的"1" + 2個堆中的"1" + s3指向的堆中的對象(注此時常量池不會生成"11")
s3.intern();    // jdk1.7之後,常量池不僅僅可以存儲對象,還可以存儲對象的引用,會直接將s3的地址存儲在常量池
String s4 = "11";    // jdk1.7之後,常量池中的地址其實就是s3的地址
System.out.println(s3 == s4); // jdk1.7之前false, jdk1.7之後true

s3 = new String("2") + new String("2");
s4 = "22";        // 常量池中不存在22,所以會新開闢一個存儲22對象的常量池地址
s3.intern();    // 常量池22的地址和s3的地址不同
System.out.println(s3 == s4); // false

// 對於什麼時候會在常量池存儲字符串對象,我想我們可以基本得出結論: 1. 顯示調用String的intern方法的時候; 2. 直接聲明字符串字面常量的時候,例如: String a = "aaa";
// 3. 字符串直接常量相加的時候,例如: String c = "aa" + "bb";  其中的aa/bb只要有任何一個不是字符串字面常量形式,都不會在常量池生成"aabb". 且此時jvm做了優化,不//   會同時生成"aa"和"bb"在字符串常量池中

如果有出入的話,再來看看具體的字節碼分析:

/**
 * 字節碼爲:
 *   0:   ldc     #16; //String 11   --- 從常量池加載字符串常量11
     2:   astore_1                   --- 將11的引用存到本地變量1,其實就是將s指向常量池中11的位置
 */
String s = "11";    

/**
 * 0:   new     #16; //class java/lang/String    --- 新開闢了一個地址,存儲new出來的對象
   3:   dup                                      --- 將new出來的對象複製了一份到棧頂(也就是s1最終指向的是堆中的另一個存儲字符串11的地址)
   4:   ldc     #18; //String 11          
   6:   invokespecial   #20; //Method java/lang/String."<init>":(Ljava/lang/String;)V
   9:   astore_1
 */
String s1 = new String("11");

/**
 * 0:   new     #16; //class java/lang/StringBuilder                       --- 可以看到jdk對字符串拼接做了優化,先是建了一個StringBuilder對象
   3:   dup
   4:   new     #18; //class java/lang/String                              --- 創建String對象
   7:   dup
   8:   ldc     #20; //String 1                                            --- 從常量池加載了1(此時常量池和堆中都會存字符串對象)
   10:  invokespecial   #22; //Method java/lang/String."<init>":(Ljava/lang/String;)V                    --- 初始化String("1")對象
   13:  invokestatic    #25; //Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
   16:  invokespecial   #29; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V             --- 初始化StringBuilder對象
   19:  new     #18; //class java/lang/String
   22:  dup
   23:  ldc     #20; //String 1
   25:  invokespecial   #22; //Method java/lang/String."<init>":(Ljava/lang/String;)V
   28:  invokevirtual   #30; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   31:  invokevirtual   #34; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   34:  astore_1                                                                                          ---從上可以看到實際上常量池目前只存了1
  36:  invokevirtual   #38; //Method java/lang/String.intern:()Ljava/lang/String;  --- 調用String.intern中,jdk1.7以後,常量池也是堆中的一部分且常量池可以存引用,這裏直接存的是s2的引用
  39:  pop                                                                                                --- 這裏直接返回的是棧頂的元素
 */
String s2 = new String("1") + new String("1");
s2.intern();

/**
 * 0:   ldc     #16; //String abc        --- 可以看到此時常量池直接存儲的是:abc, 而不會a、b、c各存一份
   2:   astore_1
 */
String s3 = "a" + "b" + "c";

/**    
0:   new     #16; //class java/lang/StringBuilder
3:   dup
4:   ldc     #18; //String why                --- 常量池的why
6:   invokespecial   #20; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
9:   ldc     #23; //String true                --- 常量池的true
11:  invokevirtual   #25; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14:  invokevirtual   #29; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
17:  astore_1
*/
String s1 = new StringBuilder("why").append("true").toString();
System.out.println(s1 == s1.intern());                            // jdk1.7之前爲false,之後爲true

下面我們延伸一下來講講字符串拼接的優化問題:

String a = "1"; 
for (int i=0; i<10; i++) { 
  a += i; 
}
0:   ldc     #16; //String 1
2:   astore_1
3:   iconst_0
4:   istore_2                               --- 循環開始
5:   goto    30           
8:   new     #18; //class java/lang/StringBuilder        --- 每個循環都建了一個StringBuilder對象,對性能有損耗
11:  dup
12:  aload_1
13:  invokestatic    #20; //Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
16:  invokespecial   #26; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
19:  iload_2
20:  invokevirtual   #29; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
23:  invokevirtual   #33; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
26:  astore_1
27:  iinc    2, 1        ---- 計數加1
30:  iload_2
31:  bipush  10
33:  if_icmplt       8

String a = "1";
for (int i=0; i<10; i++) {
    a += "1";
}
的字節碼爲:
0:   ldc     #16; //String 1
2:   astore_1
3:   iconst_0
4:   istore_2
5:   goto    31
8:   new     #18; //class java/lang/StringBuilder   ---還是會每次建立一個StringBuilder對象
11:  dup
12:  aload_1
13:  invokestatic    #20; //Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
16:  invokespecial   #26; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
19:  ldc     #16; //String 1                        ---和上一個循環的區別也僅僅在於這裏是從常量池加載1,
21:  invokevirtual   #29; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24:  invokevirtual   #33; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27:  astore_1
28:  iinc    2, 1
31:  iload_2
32:  bipush  10
34:  if_icmplt       8

可知,真正的性能瓶頸在於每次循環都建了一個StringBuilder對象
所以我們優化一下 :
StringBuilder sb = new StringBuilder("1");
for (int i=0; i<10; i++) {
    sb.append("1");
}
對應的字節碼爲:
0:   new     #16; //class java/lang/StringBuilder        -- 在循環直接初始化了StringBuilder對象
3:   dup
4:   ldc     #18; //String 1
6:   invokespecial   #20; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V        
9:   astore_1
10:  iconst_0
11:  istore_2
12:  goto    25
15:  aload_1
16:  ldc     #18; //String 1
18:  invokevirtual   #23; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21:  pop
22:  iinc    2, 1
25:  iload_2
26:  bipush  10
28:  if_icmplt       15

Where---String.intern的使用:

我們直接看一個例子來結束String.intern之旅吧:

 Integer[] DB_DATA = new Integer[10];
        Random random = new Random(10 * 10000);
        for (int i = 0; i < DB_DATA.length; i++) {
            DB_DATA[i] = random.nextInt();
        }
        long t = System.currentTimeMillis();
        String[]arr=new String[MAX];
        for (int i = 0; i < MAX; i++) {
            arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length]));                // --- 每次都要new一個對象
            // arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();    --- 其實雖然這麼多字符串,但是類型最多爲10個,大部分重複的字符串,大大減少內存
        }
        System.out.println((System.currentTimeMillis() - t) + "ms");
        System.gc();

看了評論區有很多疑問,這時就在搜索了一篇博文專解此疑問。原文地址:http://developer.51cto.com/art/201106/266454.htm

stringjava中的字符串。String類是不可變的,對String類的任何改變,都是返回一個新的String類對象。下面介紹java中的String與常量池。

1. 首先String不屬於8種基本數據類型,String是一個對象。

因爲對象的默認值是null,所以String的默認值也是null;但它又是一種特殊的對象,有其它對象沒有的一些特性。

2. new String()和new String(“”)都是申明一個新的空字符串,是空串不是null;

3. String str=”kvill”;String str=new String (“kvill”);的區別:

在這裏,我們不談堆,也不談棧,只先簡單引入常量池這個簡單的概念。

常量池(constant pool)指的是在編譯期被確定,並被保存在已編譯的.class文件中的一些數據。它包括了關於類、方法、接口等中的常量,也包括字符串常量。

看例1:

String s0=”kvill”;  
String s1=”kvill”;  
String s2=”kv” + “ill”;  
System.out.println( s0==s1 );  
System.out.println( s0==s2 );  

結果爲:

true   

true  

首先,我們要知道Java會確保一個字符串常量只有一個拷貝。

因爲例子中的s0和s1中的”kvill”都是字符串常量,它們在編譯期就被確定了,所以s0==s1爲true;而”kv”和”ill”也都是字符串常量,當一個字符串由多個字符串常量連接而成時,它自己肯定也是字符串常量,所以s2也同樣在編譯期就被解析爲一個字符串常量,所以s2也是常量池中”kvill”的一個引用。

所以我們得出s0==s1==s2;

用new String() 創建的字符串不是常量,不能在編譯期就確定,所以new String() 創建的字符串不放入常量池中,它們有自己的地址空間。

看例2:

String s0=”kvill”;  

String s1=new String(”kvill”);  

String s2=”kv” + new String(“ill”);  

System.out.println( s0==s1 );  

System.out.println( s0==s2 );  

System.out.println( s1==s2 );  

結果爲:

false  

false  

false  

例2中s0還是常量池中”kvill”的應用,s1因爲無法在編譯期確定,所以是運行時創建的新對象”kvill”的引用,s2因爲有後半部分new String(“ill”)所以也無法在編譯期確定,所以也是一個新創建對象”kvill”的應用;明白了這些也就知道爲何得出此結果了。

4. String.intern():

再補充介紹一點:存在於.class文件中的常量池,在運行期被JVM裝載,並且可以擴充。String的intern()方法就是擴充常量池的一個方法;當一個String實例str調用intern()方法時,Java查找常量池中是否有相同Unicode的字符串常量,如果有,則返回其的引用,如果沒有,則在常量池中增加一個Unicode等於str的字符串並返回它的引用;看例3就清楚了

例3:

String s0= “kvill”;  

String s1=new String(”kvill”);  

String s2=new String(“kvill”);  

System.out.println( s0==s1 );  

System.out.println( “**********” );  

s1.intern();  

s2=s2.intern(); //把常量池中“kvill”的引用賦給s2  

System.out.println( s0==s1);  

System.out.println( s0==s1.intern() );  

System.out.println( s0==s2 );  

結果爲:

false  

**********  

false //雖然執行了s1.intern(),但它的返回值沒有賦給s1  

true //說明s1.intern()返回的是常量池中”kvill”的引用  

true  

最後我再破除一個錯誤的理解:

有人說,“使用String.intern()方法則可以將一個String類的保存到一個全局String表中,如果具有相同值的Unicode字符串已經在這個表中,那麼該方法返回表中已有字符串的地址,如果在表中沒有相同值的字符串,則將自己的地址註冊到表中“如果我把他說的這個全局的String表理解爲常量池的話,他的最後一句話,“如果在表中沒有相同值的字符串,則將自己的地址註冊到表中”是錯的:

看例4:

String s1=new String("kvill");  
String s2=s1.intern();  
System.out.println( s1==s1.intern() );  
System.out.println( s1+" "+s2 );  
System.out.println( s2==s1.intern() );  

結果:

false 

kvill kvill  

true  

在這個類中我們沒有聲名一個”kvill”常量,所以常量池中一開始是沒有”kvill”的,當我們調用s1.intern()後就在常量池中新添加了一個”kvill”常量,原來的不在常量池中的”kvill”仍然存在,也就不是“將自己的地址註冊到常量池中”了。

s1==s1.intern()爲false說明原來的“kvill”仍然存在;

s2現在爲常量池中“kvill”的地址,所以有s2==s1.intern()爲true。

5. 關於equals()和==:

這個對於String簡單來說就是比較兩字符串的Unicode序列是否相當,如果相等返回true;而==是比較兩字符串的地址是否相同,也就是是否是同一個字符串的引用。

6. 關於String是不可變的

這一說又要說很多,大家只要知道String的實例一旦生成就不會再改變了,比如說:String str=”kv”+”ill”+” “+”ans”;
就是有4個字符串常量,首先”kv”和”ill”生成了”kvill”存在內存中,然後”kvill”又和” “ 生成 ”kvill “存在內存中,最後又和生成了”kvill ans”;並把這個字符串的地址賦給了str,就是因爲String的“不可變”產生了很多臨時變量,這也就是爲什麼建議用StringBuffer的原因了,因爲StringBuffer是可改變的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章