1. 程式人生 > >剖析java中的String(本文章是對網上資料的蒐集)

剖析java中的String(本文章是對網上資料的蒐集)

     首先把問題擺出來,先看以下程式碼(我們姑且稱為“程式碼一”):

String a = "ab";
String b = "a" + "b"; 
System.out.println((a == b));

估計對java有一定了解的都會回答是true,答案是對的,但是解釋呢?也許你會說:String a = "ab";建立了新的物件"ab"; 再執行String b = "a" +"b";結果b="ab",這裡沒有建立新的物件,而是從JVM字串常量池中獲取之前已經存在的"ab"物件。因此a,b具有對同一個string物件 的引用,兩個引用相等,結果true。很遺憾,這個答案,是不夠準確的。或者說,根本沒有執行時計算b = "a" + "b";這個操作.實際上執行時只有Stringb = "ab";這個觀點的解釋適合以下情況(我們姑且稱為“程式碼二
”):
String a = "ab"; 
String b = "ab"; 
System.out.println((a == b));

如果String b = "a" +"b";是在執行期執行,則以上的解釋是行不通的。執行期的兩個string相加,會產生新的物件的。(本文後面對此有解釋)。

        其實正確的解釋應該是String b = "a" + "b";編譯器將這個"a" +"b"作為常量表達式,在編譯時進行優化,直接取結果"ab",這樣這個問題退化為“程式碼二”的問題了,然後再使用第一個最開始給的不是很準確的解釋就說的通了。這裡有一個疑問就是String不是基本型別,像int secondsOfDay = 24 * 60 * 60;這樣的表示式是常量表達式,編譯器在編譯時直接計算容易理解,而"a" +"b" 這樣的表示式,string是物件不是基本型別,編譯器會把它當成常量表達式來優化嗎?下面用事實來證明:首先編譯這個類:

public class Test { 
	private String a = "aa"; 
	
}

複製class檔案,備用,然後修改為:
public class Test { 
	private String a = "a" + "a"; 
	
}

再次編譯,用ue之類的文字編輯器開啟,察看二進位制內容,可以發現,兩個class檔案完全一致,連一個位元組都不差。ok,真相大白了.根本不存在執行期的處理String b = "a" +"b";這樣的程式碼的問題,編譯時就直接優化掉了。

      下面進一步探討,什麼樣的string + 表示式會被編譯器當成常量表達式?String b = "a" + "b";這個String + String被證實是ok的,那麼string + 基本型別呢?

String a = "a1";
String b = "a" + 1; 
System.out.println((a == b)); //result = true 
String a = "atrue"; 
String b = "a" + true; 
System.out.println((a == b)); //result = true 
String a = "a3.4"; 
String b = "a" + 3.4; 
System.out.println((a == b)); //result = true

可見編譯器對string + 基本型別是當成常量表達式直接求值來優化的。再注意看這裡的string都是"**"這樣的,我們換成變數來試試:

String a = "ab";
String bb = "b"; 
String b = "a" + bb;
System.out.println((a == b)); //result = false

這個好理解,"a" + bb中的bb是變數,不能進行優化。這裡很很好的解釋了為什麼3的觀點不正確,如果String+String的操作是在執行時進行的,則會產生新的物件,而不是直接從jvm的string池中獲取。再修改一下,把bb作為常量變數:

String a = "ab";
final String bb = "b";
String b = "a" + bb; 
System.out.println((a == b)); //result = true

竟然又是true,編譯器的優化好厲害啊,考慮下面這種情況:
String a = "ab";
final String bb = getBB(); 
String b = "a" + bb; 
System.out.println((a == b)); //result = false
private static String getBB() { return "b"; }

看來java(包括編譯器和jvm)對string的優化,真的是到了極點了,string這個所謂的"物件",完全不可以看成一般的物件,java對string的處理近乎於基本型別,最大限度的優化了幾乎能優化的地方。另外感嘆一下,string的+號處理,算是java語言裡面唯一的一個"運算子過載"。

   去華為面試的時候, 第一筆試題就讓我費神去想了, 回來在機子上執行結果, 發現自己當時答錯了, 於是就狠下心來花了點時間研究這個:

String a = "a" + "b";
String b = "a" + "b";
System.out.println(a == b);
答案是nullabc!

很早的時候我就知道String拼接中間會產生StringBuilder物件(JDK1.5之前產生StringBuffer),但是當時也沒有去深究內部, 導致在華為筆試此題就錯了!

執行時, 兩個字串str1, str2的拼接首先會呼叫 String.valueOf(obj),這個Obj為str1,而String.valueOf(Obj)中的實現是return obj == null ? "null" : obj.toString(), 然後產生StringBuilder, 呼叫的StringBuilder(str1)構造方法, 把StringBuilder初始化,長度為str1.length()+16,並且呼叫append(str1)! 接下來呼叫StringBuilder.append(str2), 把第二個字串拼接進去, 然後呼叫StringBuilder.toString返回結果!

所以那道題答案的由來就是StringBuilder.append("null").append("abc").toString();

大家看了我以上的分析以後, 再碰到諸如此類的面試題應該不會再出錯了!

那麼瞭解String拼接有什麼用呢?

