剖析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.