身邊有些做Java開發的朋友,找工作時常常被考到一道關於字符串的題目。題目倒是很基礎,然而根據朋友們事後的描述,有理由認爲有的面試官自己都沒有完全搞清楚這個問題。此外,在CSDN論壇中我也多次看到一些朋友在這個問題上的迷惑。索性把自己的理解寫下來吧。
題目是一道簡單的小程序,像下面這樣:
public class Test1 {
public static void main(String args[]) {
String s = new String("Hello");
System.out.println(s);
foo(s);
System.out.println(s);
}
public static void foo(String s) {
s = new String("World");
}
}
問程序先後兩次分別會輸出什麼。
第一個肯定輸出“Hello”。關鍵是第二個,個別基礎不牢的朋友可能被考倒,但基礎稍微紮實一點的就不會。第二個輸出的也是“Hello”。
到這裏,萬事大吉。面試官兩眼放出喜悅的光芒,讚歎道:“嗯,不錯不錯。不會變的,對吧。因爲String是immutable類型,immutable類型改不了的。”他說的“immutable”是指String類沒有任何一個方法會改變對象的狀態。
這可就有問題了。確實程序兩次輸出的都是“Hello”,但原因卻不是String的immutable特性。不信換一個“mutable”的試試,比如StringBuilder或StringBuffer。
public class Test2 {
public static void main(String args[]) {
StringBuilder s = new StringBuilder("Hello");
System.out.println(s);
foo(s);
System.out.println(s);
}
public static void foo(StringBuilder s) {
s = new StringBuilder("World");
}
}
這次呢?會先輸出“Hello”,然後輸出“World”嗎?不會,仍然是兩次“Hello”。
足以說明這個問題跟String的immutable特性沒有一毛錢的關係。不管是immutable類型,還是一般的類型,用這種方式寫出來的程序main方法中前後兩個對象肯定是一樣的。
真正的原因是:Java語言的參數傳遞機制是“按值傳遞”(pass by value)。雖然Java中除基本數值類型外,其它變量都是引用,但那是另一回事。變量的語義(“引用”還是“值”)跟函數傳參的機制是兩個正交的概念。
各種編程語言中,最常見的參數傳遞方式不外乎按值傳遞和按引用傳遞(pass by reference)兩種。比如,C和Java中函數參數都是按值傳遞的,C++和C#則同時支持按值傳遞和按引用傳遞。C和Java的不同在於,C是值類型的按值傳遞,而Java是引用類型的按值傳遞(基本的數值類型除外)。
按值傳參最大的特點就是函數內部對“形參變量”本身的所做的修改外面的“實參變量”感知不到。
不過,對於StringBuilder來說我們至少有辦法讓它改變,比如像下面這樣:
public class Test3 {
public static void main(String args[]) {
StringBuilder s = new StringBuilder("Hello");
System.out.println(s);
foo(s);
System.out.println(s);
}
public static void foo(StringBuilder s) {
s.replace(0, s.length(), "World");
}
}
體會到不同了嗎?雖然參數變量本身是按值傳遞的,但這次我們對變量本身不感興趣,我們不改變變量本身,而是通過它直接修改它所引用的那個對象。這一次,Java語言“幾乎一切皆引用”的特點起作用了,程序第一次輸出“Hello”,第二次輸出“World”。熟悉C的朋友可能馬上聯想到:這很像C語言中通過指針來修改它指向的內容。
而先前那個String版本的程序,我們無法寫出一個相應的可變版本。爲什麼呢?……“嗯,不錯不錯。因爲String是immutable類型……”,這次真對了。——可見先前說“沒有一毛錢的關係”也不盡然。因爲immutable,導致我們無法針對String寫出一個像Test3那樣的“可變”程序,如此說來,“二分錢的關係”應該是有的。
“pass reference by value”就像C++ 11中的“An lvalue with rvalue reference type”一樣,乍一看挺繞的,但只要仔細想清楚,讓正交的東西“塵歸塵 土歸土”,就可以加深對語言的理解。
順便問一句,您知道java.lang.StringBuilder和java.lang.StringBuffer的區別嗎?好多面試官都喜歡“順便”問問這個,而且答對了不會加分,答不上來卻會扣分,至少扣印象分。:(
Java.lang.StringBuffer線程安全的可變字符序列。類似於 String 的字符串緩衝區,但不能修改。可將字符串緩衝區安全地用於多個線程。可以在必要時對這些方法進行同步,因此任意特定實例上的所有操作就好像是以串行順序發生的,該順序與所涉及的每個線程進行的方法調用順序一致。
每個字符串緩衝區都有一定的容量。只要字符串緩衝區所包含的字符序列的長度沒有超出此容量,就無需分配新的內部緩衝區數組。如果內部緩衝區溢出,則此容量自動增大。從 JDK 5.0 開始,爲該類增添了一個單個線程使用的等價類,即 StringBuilder 。與該類相比,通常應該優先使用 StringBuilder 類,因爲它支持所有相同的操作,但由於它不執行同步,所以速度更快。
但是如果將 StringBuilder 的實例用於多個線程是不安全的。需要這樣的同步,則建議使用 StringBuffer 。
那麼下面我們再做一個一般性推導:
在大部分情況下 StringBuilder > StringBuffer
因此,根據這個不等式的傳遞定理: 在大部分情況下
StringBuilder > StringBuffer > String
====================================================================
String是一個類,但卻是不可變的,所以String創建的算是一個字符串常量,StringBuffer和StringBuilder都是可變的。所以每次修改String對象的值都是新建一個對象再指向這個對象。而使用StringBuffer則是對StringBuffer對象本身進行操作。所以在字符串j經常改變的情況下,使用StringBuffer要快得多。
但在某些情況下:
- String S1 = “Who” + “ is” + “ faster?”;
- StringBuffer Stb = new StringBuilder(“Who”).append(“ is”).append(“ faster?”);
- String S1 = “Who” + “ is” + “ faster?”;
- StringBuffer Stb = new StringBuilder(“Who”).append(“ is”).append(“ faster?”);
S1的素對會比Stb快得多, 是因爲JVM把String對象的拼接解釋成了StringBuffer對象的拼接,其實在JVM就是:
- String S1="Who is faster?";
- String S1="Who is faster?";
不過如果,字符串是來自其他對象,如:
- String s1="Who";
- String s2=" is";
- String s3=" faster?";
- String st=s1+s2+s3;
- String s1="Who";
- String s2=" is";
- String s3=" faster?";
- String st=s1+s2+s3;
這個時候,String的速度就比不上StringBuffer了。
StringBuffer和StringBuilder
在操作字符串對象,StringBuiler是最快的,StringBuffer次之,String最慢。
- public final class StringBuffer
- extends AbstractStringBuilder
- implements java.io.Serializable, CharSequence
- ublic final class StringBuilder
- extends AbstractStringBuilder
- implements java.io.Serializable, CharSequence
- public final class StringBuffer
- extends AbstractStringBuilder
- implements java.io.Serializable, CharSequence
- ublic final class StringBuilder
- extends AbstractStringBuilder
- implements java.io.Serializable, CharSequence
可以看到StringBuffer和StringBuilder都繼承繼承了同一個抽象類。
Java.lang.StringBuffer線程安全的可變字符序列。一個類似於 String 的字符串緩衝區,但不能修改。雖然在任意時間點上它都包含某種特定的字符序列,但通過某些方法調用可以改變該序列的長度和內容。
可將字符串緩衝區安全地用於多個線程。可以在必要時對這些方法進行同步,因此任意特定實例上的所有操作就好像是以串行順序發生的,該順序與所涉及的每個線程進行的方法調用順序一致。
每個字符串緩衝區都有一定的容量。只要字符串緩衝區所包含的字符序列的長度沒有超出此容量,就無需分配新的內部緩衝區數組。如果內部緩衝區溢出,則此容量自動增大。
StringBuffer 上的主要操作是 append 和 insert 方法,可重載這些方法,以接受任意類型的數據。每個方法都能有效地將給定的數據轉換成字符串,然後將該字符串的字符追加或插入到字符串緩衝區中。append 方法始終將這些字符添加到緩衝區的末端;而 insert 方法則在指定的點添加字符。
java.lang.StringBuilder一個可變的字符序列是5.0新增的。此類提供一個與 StringBuffer 兼容的 API,但不保證同步, StringBuilder的速度比StringBuffer快。該類被設計用作 StringBuffer
的一個簡易替換,用在字符串緩衝區被單個線程使用的時候(這種情況很普遍)。兩者的方法基本相同。
如果要多次操作字符串,使用StringBuffer和StringBuilder會提高效率,但至少在數量級超過百萬時,StringBuilder的速度纔會體現出來。