在做多執行緒的時候, 往往會用到一個同步監視器物件去同步一個程式碼塊中的程式碼synchronized(Obj),   對同一個物件才會互斥,不是同一個物件就不會互斥!

這裡有個機試題,

現有程式同時啟動了4個執行緒去呼叫TestDo.doSome(key, value)方法,由於TestDo.doSome(key, value)方法內的程式碼是先暫停1秒,然後再輸出以秒為單位的當前時間值,所以,會打印出4個相同的時間值,如下所示:
  4:4:1258199615
  1:1:1258199615
  3:3:1258199615
  1:2:1258199615
        請修改程式碼,如果有幾個執行緒呼叫TestDo.doSome(key, value)方法時,傳遞進去的key相等(equals比較為true),則這幾個執行緒應互斥排隊輸出結果,即當有兩個執行緒的key都是"1"時,它們中的一個要比另外其他執行緒晚1秒輸出結果,如下所示:
  4:4:1258199615
  1:1:1258199615
  3:3:1258199615
  1:2:1258199616
   總之,當每個執行緒中指定的key相等時,這些相等key的執行緒應每隔一秒依次輸出時間值(要用互斥),如果key不同,則並行執行(相互之間不互斥)。原始程式碼如下:

package syn;  
  
//不能改動此Test類      
public class Test extends Thread{  
      
    private TestDo testDo;  
    private String key;  
    private String value;  
      
    public Test(String key,String key2,String value){  
        this.testDo = TestDo.getInstance();  
        /*常量"1"和"1"是同一個物件,下面這行程式碼就是要用"1"+""的方式產生新的物件, 
        以實現內容沒有改變,仍然相等(都還為"1"),但物件卻不再是同一個的效果*/  
        this.key = key+key2;   
        this.value = value;  
    }  
  
  
    public static void main(String[] args) throws InterruptedException{  
        Test a = new Test("1","","1");  
        Test b = new Test("1","","2");  
        Test c = new Test("3","","3");  
        Test d = new Test("4","","4");  
        System.out.println("begin:"+(System.currentTimeMillis()/1000));  
        a.start();  
        b.start();  
        c.start();  
        d.start();  
    }  
      
    public void run(){  
        testDo.doSome(key, value);  
    }  
}  
  
class TestDo {  
  
    private TestDo() {}  
    private static TestDo _instance = new TestDo();   
    public static TestDo getInstance() {  
        return _instance;  
    }  
  
    public void doSome(Object key, String value) {  
  
        // 以大括號內的是需要區域性同步的程式碼,不能改動!  
        {  
            try {  
                Thread.sleep(1000);  
                System.out.println(key+":"+value + ":"  
                        + (System.currentTimeMillis() / 1000));  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
  
}  

此題解題的思路有很多種,不可或缺的步驟就是在doSome方法內部用synchronized(o)把那個寫了註釋的程式碼塊同步, 有些人肯定會說:

我直接synchronized(key),不就完了麼.?  這類人肯定是新手級別的了!

上面說了,synchronized(Obj),   對同一個物件才會互斥,不是同一個物件就不會互斥! 大家請看下Test類中的構造方法裡面對key做了什麼處理?

this.key = key + key2;

關於字串的拼接,  如果是兩個常量的拼接, 那麼你無論拼接多少下都是同一個物件,  這個是編譯時 編譯器自動去優化的(想知道具體原理的自己去網上搜下).

String a = "a" + "b";  
String b = "a" + "b";  
System.out.println(a == b);  

這段程式碼輸出true沒有問題

但是一旦涉及到變量了, 我在上面標紅加粗的執行時,    此時拼接字串就會產生StringBuilder,  然而拼接完返回的字串是怎麼返回的呢?

在StringBuilder.toString()中的實現是new String(char value[], int offset, int count), 既然是建立String返回的, 那麼呼叫一次toString,就是一個不同的物件

String a = "a";  
String b = "b";  
String s1 = a + b;  
String s2 = a + b;  
System.out.println(s1 == s2); 

這個輸出就是false!

所以在那道機試題中, 就不能直接用synchronized(key)去同步了,  如果你完完全全很耐心的看完本文, 那麼應該知道如何用synchronized(key)同步那段程式碼了!

不錯, 就是修改Test構造方法中的 this.key = key + key2;為this.key = key;

因為字串不涉及到拼接的時候, 只要不new, 多少都是指向同一個物件!

當然這道多執行緒的題你也可以把那個key丟到集合裡面去,用集合去的contains(obj)去判斷,如果集合中存在, 就取集合中的, 否則往集合中新增,但是記住一定要使用併發包下面的集合, 否則可能會丟擲ConcurrentModificationException

所以在那道機試題中, 就不能直接用synchronized(key)去同步了,  如果你完完全全很耐心的看完本文, 那麼應該知道如何用synchronized(key)同步那段程式碼了!

不錯, 就是修改Test構造方法中的 this.key = key + key2;為this.key = key;

因為字串不涉及到拼接的時候, 只要不new, 多少都是指向同一個物件!

當然這道多執行緒的題你也可以把那個key丟到集合裡面去,用集合去的contains(obj)去判斷,如果集合中存在, 就取集合中的, 否則往集合中新增,但是記住一定要使用併發包下面的集合, 否則可能會丟擲ConcurrentModificationException.