String
類(lèi)可以認(rèn)為是 Java 語(yǔ)言中最為常用的類(lèi)了,對(duì)于 String
的理解更是 Java 面試題的扯疲客淳衙。
但作為一個(gè) Java 程序員,對(duì)于 String
是否足夠了解了呢饺著?
本篇文章將對(duì) String
的存儲(chǔ)箫攀,使用做一個(gè)詳細(xì)的探討。
先來(lái)簡(jiǎn)單介紹下 String
幼衰,String
是 JDK 提供的位于 java.lang
中的基礎(chǔ)類(lèi)靴跛,但區(qū)別于 byte,short渡嚣,int梢睛,long,char严拒,boolean扬绪,float竖独,double
這些基本類(lèi)型裤唠,String
不是基本數(shù)據(jù)類(lèi)型,而是一個(gè)類(lèi)莹痢。
因?yàn)槭穷?lèi)种蘸,實(shí)例化的String
對(duì)象的空值為 null
,但String
是如此常用竞膳,于是 JDK 對(duì)其有特殊的優(yōu)化航瞭。
String 的存在形式
上文提到,String
是 JDK 提供的類(lèi)坦辟,要學(xué)習(xí) JDK刊侯,最好的方法就是閱讀其源碼。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
...
分析源碼可以知道锉走,Java 中String
是以 char 數(shù)組
的形式存在的滨彻。
討論下 Java 中的 char 類(lèi)型
char
基本數(shù)據(jù)類(lèi)型是 Java 中用于存儲(chǔ)字符的藕届。String
就是以 char數(shù)組
形式存儲(chǔ)的,要理解 String
就必須先了解 char
亭饵。
但在討論 char
之前休偶,還需要介紹另外兩點(diǎn)知識(shí)。
編碼 unicode vs UTF
unicode辜羊,稱(chēng)為統(tǒng)一字符編碼踏兜,是國(guó)際上對(duì)千奇百怪字符的統(tǒng)一的編號(hào)。unicode 最初的 256 個(gè)字符八秃,是繼承于 ASCII 編碼碱妆。例如英文字母 a
,在 unicode 中編號(hào)為 97昔驱,中文 我
字山橄,在 unicode 中的編號(hào)就是 25105。簡(jiǎn)單的說(shuō)舍悯,unicode 就是統(tǒng)一的字符編號(hào)航棱。但是,這么多字符如何在代碼中表示呢萌衬?這便是 UTF饮醇。
UTF,unicode 轉(zhuǎn)換格式(Unicode Transformation Format)秕豫,UTF 有多種編碼方式朴艰,比較常用的就是 UTF-8
和 UTF-16
,這兩者各有優(yōu)劣混移,UTF-8
信息密度更高祠墅,傳輸、存儲(chǔ)效率更高歌径,UTF-16
字符對(duì)齊毁嗦,易于程序處理,利于優(yōu)化計(jì)算效率回铛。使用應(yīng)視情形而定狗准。
-
UTF-8
是通過(guò)變長(zhǎng)來(lái)表示 unicode 字符的,以 byte 為單位茵肃,長(zhǎng)度范圍 1~6腔长。例如a
編號(hào)是 97,就用一個(gè) byte验残,也就是 8 bit 來(lái)編碼捞附,而我
的編號(hào)是 25105,一個(gè) byte 無(wú)法編碼,于是就用 2個(gè) byte 來(lái)編碼鸟召。 -
UTF-16
則是固定長(zhǎng)度編碼想鹰。統(tǒng)一用 2 byte,也就是 16bit 進(jìn)行編碼药版,但 unicode 當(dāng)前字符集已經(jīng)超出 16bit 所能編碼范圍了(16bit辑舷,可對(duì) 2^16 = 65536 個(gè)字符進(jìn)行編碼),因此也會(huì)用 4 byte 來(lái)表示槽片。
內(nèi)碼與外碼
內(nèi)碼 internal encoding何缓,外碼 external encoding
- 內(nèi)碼是語(yǔ)言運(yùn)行時(shí),
char
在內(nèi)存中的編碼方式还栓。 - 外碼是除了內(nèi)碼以外的編碼碌廓,例如源碼編譯生成的目標(biāo)文件(可執(zhí)行文件、.class 文件)中的編碼均為外碼剩盒。
那么 Java 中的 char 呢谷婆?
char
是 Java 的基本類(lèi)型之一,用來(lái)表示字符辽聊。
JVM 采用的內(nèi)碼纪挎,是 UTF-16
,也就是說(shuō) Java 中的 char 的長(zhǎng)度為 2 byte跟匆,即 16 bit异袄。
但上文提到,僅 16 bit 已無(wú)法表示所有的 unicode玛臂,因此為了向下兼容烤蜕,Java 的 char 保留為 16bit,若有無(wú)法用 16 bit 表示的字符迹冤,則采用 2 char讽营,即 4 byte,32 bit 來(lái)表示泡徙。
Java 的 class 文件采用 UTF-8 存儲(chǔ)字符橱鹏。
char
在 class 中以UTF-8
方式編碼,區(qū)別于內(nèi)碼中的char
Character
锋勺,關(guān)于 char
的更多
Java 采用 UTF-16
為字符編碼蚀瘸。但 unicode 字符集已經(jīng)超出 16bit 所能表述的范圍狡蝶,因此有些字符會(huì)采用 2char庶橱,即 32 bit 進(jìn)行編碼。
為了方便處理贪惹,Java 提供了 Character
類(lèi)苏章。Character
對(duì) char
進(jìn)行了封裝,并提供了一些方法,主要是char
類(lèi)型的判斷(是數(shù)字還是中文)枫绅、大小寫(xiě)裝換泉孩、比較等等。具體方法并淋,可以參考 JDK 源碼java.lang.Character
寓搬。
提到 Character
,主要是強(qiáng)調(diào)以下幾點(diǎn):
code point
vscode unit
碼位code point
:指字符在 unicode 字符集中的編號(hào)县耽,用int
表示句喷,int 為 32bit,現(xiàn)階段可表示 unicode 字符集兔毙。范圍為U+0000 ~ U+10FFFF
唾琼。
code unit
:對(duì)應(yīng)一個(gè)char
,可由 1個(gè)或 2個(gè)code unit
組成code point
澎剥。這兩個(gè)概念主要涉及UTF-16
實(shí)現(xiàn)锡溯。基本多語(yǔ)言平面
Basic Multilingual Plane (BMP)
vs 輔助平面Supplementary Character
這兩個(gè)概念,是針對(duì) unicode 字符集而言哑姚。當(dāng)前 Java 支持的 unicode 字符集范圍為U+0000 ~ U+10FFFF
祭饭,若超出此范圍,則無(wú)法處理叙量。
Basic Multilingual Plane (BMP)
:用于表示U+0000 ~ U+FFFF
范圍的字符甜癞。
Supplementary Character
:unicode 超出U+FFFF
范圍后,需要用 2個(gè) char 表示宛乃,超出部分稱(chēng)為Supplementary Character
悠咱,由于code point
范圍最大為U+10FFFF
,所以Supplementary Character
最多為 5bit征炼,高位的 11bit 必須均為 0析既,否則表示字符超出 Java 當(dāng)前字符集范圍。處理單個(gè)char
時(shí)谆奥,不需要使用Supplementary Character
眼坏,當(dāng)以int
表示字符時(shí),才需要使用酸些。
具體可參考維基百科 UTF-16 介紹宰译。
String 是 char[]
以上分析源碼,知道了 String
是以 final char[]
的形式存儲(chǔ)的魄懂,并且知道了由于 Java 采用 UTF-16
編碼 unicode沿侈,因此有些字符由 2 char 表示。
int len1 = "1".length(); // = 1
int len2 = "我".length(); // = 1
int len3 = "??".length(); // = 2
// 用以下方法獲得真正的 unicode 字符個(gè)數(shù)
String emoji = "??";
int len3 = emoji.codePointCount(0, emoji.length());
String
類(lèi)中還提供了一些常用的字符處理方法市栗,將在下面的實(shí)踐章節(jié)進(jìn)行介紹缀拭,讓我們下來(lái)看看 String
是如何在 JVM 中存儲(chǔ)的咳短。
Java 中 String 的存儲(chǔ)
-
String
底層是final char[]
,是常量蛛淋。在 JVM 中咙好,位于字符串常量池。所謂常量褐荷,就是一旦創(chuàng)建勾效,就不無(wú)更改。 - 只要
String
的值發(fā)生變更叛甫,Java 的處理方式是新建一個(gè)String
對(duì)象葵第。 - 由于
String
是類(lèi),其實(shí)例為對(duì)象合溺。Java 在處理對(duì)象傳遞是卒密,均是引用拷貝。 - 對(duì)
String
的只讀棠赛,任何引用均不會(huì)修改其值哮奇。
JDK1.7 中 JVM 把
String
常量池從方法區(qū)中移除了;JDK1.8 中 JVM 把String
常量池移入了堆中睛约,同時(shí)取消了“永久代”鼎俘,改用元空間代替(Metaspace)運(yùn)行時(shí)常量池中的內(nèi)容,主要源于 class 靜態(tài)常量池辩涝,也就是編譯階段確定的常量池贸伐。但也可以通過(guò)
String.intern()
方法,手動(dòng)將字符串常量放入運(yùn)行時(shí)常量池中怔揩,否則 JVM 不會(huì)主動(dòng)添加常量至常量池捉邢。
為何選擇常量池存放 String
常量池是為了避免頻繁的創(chuàng)建和銷(xiāo)毀對(duì)象而影響系統(tǒng)性能,其實(shí)現(xiàn)了對(duì)象的共享商膊。
例如字符串常量池伏伐,在編譯階段就把所有的字符串文字放到一個(gè)常量池中。
- 節(jié)省內(nèi)存空間:常量池中所有相同的字符串常量被合并晕拆,只占用一個(gè)空間藐翎。
- 節(jié)省運(yùn)行時(shí)間:比較字符串時(shí),
==
比equals()
快实幕。對(duì)于兩個(gè)引用變量吝镣,只用==
判斷引用是否相等,也就可以判斷實(shí)際值是否相等昆庇。
String
何時(shí)為常量末贾,入常量池
何時(shí)視為常量,何時(shí)入常量池凰锡?先了解什么是常量表達(dá)式和 ==
與 equals()
的區(qū)別吧未舟。
常量表達(dá)式
要解決這個(gè)問(wèn)題圈暗,要先理解常量表達(dá)式掂为。
常量表達(dá)式
:指代表基本數(shù)據(jù)類(lèi)型或者 String
數(shù)據(jù)類(lèi)型的表達(dá)式裕膀,能在編譯期間能計(jì)算出來(lái)的值,因此表達(dá)式中的均需為常量勇哗,不可為變量昼扛。
對(duì)于常量表達(dá)式,Java 編譯時(shí)會(huì)進(jìn)行優(yōu)化欲诺,直接賦予計(jì)算后的常量值抄谐。
==
和 equals()
-
==
: 判斷兩個(gè)對(duì)象是否為同一對(duì)象,即判斷引用的是否為同一個(gè)對(duì)象扰法。 -
equals()
:判斷兩個(gè)對(duì)象的值是否相同蛹含。類(lèi)中默認(rèn)的equals()
同==
判斷,但可被自定義覆蓋塞颁。
舉例
了解了常量表達(dá)式浦箱,來(lái)看看下面的實(shí)例。
private final static String staticA = "AAA"; // 常量
private final static String staticB = "111"; // 常量
private final static String staticC;
private final static String staticD;
private final static String staticE;
static {
staticC = "AAA";
staticD = "111";
staticE = "AAA111";
}
public static void main(String[] args) {
String str0 = "AAA111";
String str1 = "AAA" + "111";
String str2 = staticA + staticB;
String str3 = staticC + staticD;
String str4 = "AAA" + 111;
String str5 = staticA + 111;
String str6 = staticC + 111;
String str7;
str7 = staticC + staticD;
String str8;
str8 = str7 + "";
String str9 = str8.intern();
System.out.println(str0 == str8); // true
}
看如下代碼祠锣,其中 str0~str8
的值均為 AAA111
酷窥。
但當(dāng)彼此進(jìn)行 ==
操作時(shí),卻不均為 true
伴网,說(shuō)明底層并未指向相同的對(duì)象蓬推。
staticE, str0, str1, str2, str4, str5,str9
彼此進(jìn)行 ==
判斷時(shí),為 true
澡腾。
staticA == staticC沸伏,staticB == staticD
為 true
。
str3, str6, str7, str8
彼此均為 false
动分。
此圖為 Java8 示意馋评,Java8 之前的運(yùn)行時(shí)常量池是在方法區(qū)。
對(duì)以上代碼分析:
staticA ~ staticE
五個(gè)變量刺啦,均為 final
常量留特。但 staticC~staticE
與 staticA,staticB
略有區(qū)別玛瘸,staticC~staticD
雖然是常量蜕青,但在編譯期未被賦值,是到運(yùn)行時(shí)才被賦值糊渊,因此性質(zhì)類(lèi)似于一個(gè)變量右核,不可視為編譯時(shí)常量。staticE
也是變量渺绒,但賦值直接為 AAA111
贺喝。
str0~str8
部分菱鸥,均為棧內(nèi)定義的變量。
-
str0
在編譯時(shí)躏鱼,直接賦值氮采,執(zhí)行的是常量表達(dá)式。AAA111
入常量池染苛,str0 為其引用鹊漠。 -
str1
在編譯時(shí),是由兩個(gè)常量AAA
和111
連接所得茶行,值也可以確定躯概。由于str0
時(shí),已經(jīng)將AAA111
放入常量池畔师,因此str1
復(fù)用娶靡,引用同一常量池對(duì)象。 -
str2
是staticA
和staticB
連接看锉,由于staticA姿锭,staticB
值是常量,執(zhí)行的是常量表達(dá)式度陆,引用常量池艾凯。 -
str3
是staticC
和staticD
連接,但staticC
和staticD
未被直接賦值懂傀,編譯期無(wú)法決定值趾诗。 -
str4
和str5
均能在編譯期決定值,因此也引用常量池 -
str6~str8
均無(wú)法在編譯期決定值蹬蚁,因此不引用常量池恃泪。 -
str9
使用了String.inertn()
,若字符串已在常量池存在犀斋,則引用已有常量池對(duì)象贝乎,若不存在,則會(huì)手動(dòng)將字符串放入字符串常量池叽粹,并引用览效。
討論完常量池的情況,再來(lái)看看堆的情況虫几。
String sA = "ABCD";
String sB = new String("ABCD");
String sC = new String("ABCD").intern();
System.out.println(sA == sB); // false
System.out.println(sA == sC); // true
System.out.println(sB == sC); //false
如上代碼锤灿,當(dāng) new
一個(gè)對(duì)象時(shí),Java 會(huì)將其放置于堆中辆脸。因此但校,顯然不會(huì)與常量池中的引用相等,sA == sB
為 false啡氢。
但如上文所述状囱,如果主動(dòng)調(diào)用 String.intern()
方法术裸,則會(huì)將字符串放入常量池,此處 ABCD
字符串已存在亭枷,因此sC
直接引用常量池中的字符串對(duì)象袭艺。
仔細(xì)分析可知,在 new String("ABCD")
時(shí)奶栖,可能創(chuàng)建一個(gè)或兩個(gè)對(duì)象匹表。若 new
的字符串已經(jīng)存在门坷,則僅會(huì)在堆上創(chuàng)建一個(gè)對(duì)象宣鄙,但若字符串不存在,則會(huì)先在常量池中創(chuàng)建默蚌,然后再堆中創(chuàng)建對(duì)該字符串的引用冻晤。
String 實(shí)踐
這部分,主要是總結(jié) 《Java 編程思想》13章字符串章節(jié)绸吸。
JDK 中 +
的重載與 StringBuilder
優(yōu)化
由于 String
對(duì)象的不可變鼻弧。每次對(duì)字符串的變更,均會(huì)創(chuàng)建一個(gè)新的對(duì)象锦茁,那么出現(xiàn)下面情況時(shí)攘轩,會(huì)產(chǎn)生大量的中間變量,使得代碼效率降低码俩。
String hello = "h" + "e" + "l" + "l" + "o";
若不進(jìn)行優(yōu)化度帮,上面代碼會(huì)在字符串常量池中創(chuàng)建 h, e, l, o, he, hel, hell, hello
,這么多中間對(duì)象稿存。
Java 對(duì)此進(jìn)行了優(yōu)化笨篷。
以下代碼為例
public static void main(String[] args) {
String str1 = "abc";
String str2 = str1 + "h" + "e" + "l" + "l" + "o";
}
利用 JDK 提供的 javap -c XXXX
反編譯工具,可以看到底層實(shí)現(xiàn)瓣履。
0: ldc #2 // String abc
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String h
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: ldc #7 // String e
21: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: ldc #8 // String l
26: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
29: ldc #8 // String l
31: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
34: ldc #9 // String o
36: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
39: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
42: astore_2
可以發(fā)現(xiàn)率翅,編譯器自動(dòng)引入了 StringBuilder
類(lèi),在每次重載 +
時(shí)袖迎,底層均調(diào)用一次 StringBuilder.append()
方法冕臭。這減少了中間對(duì)象,提高了效率燕锥。
雖然編譯器會(huì)幫助我們優(yōu)化辜贵,但用 +
效率還是比較低。這是因?yàn)槊看螆?zhí)行字符串 +
脯宿,都會(huì)創(chuàng)建 StringBuilder
對(duì)象念颈。
String str1 = "";
for (int i = 0; i < 100; i++) {
str1 += i;
}
對(duì)應(yīng)反編譯字節(jié)碼為,從 6~18 行為循環(huán)连霉,第 10行榴芳,會(huì)創(chuàng)建 StringBuilder
對(duì)象嗡靡。在循環(huán)中,創(chuàng)建對(duì)象窟感,調(diào)用了兩次 append()
方法和一次 toString()
方法讨彼,效率不高。
0: ldc #2 // String
2: astore_1
3: iconst_0
4: istore_2
5: iload_2
6: bipush 100
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
因此還是推薦主動(dòng)創(chuàng)建 StringBuilder
對(duì)象柿祈」螅可以?xún)?yōu)化為
String str1 = "";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
sb.append(i);
}
str1 = sb.toString();
反編譯結(jié)果如下,可以看到在循環(huán)外創(chuàng)建了一次 StringBuilder
,并且循環(huán)內(nèi)也只調(diào)用了一次 append()
方法站蝠,最終調(diào)用了一次 toString()
奥务。
0: ldc #2 // String
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: astore_2
11: iconst_0
12: istore_3
13: iload_3
14: bipush 100
16: if_icmpge 31
19: aload_2
20: iload_3
21: invokevirtual #5 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
24: pop
25: iinc 3, 1
28: goto 13
31: aload_2
32: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
35: astore_1
避免 toString()
無(wú)意識(shí)的遞歸
Java 的所有類(lèi)均繼承于 Object
,因此所有類(lèi)均可重寫(xiě) toString()
方法重荠,toString
方法常被用于打印對(duì)象的基本信息。
但如果在 toString
方法中虚茶,用到了 this
戈鲁,便會(huì)出現(xiàn)無(wú)限遞歸,報(bào) StackOverflowError
異常嘹叫。例如以下代碼:
public class InfRec {
@Override
public String toString() {
return "InfRec" + this;
}
public static void main(String[] args) {
System.out.println(new InfRec());
}
}
應(yīng)該將 this
改為 super.toString()
婆殿;
StringBuilder
vs StringBuffer
-
StringBuilder
:非線性安全,效率更高罩扇,于 Java 5中加入 -
StringBuffer
:線性安全婆芦,使用了synchronized
關(guān)鍵字。效率低暮蹂,不推薦使用寞缝,即使是多線程環(huán)境,也有更好的方案仰泻。
更多
String
使用荆陆,參考 JDK 源碼
總結(jié)
-
String
不是基礎(chǔ)數(shù)據(jù)類(lèi)型,是一個(gè)類(lèi)集侯,默認(rèn)值是null
而非""
被啼。 -
String
是由char[]
構(gòu)成,Java 內(nèi)碼采用UTF-16
對(duì) unicode 編碼棠枉。因此存在一個(gè)字符長(zhǎng)度為 2的情況浓体,如 ?? 對(duì)應(yīng)的\uD83D\uDE02
。 -
String
為常量辈讶,一旦定義不可變更命浴。若修改,會(huì)創(chuàng)建新的對(duì)象。 -
String
傳遞時(shí)為引用拷貝生闲。 - 通過(guò)定義常量或者常量表達(dá)式媳溺,可以于編譯期確定
String
的值的,會(huì)將該字符串放入 class 靜態(tài)常量池碍讯,當(dāng)類(lèi)加載時(shí)悬蔽,載入至運(yùn)行時(shí)常量池。 - 可通過(guò)
String.intern()
方法捉兴,主動(dòng)將字符串放置入常量池蝎困,若常量池已存在該字符串,會(huì)直接引用倍啥。若不主動(dòng)調(diào)用intern()
方法禾乘,JVM 不會(huì)主動(dòng)將字符串放入常量池。 -
new String("ABCD")
過(guò)程逗栽,會(huì)創(chuàng)建一個(gè)或兩個(gè)對(duì)象盖袭,或有一個(gè)位于常量池失暂,另一個(gè)位于堆中彼宠。 - 當(dāng)代碼涉及較多字符串
+
操作時(shí),使用StringBuilder
能提高效率 - 不要在
toString
方法中使用this
弟塞,避免無(wú)限遞歸凭峡,應(yīng)該用super.toString()
-
StringBuilder
非線性安全,StringBuffer
使用了synchronized
關(guān)鍵字决记,效率低摧冀,不推薦使用。
參考資料
[1] 深入理解Java虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐(第2版)系宫,作者周志明
[2] 《Java 編程思想》第4版索昂,作者 Bruce Eckel
[3] class文件常量池和運(yùn)行時(shí)常量池比對(duì), http://www.ifcoding.com/archives/284.html
[4] 什么是字符串常量池扩借?椒惨, http://www.importnew.com/10756.html
[5] Java篇-String詳解, TianTianBaby223潮罪,http://www.reibang.com/p/d832752caf0c
[6] Java常用類(lèi)(二)String類(lèi)詳解康谆, https://www.cnblogs.com/zhangyinhua/p/7689974.html
[7] String類(lèi)詳解, https://juejin.im/post/59f6eb076fb9a045154329cc
[8] Top 10 questions of Java Strings嫉到,http://www.programcreek.com/2013/09/top-10-faqs-of-java-strings/
[9] Java中String詳解沃暗,作者 Lolita, https://zhuanlan.zhihu.com/p/29629508