(Java)關於String的面試問題

身邊有些做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要快得多。

但在某些情況下:

Java代碼
  1. String S1 = “Who” + “ is” + “ faster?”;   
  2. StringBuffer Stb = new StringBuilder(“Who”).append(“ is”).append(“ faster?”);  
Java代碼
  1. String S1 = “Who” + “ is” + “ faster?”;   
  2. StringBuffer Stb = new StringBuilder(“Who”).append(“ is”).append(“ faster?”);  

S1的素對會比Stb快得多, 是因爲JVM把String對象的拼接解釋成了StringBuffer對象的拼接,其實在JVM就是:

Java代碼
  1. String S1="Who   is faster?";  
Java代碼
  1. String S1="Who   is faster?";  

不過如果,字符串是來自其他對象,如:

Java代碼
  1. String s1="Who";   
  2. String s2=" is";   
  3. String s3=" faster?";   
  4. String   st=s1+s2+s3;  
Java代碼
  1. String s1="Who";   
  2. String s2=" is";   
  3. String s3=" faster?";   
  4. String   st=s1+s2+s3;  

這個時候,String的速度就比不上StringBuffer了。

StringBuffer和StringBuilder

在操作字符串對象,StringBuiler是最快的,StringBuffer次之,String最慢。

Java代碼
  1. public final class StringBuffer   
  2.    extends AbstractStringBuilder   
  3.    implements java.io.Serializable, CharSequence   
  4.   
  5. ublic final class StringBuilder   
  6.    extends AbstractStringBuilder   
  7.    implements java.io.Serializable, CharSequence  
Java代碼
  1. public final class StringBuffer   
  2.    extends AbstractStringBuilder   
  3.    implements java.io.Serializable, CharSequence   
  4.   
  5. ublic final class StringBuilder   
  6.    extends AbstractStringBuilder   
  7.    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的速度纔會體現出來。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章