本文首發(fā):windCoder.com
由于具體關(guān)注的內(nèi)容的特殊性,如無特殊注明,本文討論均基于Java8。
不可變
String對(duì)象是不可變的。每次修改都是創(chuàng)建了一個(gè)全新的String對(duì)象,以包含修改后的字符串內(nèi)容,最初的String對(duì)象在原處絲毫未動(dòng)。
對(duì)一個(gè)方法而言,參數(shù)是為該方法提供信息的,而不是想讓該方法改變自己的。
- String類是final的,不可被繼承。
- String類的本質(zhì)是字符數(shù)組char[], 并且其值不可改變。即:
private final char value[]; - String類對(duì)象有個(gè)特殊的創(chuàng)建的方式,直接賦值,如'''String x = "abc"```, 字面量(String Literals)"abc" 就表示一個(gè)字符串對(duì)象,變量 x 指向其該字符串對(duì)象的地址,即是一個(gè)引用。
- JVM存在一個(gè)String Pool(String池/字符串常量池/全局字符串池,也有叫做string literal pool),1.7之前處于方法區(qū)中,之后被分離出來放在了堆中。
- 兩個(gè)有用的類StringBuffer和StringBuilder。前者線程安全,但比后者速度較慢。
- 1.8新出了一個(gè)StringJoiner類,,用于構(gòu)造由分隔符分隔的字符序列,并可選擇性地從提供的前綴開始和以提供的后綴結(jié)尾。
重載“+”
內(nèi)部并不是創(chuàng)建n個(gè)String對(duì)象,而是創(chuàng)建了一個(gè)StringBuilder對(duì)象,通過其append()方法連接,最后調(diào)用toStrong()方法返回。
當(dāng)為類似String s = "a" + "b" + "c";的單行操作時(shí),編譯器會(huì)執(zhí)行優(yōu)化,在編譯時(shí)直接合成一個(gè)“abc”。
該操作適用于單行“+”操作,不適用于循環(huán)(如for等)。因?yàn)樵谘h(huán)中,每次循環(huán)會(huì)生成一個(gè)新的個(gè)StringBuilder對(duì)象。
循環(huán)時(shí)的手動(dòng)優(yōu)化:在外創(chuàng)建StringBuilder對(duì)象,在循環(huán)內(nèi)部執(zhí)行append()方法拼接字符串。
StringBuilder 是JavaSE5引入的,之前都是StringBuffer。后者是線程安全的,因此開銷會(huì)大些,所以在javaSE5及以后中,字符串操作應(yīng)該還會(huì)更快一點(diǎn)。
創(chuàng)建
創(chuàng)建方式
創(chuàng)建字符串的方式很多,歸納起來有三類:
- 使用new關(guān)鍵字創(chuàng)建字符串,比如String s1 = new String("abc");
- 直接指定。比如String s2 = "abc";
- 使用串聯(lián)生成新的字符串。比如String s3 = "ab" + "c";
分析創(chuàng)建
下面一起看下在創(chuàng)建與運(yùn)行時(shí)內(nèi)部具體發(fā)生了些什么。
示例1
public class StringDemo1 {
public static void main(String[] args) {
String s1 = new String("123");
}
}
當(dāng)僅運(yùn)行這段代碼期間,涉及用戶聲明的幾個(gè)String變量?
答案很簡(jiǎn)單:
一個(gè),就是String s。
涉及的實(shí)例/對(duì)象呢?
先說答案:
兩個(gè),一個(gè)是字符串字面量"123"所對(duì)應(yīng)的、駐留(intern)在一個(gè)全局共享的字符串常量池中的實(shí)例,另一個(gè)是通過new String(String)創(chuàng)建并初始化的、內(nèi)容與"123"相同的實(shí)例。
至于原因,要從StringDemo1類的編譯說起:
當(dāng)編譯完成,會(huì)生成StringDemo1.class文件,該文件中,"123"會(huì)被提取并放置在class常量池中,當(dāng)JVM加載類時(shí)會(huì)通過讀取該class常量池創(chuàng)建并駐留一個(gè)String實(shí)例作為常量來對(duì)應(yīng)"123"字面量(其引用存儲(chǔ)在String Pool中,未注明時(shí)以下均稱“字符串池”或“常量池”),這是一個(gè)全局共享的,只有當(dāng)字符串池中沒有相同內(nèi)容的字符串時(shí)才需要?jiǎng)?chuàng)建。
當(dāng)執(zhí)行main方法中的new語(yǔ)句時(shí),JVM會(huì)執(zhí)行的字節(jié)碼類似:
0: new #2 // class java/lang/String
3: dup
4: ldc #3 // String 123
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
這之中出現(xiàn)過多少次new java/lang/String就是創(chuàng)建了多少個(gè)String對(duì)象,即代碼String s1 = new String("123");每執(zhí)行一次只會(huì)創(chuàng)建一個(gè)實(shí)例對(duì)象。
下面是RednaxelaFX對(duì)于這段字節(jié)碼含義的描述:
在JVM里,“new”字節(jié)碼指令只負(fù)責(zé)把實(shí)例創(chuàng)建出來(包括分配空間、設(shè)定類型、所有字段設(shè)置默認(rèn)值等工作),并且把指向新創(chuàng)建對(duì)象的引用壓到操作數(shù)棧頂。此時(shí)該引用還不能直接使用,處于未初始化狀態(tài)(uninitialized);
如果某方法a含有代碼試圖通過未初始化狀態(tài)的引用來調(diào)用任何實(shí)例方法,那么方法a會(huì)通不過JVM的字節(jié)碼校驗(yàn),從而被JVM拒絕執(zhí)行。
能對(duì)未初始化狀態(tài)的引用做的唯一一種事情就是通過它調(diào)用實(shí)例構(gòu)造器,在Class文件層面表現(xiàn)為特殊初始化方法
<init>。實(shí)際調(diào)用的指令是invokespecial,而在實(shí)際調(diào)用前要把需要的參數(shù)按順序壓到操作數(shù)棧上。
在上面的字節(jié)碼例子中,壓參數(shù)的指令包括dup和ldc兩條,分別把隱藏參數(shù)(新創(chuàng)建的實(shí)例的引用,對(duì)于實(shí)例構(gòu)造器來說就是“this”)與顯式聲明的第一個(gè)實(shí)際參數(shù)("123"常量的引用)壓到操作數(shù)棧上
最終如圖:

黑線表示String對(duì)象的內(nèi)容指向。
示例2
public class StringDemo2 {
public static void main(String[] args) {
String s1 = new String("123");
String s2 = "123";
}
}
這里我們看下String s2 = "123";的字節(jié)碼:
10: ldc #3 // String 123
12: astore_2
由此可見s2直接引用的是字符串常量池中的對(duì)象。故該實(shí)例中依舊是生成了2個(gè)實(shí)例對(duì)象。如圖:

黑線同實(shí)例1中的,紅線為s2引用的指向,因?yàn)槌A砍刂幸呀?jīng)存在"123",所以不會(huì)再創(chuàng)建。s2會(huì)通過查詢常量池獲取池中"123"的地址并指向。
若再加一個(gè)String s3 = new String("123");呢?此時(shí)只會(huì)再創(chuàng)建一個(gè)實(shí)例對(duì)象,從而一共是3個(gè)。從而有了如下:
public class StringDemo2 {
public static void main(String[] args) {
String s1 = new String("123");
String s2 = "123";
String s3 = new String("123");
PrintUtill.println(s1==s2);
PrintUtill.println(s2==s3);
PrintUtill.println(s1==s3)
}
}
結(jié)果為:
false
false
false
StringJoiner用法簡(jiǎn)介
StringJoiner類是Java8的一個(gè)新類(還有一個(gè)新類Optional可用來解決空指針的問題),可以通過指定分隔符拼接字符串,功能與String.join方法類似,同時(shí)可選擇性地從提供的前綴開始和以提供的后綴結(jié)尾。這里簡(jiǎn)單展示用法,不做過多討論。
StringJoiner sj = new StringJoiner(":", "[", "]");
sj.add("www").add("windcoder").add("com");
String desiredString = sj.toString();
PrintUtill.println(desiredString);
執(zhí)行結(jié)果:
[www:windcoder:com]
String.join()內(nèi)部實(shí)現(xiàn)則用了StringJoiner,其源碼如下:
public static String join(CharSequence delimiter, CharSequence... elements) {
Objects.requireNonNull(delimiter);
Objects.requireNonNull(elements);
// Number of elements not likely worth Arrays.stream overhead.
StringJoiner joiner = new StringJoiner(delimiter);
for (CharSequence cs: elements) {
joiner.add(cs);
}
return joiner.toString();
}
反編譯指令
基礎(chǔ)命令
javap反編譯指令可查看編譯后的.class文件的字節(jié)碼信息,這里是做簡(jiǎn)單的使用記錄,不做過多討論:
javap -c Concatenation
若想查看更詳細(xì)的常量池等信息,可添加-verbose選項(xiàng),即:
javap -c -verbose Concatenation
-c 輸出類中各方法的未解析的代碼,即構(gòu)成 Java 字節(jié)碼的指令。
-verbose 輸出堆棧大小、各方法的 locals 及 args 數(shù),以及class文件的編譯版本。
如當(dāng)想反編譯上面的StringDemo1.class文件,執(zhí)行如下命令即可:
javap -c StringDemo1.class
指令簡(jiǎn)說
dup 復(fù)制棧頂數(shù)值(數(shù)值不能是long或double類型的)并將復(fù)制值壓入棧頂
ldc 將int, float或String型常量值從常量池中推送至棧頂。
invokespecial 調(diào)用實(shí)例構(gòu)造器<init>方法, 私有方法和父類方法
官方對(duì)dup的解釋(6.5.dup)如下:
Duplicate the top value on the operand stack and push the duplicated value onto the operand stack.
The dup instruction must not be used unless value is a value of a category 1 computational type (§2.11.1).
官方對(duì)ldc推送String的描述如下,由此也可看出字符串常量池中的存儲(chǔ)的String屬于引用,當(dāng)ldc推送時(shí),其實(shí)推送的也是引用:
The index is an unsigned byte that must be a valid index into the run-time constant pool of the current class (§2.6). The run-time constant pool entry at index either must be a run-time constant of type int or float, or a reference to a string literal, or a symbolic reference to a class, method type, or method handle (§5.1).
.....
if the run-time constant pool entry is a reference to an instance of class String representing a string literal (§5.1), then a reference to that instance, value, is pushed onto the operand stack.(6.5.ldc)
.....