【Java基礎】JAVA不可變類(immutable)機制與String的不可變性

 

 

1、不可變類簡介

不可變類:所謂的不可變類是指這個類的實例一旦建立完成後,就不能改變其成員變量值。如JDK內部自帶的不少不可變類:Interger、Long和String(8種基本數據類型的包裝類和String都是不可變類)等。
可變類:相對於不可變類,可變類建立實例後能夠改變其成員變量值,開發中建立的大部分類都屬於可變類。java

下面的理解可能會易懂一些:sql

 

{概念:不可變類的意思是建立該類的實例後,該實例的屬性是不可改變的。Java中的8個包裝類和String類都是不可變類。因此不可變類並非指該類是被final修飾的,而是指該類的屬性是被final修飾的。數據庫

自定義不可變類遵照以下原則:數組

一、使用private和final修飾符來修飾該類的屬性。緩存

二、提供帶參數的構造器,用於根據傳入的參數來初始化屬性。安全

三、僅爲該類屬性提供getter方法,不要提供setter方法。併發

四、若是有必要,重寫hashCode和equals方法,同時應保證兩個用equals方法判斷爲相等的對象,其hashCode也應相等。}ide

 

2、不可變類的優勢

說完可變類和不可變類的區別,咱們須要進一步瞭解爲何要有不可變類?這樣的特性對JAVA來講帶來怎樣的好處?測試

  1. 線程安全
    不可變對象是線程安全的,在線程之間能夠相互共享,不須要利用特殊機制來保證同步問題,由於對象的值沒法改變。能夠下降併發錯誤的可能性,由於不須要用一些鎖機制等保證內存一致性問題也減小了同步開銷。
  2. 易於構造、使用和測試
  3. ...

3、不可變類的設計方法

對於設計不可變類,我的總結出如下原則:this

1. 類添加final修飾符,保證類不被繼承
若是類能夠被繼承會破壞類的不可變性機制,只要繼承類覆蓋父類的方法而且繼承類能夠改變成員變量值,那麼一旦子類以父類的形式出現時,不能保證當前類是否可變。

2. 保證全部成員變量必須私有,而且加上final修飾
經過這種方式保證成員變量不可改變。但只作到這一步還不夠,由於若是是對象成員變量有可能再外部改變其值。因此第4點彌補這個不足。

3. 不提供改變成員變量的方法,包括setter
避免經過其餘接口改變成員變量的值,破壞不可變特性。

4.經過構造器初始化全部成員,進行深拷貝(deep copy)

若是構造器傳入的對象直接賦值給成員變量,仍是能夠經過對傳入對象的修改進而致使改變內部變量的值。例如:

public final class ImmutableDemo { private final int[] myArray; public ImmutableDemo(int[] array) { this.myArray = array; // wrong } }

這種方式不能保證不可變性,myArray和array指向同一塊內存地址,用戶能夠在ImmutableDemo以外經過修改array對象的值來改變myArray內部的值。
爲了保證內部的值不被修改,能夠採用深度copy來建立一個新內存保存傳入的值。正確作法:

public final class MyImmutableDemo { private final int[] myArray; public MyImmutableDemo(int[] array) { this.myArray = array.clone(); } }

5. 在getter方法中,不要直接返回對象自己,而是克隆對象,並返回對象的拷貝
這種作法也是防止對象外泄,防止經過getter得到內部可變成員對象後對成員變量直接操做,致使成員變量發生改變。

4、String對象的不可變性

string對象在內存建立後就不可改變,不可變對象的建立通常知足以上5個原則,咱們看看String代碼是如何實現的。

public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; /** The offset is the first index of the storage that is used. */ private final int offset; /** The count is the number of characters in the String. */ private final int count; /** Cache the hash code for the string */ private int hash; // Default to 0 .... public String(char value[]) { this.value = Arrays.copyOf(value, value.length); // deep copy操做 } ... public char[] toCharArray() { // Cannot use Arrays.copyOf because of class initialization order issues char result[] = new char[value.length]; System.arraycopy(value, 0, result, 0, value.length); return result; } ... }

如上代碼所示,能夠觀察到如下設計細節:

