字符串操作是最常見的操作犹褒。在Java中抵窒,往往使用String類來進行各種字符串操作。
而對于String這個類化漆,其實隱含不少特性估脆。對此,自己最近梳理了一遍座云。
字符串創(chuàng)建
常用方式主要兩種:
String a = "123"
和
String b = new String("123");
第一種方式疙赠,”123“直接存儲在常量池;第二種方式實際創(chuàng)建了兩個對象朦拖,第一個對象是”123“字符串在常量池中圃阳,第二個對象是在java堆中的String對象。
不可變
翻看jdk源碼璧帝,java.lang.String的定義是這樣的:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
...
...
以上代碼在jdk7捍岳,8都是一致的。
String類用final修飾睬隶,從java的final關(guān)鍵字語義上看锣夹,說明String不可繼承。
其次苏潜,String的底層其實用了一個final類型的char數(shù)組來存儲银萍,說明該字段一旦創(chuàng)建后就不能改變。
其中有個很容易引起迷惑的地方恤左,必須要弄清楚:String對象本質(zhì)是引用贴唇,我們所說的不可變是指引用指向的對象內(nèi)容不可變,并不是引用不可變飞袋。而String類中涉及修改的方法(substring戳气、replace、toLowerCase等)都是創(chuàng)建一個新的字符串巧鸭,并把這個它重新賦給引用瓶您。這就說明引用是重新指向了一個新的的字符串,但原來的字符串依舊存在內(nèi)存里。
雖然說String不可變览闰,但也不是絕對不可變芯肤,可以通過反射機制進行修改。然而大多情況不需要也沒必要用到反射压鉴,這里就不詳細討論了
具體可參考Java中的String為什么是不可變的? -- String源碼分析
里面闡述比較詳細了锻拘。
字符串‘+’操作
以前的文章中很多都說字符串‘+’操作會導(dǎo)致性能低效油吭,要用StringBuilder或StringBuffer。但其實現(xiàn)在的JVM已經(jīng)優(yōu)化得足夠強大署拟,
例如以下代碼
public class StringTest{
public static void main(String[] args) {
String a = "hello";
String b = "abc" + "def" + 47 + a;
}
通過 javap -c 反編譯出來的字節(jié)碼如下:
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String hello
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: ldc #5 // String abcdef47
12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: aload_1
16: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_2
23: return
}
我們可以看到婉宰,字符串的‘+’操作其實在實際執(zhí)行過程中,就是一個StringBuilder的append操作推穷。
再看這段代碼:
public class StringTest{
public static void main(String[] args) {
String a = "";
for (int i=0;i<10;i++) {
a = a + i;
}
}
}
對應(yīng)的字節(jié)碼:
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String
2: astore_1
3: iconst_0
4: istore_2
5: iload_2
6: bipush 10
8: if_icmpge 36
11: new #3 // class java/lang/StringBuilder
14: dup
15: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
18: aload_1
19: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: iload_2
23: invokevirtual #6 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
26: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
29: astore_1
30: iinc 2, 1
33: goto 5
36: return
}
對于循環(huán)內(nèi)的字符串拼接心包,雖然還是轉(zhuǎn)化為StringBuilder,但是情況有點不同馒铃,留意8:和 33:之間的代碼就是每一次循環(huán)所執(zhí)行的操作蟹腾,可以看到每一次的循環(huán)都創(chuàng)建了一個新的StringBuilder對象。
所以總結(jié)來說区宇,對于普通的一次性的‘+’操作娃殖,可以放心使用;但循環(huán)下的‘+’议谷,因為每一次都要new 一個StringBuilder而導(dǎo)致性能降低炉爆,因此還是先自己定義一個StringBuilder,然后每次循環(huán)通過append操作來完成
字符串常量池
JVM為了提高性能和減少內(nèi)存開銷卧晓,在實例化字符串常量的時候進行了一些優(yōu)化芬首。為了減少在JVM中創(chuàng)建的字符串的數(shù)量,字符串類維護了一個字符串池逼裆,每當創(chuàng)建字符串常量時郁稍,JVM會首先檢查字符串常量池。如果字符串已經(jīng)存在池中波附,就返回池中的實例引用艺晴。如果字符串不在池中,就會實例化一個字符串并放到池中
在JDK6中掸屡,字符串常量池在永久代分配內(nèi)存封寞;而JDK7開始,常量池已經(jīng)在Java堆上分配內(nèi)存仅财。
而字符串常量池本質(zhì)是個固定容量的HashMap狈究。 Java7和8可以通過 -XX:StringTableSize 設(shè)置其map size。
在Java6到Java7u40之前-XX:StringTableSize的默認大小是1009盏求;7u40之后擴大到60013抖锥。
-XX:+PrintStringTableStatistics 會在程序終止時打印字符串常量池的使用情況
String.intern
String.intern是把雙刃劍亿眠,用時需謹慎,切記切記0醴稀D上瘛!
String.intern()是一個Native方法拯勉,底層調(diào)用C++的 StringTable::intern 方法竟趾。
源碼注釋:當調(diào)用 intern 方法時,如果常量池中已經(jīng)存在該字符串宫峦,則返回池中的字符串岔帽;否則將此字符串添加到常量池中,并返回字符串的引用导绷。
在這里jdk6和jdk7表現(xiàn)有點區(qū)別:
- jdk6會直接生成一個新的字符串對象到常量池中犀勒,并返回該對象引用
- jdk7因為常量池不在Perm區(qū),不需要重新生成對象妥曲,而是直接存儲堆中的引用
關(guān)于stirng.intern的更深入分析可看
深入解析String#intern
以及
詳細可看白衣大神的String.intern() 祛魅
substring()
jdk6與jdk7中的實現(xiàn)方式不一樣贾费。
jdk6調(diào)用substring()雖然會創(chuàng)建一個新的字符串對象,但里面的char[] 仍然指向原來的那個,
因此對一個很長很長的字符串進行截取后,可能導(dǎo)致內(nèi)存泄露枕赵。
jdk7中substring()方法在堆中真正的創(chuàng)建了一個新的數(shù)組,原字符數(shù)組沒有被引用后就被GC回收了.因此避免了上述問題.
如有紕漏,敬請指出~
參考:
String.intern in Java 6, 7 and 8 – string pooling
JDK6和JDK7中的substring()方法