什麼是不可變對象?
《Effective Java》這本書對於不可變對象做了如下說明:
不可變對象(Immutable Object):對象一旦被創建後,對象所有的狀態及屬性在其生命週期內不會發生任何變化。
以上比較直白的表達了不可變對象的含義,我們常見的包裝類都是不可變類型,如String、Integer、Long等。
以下舉例說明
從輸出結果可以看出,在對str進行了字符串替換之後,即將'x'替換為'X',str1指向的字符串對象是沒有發生變化的。這也佐證了以上String為不可變類型這一說法。
String的不可變性如何保證?
String對象在堆內存(jdk1.7後字符串常量池也被劃分到了堆內存中)創建後就不可改變,我們通過源碼來看下Java語言的設計者是如何保證String類型的不可變性的。
可以看到如下設計細節:
- String類被final關鍵字修飾,說明String不可被繼承。
- String內部所有成員屬性都設置為私有的。
- 不存在成員屬性的setter方法。
- 並將value、offset和count設置為final。
- 當傳入可變數組value[]時,進行數組的深拷貝而不是直接將value[]賦值給value。
- 獲取value時不是直接返回對象的引用,而是返回對象的深拷貝副本。
以上六個方面共同保證了String類型不可變的特性。
String的不可變性有何優點?
提高運行效率,降低系統開銷。主要體現在字符串常量池的設計上,字符串常量池可以將一些字符常量放在常量池中重複使用,避免每次都重新創建相同的對象、節省存儲空間。但如果字符串是可變的,此時相同內容的String還指向常量池的同一個內存空間,當某個變量改變了該內存的值時,其他變量的值也會發生改變。所以不符合常量池設計的初衷。
線程安全考慮。同一個字符串實例可以被多個線程共享。這樣便不用因為線程安全問題而使用同步。字符串自己便是線程安全的(不可變、無狀態)。
Java語言基礎設施的安全性。類加載器要用到字符串,根據類的全限定名來加載指定類到內存。全限定名就是用字符串表示的,String的不可變性提供了安全性,以便正確的類被加載。設想下如果你想加載java.sql.Connection類,而這個值被改成了hacked.Connection,那麼類的加載豈不是安全亂了套了。
容器使用過程的安全和效率考慮。 因為字符串是不可變的,所以在它創建的時候hashcode就被緩存了,不需要重新計算。這就使得字符串很適合作為Map中的鍵,字符串的處理速度要快過其它的鍵對象。這就是HashMap中的鍵往往都使用字符串。
String的不可變性有何缺點?
可能創建大量的String對象。如果有對String對象值頻繁改變的需求,那麼會創建大量的String對象,此時會佔用較多的堆內存空間,大家在開發過程中可以使用StringBuffer和StringBuild來代替String來實現某些場景的功能,以避免創建較多的String對象。
String真的"完全不可改變"嗎?
String作為不可變類型雖然具備不可變性,但是也並不是完全不可變的,通過反射就可以打破這種不可變性。舉例如下:
大家看到這裡可能就犯嘀咕了,既然String對象能夠改變,為什麼還叫不可變類型呢?這裡大家可能對不可變類型有些誤解,從其含義可以看出來String的不可變性是針對Java語言開發者在String的使用上的。目的是來幫助大家更簡單地去編寫代碼,減少程序編寫過程中出錯的概率,這是不可變對象的初衷。而反射是Java語言另一高級特性,而非針對String的。所以就String自身而言,其不可變性的定位和表述是完全沒有問題的。另外如果真要靠通過反射來改變一個對象的狀態,此時編寫代碼的人也應該會意識到此類在設計的時候就不希望其狀態被更改,從而引起編寫代碼的人的注意。
如果你覺得本文對你有些許幫助,歡迎關注、收藏和轉發