我們來考慮一個(gè)關(guān)于java中String的問題: "abc" + '/'和 "abc" + "/"的區(qū)別. 通過這個(gè)例子, 我們可以順便練習(xí)一下JDK工具中javap的用法, 原問題是這樣的:
把斜杠/當(dāng)作字符或字符串有什么區(qū)別呢?
一個(gè)是當(dāng)作基本數(shù)據(jù)類型char,一個(gè)是對象String。具體有什么區(qū)別呢?
當(dāng)作字符效率會更高嗎?
String str = "abc" + '/';
和
String str = "abc" + "/";
1、編譯器優(yōu)化
首先大家應(yīng)該知道, 上面那兩句效果是一樣的, 因?yàn)榫幾g器會把上面那兩句都優(yōu)化成下面的樣子:
1
|
String str = "abc/" ; |
我們可以通過javap證明這一點(diǎn). 關(guān)于javap, 可以參考《每個(gè)Java開發(fā)者都應(yīng)該知道的5個(gè)JDK工具》. 我們首先創(chuàng)建一個(gè)類: StringOne, 在主方法中填入下面的代碼:
1
2
3
4
|
StringOne.java String str1 = "abc" + '/' ; String str2 = "abc" + "/" ; System.out.println(str1 == str2); |
編譯并運(yùn)行, 輸出結(jié)果為true. 接下來, 該我們的javap登場了, 在命令行中輸入下面的命令:
1
|
javap -v -l StringOne. class > StringOne.s |
然后查看生成的StringOne.s文件. 就會發(fā)現(xiàn)其中有這么幾行
1
2
3
4
5
6
7
8
9
|
StringOne.s # 2 = String # 20 // abc/ ... # 20 = Utf8 abc/ ... 0 : ldc # 2 // String abc/ 2 : astore_1 3 : ldc # 2 // String abc/ 5 : astore_2 |
說明str1和str2都引用字符串"abc\".
2、使用javap分析差異
現(xiàn)在我們換一個(gè)問法, 下面的代碼中stringAddString和stringAddChar方法有什么區(qū)別?
1
2
3
4
5
6
7
8
|
StringTwo public static String stringAddString(String str1, String str2){ return str1 + str2; } public static String stringAddChar(String str, char ch){ return str + ch; } |
這次再使用javap進(jìn)行反編譯, 生成文件的部分內(nèi)容如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
StringTwo.s public java.lang.String stringAddString(java.lang.String, java.lang.String); descriptor: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; flags: ACC_PUBLIC Code: stack= 2 , locals= 3 , args_size= 3 0 : new # 2 // class java/lang/StringBuilder 3 : dup 4 : invokespecial # 3 // Method java/lang/StringBuilder."< init>":()V 7 : aload_1 8 : invokevirtual # 4 // Method java/lang/StringBuilder. append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 11 : aload_2 12 : invokevirtual # 4 // Method java/lang/StringBuilder. append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 15 : invokevirtual # 5 // Method java/lang/StringBuilder. toString:()Ljava/lang/String; 18 : areturn public java.lang.String stringAddChar(java.lang.String, char ); descriptor: (Ljava/lang/String;C)Ljava/lang/String; flags: ACC_PUBLIC Code: stack= 2 , locals= 3 , args_size= 3 0 : new # 2 // class java/lang/StringBuilder 3 : dup 4 : invokespecial # 3 // Method java/lang/StringBuilder."<init>":()V 7 : aload_1 8 : invokevirtual # 4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 11 : iload_2 12 : invokevirtual # 6 // Method java/lang/StringBuilder.append:(C)Ljava/lang/StringBuilder; 15 : invokevirtual # 5 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 18 : areturn |
現(xiàn)在, 我們已經(jīng)可以很清楚的看出這兩個(gè)方法執(zhí)行的流程了:
stringAddString
- 創(chuàng)建一個(gè)StringBuilder對象
- 使用append方法, 依次將兩個(gè)參數(shù)添加到剛才創(chuàng)建的StringBuilder中.
- 調(diào)用toString方法.
- return toString方法的返回值.
stringAddChar的過程和stringAddString一樣, 只是在第二次調(diào)用append方法時(shí)stringAddString的參數(shù)是String類型, 而stringAddChar的參數(shù)是char類型.
3、StringBuilder類的append(char)方法和append(String)方法
這里,我們直接查看源碼就好了(我的是jdk1.8.0_60附帶的源碼)。 注意,雖然文檔上顯示StringBuilder繼承自O(shè)bject, 但是從源碼來看, 它是繼承自抽象類AbstractStringBuilder的。而且append方法是由AbstractStringBuilder實(shí)現(xiàn)的。
AbstractStringBuilder.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public AbstractStringBuilder append( char c) { ensureCapacityInternal(count + 1 ); // 確保數(shù)組能夠容納count+1個(gè)字符 value[count++] = c; return this ; } public AbstractStringBuilder append(String str) { if (str == null ) return appendNull(); int len = str.length(); ensureCapacityInternal(count + len); str.getChars( 0 , len, value, count); // 拷貝字符串中的字符數(shù)組到本對象的字符數(shù)組中 count += len; return this ; } |
剩下的就不再貼出來了。String.getChars(int, int, char[], int)最終依賴于public static native void arraycopy(Object, int, Object, int, int)。也就是說有可能是C語言寫的,在拷貝大型數(shù)組時(shí)效率應(yīng)該會比java寫的程序好一些。 那么,現(xiàn)在說說我的理解:
從直接內(nèi)存來說, 由于String中包含char數(shù)組, 而數(shù)組應(yīng)該是有長度字段的, 同時(shí)String類還有一個(gè)int hash屬性, 再加上對象本身會占用額外的內(nèi)存存儲其他信息,所以字符串會多占用一些內(nèi)存. 但是如果字符串非常長, 那么這些內(nèi)存開銷差不多就可以忽略了; 而如果像"/"這種情況, 字符串比較(非常)短,那么就很可能有許多個(gè)共享引用來分擔(dān)這些內(nèi)存開銷, 那么多余的內(nèi)存開銷還是可以忽略的.
從調(diào)用堆棧上, 由于這里String只比char多了一兩層函數(shù)調(diào)用,所以如果不考慮函數(shù)調(diào)用開銷(包括時(shí)間和空間), 應(yīng)該差不多;考慮函數(shù)調(diào)用開銷, 應(yīng)該 "abc" + '/'更好一些; 但是當(dāng)需要連接若干個(gè)字符時(shí)(感覺這種情況應(yīng)該更常見吧?), 由于使用char需要循環(huán)好多次才能完成連接, 調(diào)用的函數(shù)次數(shù)只會比使用String更多. 同時(shí)拷貝也不會比String直接拷貝一個(gè)數(shù)組更快. 所以這個(gè)時(shí)候就變成了"abc" + "/"吞吐量更大.
現(xiàn)在感覺這個(gè)問題像是在問: 讀寫文件時(shí)使用系統(tǒng)調(diào)用效率高, 還是使用標(biāo)準(zhǔn)函數(shù)庫中的IO庫效率高. 個(gè)人感覺, 雖然標(biāo)準(zhǔn)IO庫最后還得調(diào)用系統(tǒng)調(diào)用, 而且這之間會產(chǎn)生一些臨時(shí)變量, 以及更深層次的調(diào)用堆棧, 但是由于IO庫的緩沖等機(jī)制, 所以IO庫的吞吐量會更大, 而系統(tǒng)調(diào)用的實(shí)時(shí)性會好一些. 同樣, 雖然String類會多幾個(gè)字段, 有更深層次的函數(shù)堆棧, 但是由于緩存以及更直接的拷貝, 吞吐量應(yīng)該會更好一些.
新的問題
從上面javap的反編譯代碼來看, 兩個(gè)String相加, 會變成向StringBuilder中append字符串. 那么理論上, 下面哪段代碼的效率好呢?
1
2
3
4
5
6
|
String str1 = "abc" + "123" ; // 1 StringBuilder stringBuilder = new StringBuilder(); // 2 stringBuilder.append( "abc" ); stringBuilder.append( "123" ); String str2 = stringBuilder.toString(); |
這個(gè)問題就留給大家思考吧!
以上就是本文的全部內(nèi)容,幫助大家更好的區(qū)分java中String+String和String+char,希望對大家的學(xué)習(xí)有所幫助。