  1. String類被final修飾,不可繼承
  2. string內部全部成員都設置爲私有變量
  3. 不存在value的setter
  4. 並將value和offset設置爲final。
  5. 當傳入可變數組value[]時,進行copy而不是直接將value[]複製給內部變量.
  6. 獲取value時不是直接返回對象引用,而是返回對象的copy.

這都符合上面總結的不變類型的特性,也保證了String類型是不可變的類。

5、String對象的不可變性的優缺點

從上一節分析,String數據不可變類,那設置這樣的特性有什麼好處呢?我總結爲如下幾點:

1.字符串常量池的須要.
字符串常量池能夠將一些字符常量放在常量池中重複使用,避免每次都從新建立相同的對象、節省存儲空間。但若是字符串是可變的,此時相同內容的String 還指向常量池的同一個內存空間,當某個變量改變了該內存的值時,其餘遍歷的值也會發生改變。因此不符合常量池設計的初衷。

2. 線程安全考慮
同一個字符串實例能夠被多個線程共享。這樣便不用由於線程安全問題而使用同步。字符串本身即是線程安全的。

3. 類加載器要用到字符串,不可變性提供了安全性,以便正確的類被加載。譬如你想加載java.sql.Connection類,而這個值被改爲了myhacked.Connection,那麼會對你的數據庫形成不可知的破壞。

4. 支持hash映射和緩存。
由於字符串是不可變的,因此在它建立的時候hashcode就被緩存了,不須要從新計算。這就使得字符串很適合做爲Map中的鍵,字符串的處理速度要快過其它的鍵對象。這就是HashMap中的鍵每每都使用字符串。

缺點:

  1. 若是有對String對象值改變的需求,那麼會建立大量的String對象。

6、String對象的是否真的不可變

雖然String對象將value設置爲final,而且還經過各類機制保證其成員變量不可改變。可是仍是能夠經過反射機制的手段改變其值。例如:

    //建立字符串"Hello World", 並賦給引用s
    String s = "Hello World"; System.out.println("s = " + s); //Hello World //獲取String類中的value字段 Field valueFieldOfString = String.class.getDeclaredField("value"); //改變value屬性的訪問權限 valueFieldOfString.setAccessible(true); //獲取s對象上的value屬性的值 char[] value = (char[]) valueFieldOfString.get(s); //改變value所引用的數組中的第5個字符 value[5] = '_'; System.out.println("s = " + s); //Hello_World

打印結果爲:

s = Hello World
s = Hello_World

發現String的值已經發生了改變。也就是說,經過反射是能夠修改所謂的「不可變」對象的

總結

不可變類是實例建立後就不能夠改變成員遍歷的值。這種特性使得不可變類提供了線程安全的特性但同時也帶來了對象建立的開銷,每更改一個屬性都是從新建立一 個新的對象。JDK內部也提供了不少不可變類如Integer、Double、String等。String的不可變特性主要爲了知足常量池、線程安全、 類加載的需求。合理使用不可變類能夠帶來極大的好處。

具體實例

構造一個不可變類很是容易,下面舉一個簡單例子:

[java] view plain copy
 
