在所有編程語言領域,我想字符串應該是地球上最常用的表達手段了吧。
在java的世界裡,String是作為類出現的,核心的一個域就是一個char數組,內部就是通過維護一個不可變的char數組,來向外部輸出的。
這是jdk一段String類定義,首先類是final,表明類不可被繼承;核心域是private final的,final表明這個引用所指向的內存地址不會改變,但這還不足說明value[]是不可變的;因為引用所指向的內存的值有可能發生變化,但是jdk是不會讓這樣的事情發生的。private 保證這個域對外部來說是不可見的,這還不夠,對value還要進行 保護性拷貝 。
舉一個簡單的例子:
這是一個String的構造函數,參數是一個char數組引用,它並沒有把這個數組引用直接賦值給實例對象的value成員變量,而是通過一個Arrays.copyOf的方式拷貝一個數組再給到對象的成員變量。為什麼呢?假設它這裡是直接一個賦值,那String的不可變性就徹底被破壞了,因為如此一來,存在一個外部引用與實例對象value引用指向相同的內存地址,通過外部引用就可以改變這個char數組對象,最終導致的結果就是String不再不可變。幸好JDK中所有的對value的操作都是保護性拷貝操作,不管是被賦值,還是賦值給其它外部引用。
說了這麼多,為什麼JAVA要String保持一個不可變的狀態呢?原因其實很簡單,因為String太太太太常用了,地球上沒有能比這個更常用的對象了,設計成不可變的,是為了減少大量的同步鎖的開銷。但是要注意 並不是聲明成final的類一定是不可變的 。
根據effective java一書中提到,類不變需遵循五條規則:
1.不提供任何機會修改對象狀態的方法
2.保證類不被擴展
3.所有域都是final
4.所有域都是私有的
5.確保對於任何可變組件的互斥訪問
有興趣的同學可以參考 effective 第十五條,這裡就不展開講了。作了那麼久的鋪墊,接下來可以談談avoid getfield opcode了,按翻譯來說就是防止"調用訪問域的操作碼",這段tip來自一段注釋。
十分常用的replace方法,內部算法大概是這樣一個過程:先找到第一個oldChar的下標i,拷貝小標i之前的舊數組的內容到新的數組,新數組[i]='newChar',遍歷i之後的內容,若舊數組出現為oldChar則在新數組中替換為newChar,若沒有出現,則拷貝舊值到新數組。
起初我很奇怪,到底為什麼,一定要找到第一個出現oldChar的下標,為什麼不直接遍歷數組中每一個char 若為舊值,替換為新值。我從時間復雜度,空間復雜度去考慮這個算法,始終沒有得到結果。我還是太年輕啊,後來才發覺其實還是為了維護一個String的設計原則:"對於擁有相同的字符字面量的情況下,String的構造還是優先返回原字符串對象"。這麼做應該是為了解決堆內存吧。
那麼,這一句注釋到底是什麼意思呢?要理解這句話,需要對JVM有一定的了解。
JVM在運行中的數據區,分為五個部分:方法區,堆區,虛擬機棧,本地方法棧,程序計數器。
首先類相關的信息肯定是放在方法區的,堆中放一些實例對象,程序計數器始終指向下一條將要執行的指令,虛擬機棧和本地方法棧分別是用來於普通方法和本地方法的。
著重說一下虛擬機棧,它是線程私有的,描述的是java方法執行的內存模型:每個方法在執行的同時創建一個棧幀,用於存放局部變量表,操作數棧,方法入口,動態鏈接等。
局部變量表用來存放一些基本數據類,和引用。操作數棧的話,是用來作運算用的,打個比方
int a=1; int b =2; int c =a+b;
JVM會先把a,b的值壓入到操作數棧保存起來,等到程序計數器執行加法指令的時候,再把a,b從棧中pop出來。重點就在於a,b值是從哪裡壓入到棧中的,如果沒有那麼接下來要遍歷的就是value數組了,value毫無疑問是堆中的數據,也就是每一次遍歷會經歷,數據由堆中取出,進入棧幀,再由棧幀壓入到操作數棧,最後pop出來進行運算。
如果執行了上述代碼,情況就大不相同了,val就是一個局部變量,它會被存放在局部變量表中,接來下的運算,就是局部變量表到操作數棧了,屬於一個棧幀內的數據轉移。JVM為了避免頻繁進行堆棧數據轉移,將值復制到本地變量一次,以避免在接下來的幾行中循環的每一次迭代,從堆中多次取下的字段值。
除了我提到的這些,String源碼中還有許多值得學習的算法,設計方式,代碼優化,比如,還有常量池的實現。
最後我想求教一個問題啊,在JDK7點版本中,String引入了一段靜態代碼塊,
我甚是不解啊,不知道有沒有大神幫我解讀一下這段代碼的含義?