  1. package public Address {  
  2.   String detail;  
  3.  Address() {  
  4. .detail = ;  
  5.  Address(String detail) {  
  6. .detail = detail;  
  7.  String getDetail() {  
  8.  detail;  
  9.   
  10.   hashCode() {  
  11.  detail.hashCode();  
  12.   
  13.   equals(Object obj) {  
  14.  (obj  Address) {  
  15.  (.getDetail().equals(address.getDetail())) {  
  16.  ;  
  17.  ;  
  18. }  


可是值得注意的是,該類的屬性雖然是被final修飾的,但若屬性是非String的其餘引用類型的話,那麼雖然該屬性的內容(所指對象的地址)不會改 變,但其指向的對象卻有可能會改變,這樣的類固然並不能成爲不可變類。好比下面的Person類中有一個Name類型的屬性:

[java] view plain copy
 
  1. package public Person {  
  2.   Name name;  
  3.  Person(Name name) {  
  4. ();  
  5. .name = name;  
  6.  Name getName() {  
  7.  name;  
  8.    main(String[] args) {  
  9.  Name();  
  10.  Person(n);  
  11.   
  12. );  
  13. }  


Name:

[java] view plain copy
 
  1. package public Name {  
  2.  String firstName;  
  3.  String lastName;  
  4.  Name() {  
  5. ();  
  6.  Name(String firstName, String lastName) {  
  7. ();  
  8. .firstName = firstName;  
  9. .lastName = lastName;  
  10.  String getFirstName() {  
  11.  firstName;  
  12.   setFirstName(String firstName) {  
  13. .firstName = firstName;  
  14.  String getLastName() {  
  15.  lastName;  
  16.   setLastName(String lastName) {  
  17. .lastName = lastName;  
  18. }  


運行上面程序能夠看到,Person對象的Name屬性的firstName屬性已經被改變,這就違背了不可變類設計的初衷。咱們能夠採起以下辦法來解決,修改Person類以下:

[java] view plain copy
 
  1. package public Person {  
  2.   Name name;  
  3.  Person(Name name) {  
  4. ();  
  5.   
  6.   
  7. .name =  Name(name.getFirstName(), name.getLastName());  
  8.  Name getName() {  
  9.   
  10.   
  11.   Name(name.getFirstName(), name.getLastName());  
  12.    main(String[] args) {  
  13.  Name();  
  14.  Person(n);  
  15.   
  16. );  
  17. }  


再次運行程序,發現Person對象的Name屬性的firstName屬性沒有改變了。

另外,因爲不可變類的實例的狀態不可改變,因此能夠很方便地被多個對象所共享,那麼若是程序要常用相同的不可變類實例,爲了減小系統開銷,通常要考慮使用緩存機制。下面使用數組做爲緩存池來構建一個能夠緩存實例的不可變類:

[java] view plain copy
 
  1. package public CacheImmutale {  
  2.   String name;  
  3.   CacheImmutale[] cache =  CacheImmutale[];  
  4.    pos = ;  
  5.  CacheImmutale(String name) {  
  6. ();  
  7. .name = name;  
  8.  String getName() {  
  9.  name;  
  10.   CacheImmutale valueOf(String name) {  
  11.   
  12.  ( i = ; i < pos; i++) {  
  13.   
  14.  (cache[i] !=  && cache[i].getName().equals(name)) {  
  15.  cache[i];  
  16.   
  17.  (pos == ) {  
  18.   
  19. ] =  CacheImmutale(name);  
  20. ;  
  21.  cache[];  
  22.  {  
  23.   
  24.  CacheImmutale(name);  
  25.  cache[pos - ];  
  26.   
  27.   hashCode() {  
  28.  name.hashCode();  
  29.   
  30.   equals(Object obj) {  
  31.  (obj  CacheImmutale) {  
  32.  (name.equals(ci.getName())) {  
  33.  ;  
  34.  ;  
  35.    main(String[] args) {  
  36. );  
  37. );  
  38.   
  39. }  

對於緩存的使用,應根據系統需求而定,簡單的說,若是某個對象使用的次數很少,重複使用的機率不大,就不必使用緩存,畢竟緩存的對象也會佔用系統內存。若是某個對象須要頻換地重複使用,這時就應該使用緩存了。

 另 外,上面的示例來源瘋狂JAVA講義一書,我的對上面那個Person類裏面的屬性是引用類型的解決辦法存有疑問,他那種辦法雖然保證的Person對象 的Name屬性所指對象的內容沒有改變,但Person對象返回的Name屬性已經不是同一個屬性了,它的地址已發生改變,賦值和返回都是經過new出來 的,我我的作了以下改進,以爲更合理:

[java] view plain copy
 
  1. package public Person {  
  2.   Name name;  
  3.  Person(Name name) {  
  4. ();  
  5.   
  6.   
  7. .name =  Name(name.getFirstName(), name.getLastName());  
  8.  Name getName() {  
  9.   
  10.  name;  
  11.    main(String[] args) {  
  12.  Name();  
  13.  Person(n);  
  14.   
  15. );  
  16. }  

從打印結果能夠看出p的name屬性的地址和所指內容都沒變。

相關文章
相關標籤/搜索