1. 程式人生 > 程式設計 >通過例項解析java String不可變性

通過例項解析java String不可變性

一、原理

  1、不變模式(不可變物件)

  在並行軟體開發過程中,同步操作似乎是必不可少的。當多執行緒對同一個物件進行讀寫操作時,為了保證物件資料的一致性和正確性,有必要對物件進行同步。而同步操作對系統性能是相當的損耗。為了能儘可能的去除這些同步操作,提高並行程式效能,可以使用一種不可改變的物件,依靠物件的不變性,可以確保其在沒有同步操作的多執行緒環境中依然始終保持內部狀態的一致性和正確性。這就是不變模式。

  不變模式天生就是多執行緒友好的,它的核心思想是,一個物件一旦被建立,則它的內部狀態將永遠不會發生改變。所以,沒有一個執行緒可以修改其內部狀態和資料,同時其內部狀態也絕不會自行發生改變。基於這些特性,對不變物件的多執行緒操作不需要進行同步控制。

  同時還需要注意,不變模式和只讀屬性是有一定的區別的,不變模式是比讀屬性具有更強的一致性和不變性。對只讀屬性的物件而言,物件本身不能被其他執行緒修改,但是物件身狀態卻可能自行修改比如,一個物件的存活時間(物件建立時間和當前時間的時間差)是隻讀的,因為任何個第三方執行緒都不能修改這個屬性,但是這是一個可變的屬性,因為隨著時間的推移,存活時司時刻都在發生變化。而不變模式則要求,無論出於什麼原因,物件自建立後,其內部狀態和資料保持絕對的穩定。

  2、怎麼實現不可變物件

  在Java語言中,不變模式的實現很簡單。為確保物件被建立後,不發生任何改變,並保證不變模式正常工作,只需要注意以下4點:

  • 去除 setter方法以及所有修改自身屬性的方法。
  • 將所有屬性設定為私有,並用final標記,確保其不可修改
  • 確保沒有子類可以過載修改它的行為。
  • 有一個可以建立完整物件的建構函式。

是不是和final的功能很吻合。我們複習一下java中final的作用。

  • final修飾類,表示該類不能被繼承,俗稱斷子絕孫類,該類的所有方法自動地成為final方法
  • final修飾方法,表示子類不可重寫該方法
  • final修飾基本資料型別變數,表示該變數為常量,值不能再修改
  • final修飾引用型別變數,表示該引用在構造物件之後不能指向其他的物件,但該引用指向的物件的狀態可以改變

  這裡需要說明的是:當使用final修飾基本型別變數時,不能對基本型別變數重新賦值,因此基本型別變數不能被改變。但對於引用型別變數而言,它儲存的僅僅是一個引用,final只保證這個引用變數所引用的地址不會改變,即一直引用同一個物件,但這個物件完全可以發生改變。例如某個指向陣列的final引用,它必須從此至終指向初始化時指向的陣列,但是這個陣列的內容完全可以改變。

二、String原始碼分析

以下是jdk1.8中String類的部分原始碼。 

public final class String implements java.io.Serializable,Comparable<String>,CharSequence {  
    /** The value is used for character storage. */
  private final char value[];  /** Cache the hash code for the string */
    private int hash; // Default to 0
 
  /** use serialVersionUID from JDK 1.0.2 for interoperability */
  private static final long serialVersionUID = -6849794470754667710L;  /**
     ...}

  首先可以看到,String類使用了final修飾符,表明String類是不可繼承的。然後,我們主要關注String類的成員變數value,value是char[]型別,因此String物件實際上是用這個字元陣列進行封裝的。再看value的修飾符,使用了private,也沒有提供setter方法,所以在String類的外部不能修改value,同時value也使用了final進行修飾,那麼在String類的內部也不能修改value,也就是說value一旦賦予初始值之後,value指向的地址就不能再改變了。但是上面final修飾引用型別變數的內容提到,這隻能保證value不能指向其他的物件,但value指向的物件的狀態是可以改變的。通過檢視String類原始碼可以發現,String類不可變,關鍵是因為SUN公司的工程師,在後面所有String的方法裡都很小心的沒有去動字元數組裡的元素。所以String類不可變的關鍵都在底層的實現,而不僅僅是一個final。

三、修改String使其“可變”

  雖然value是final修飾的,只是說明value不能再重新指向其他的引用。但是value指向的陣列可以改變,一般情況下我們是沒有辦法訪問到這個value指向的陣列的元素。But,反射,對,反射可以,牛逼吧。可以反射出String物件中的value屬性, 進而改變通過獲得的value引用改變陣列的結構。

public static void main(String[] args) throws Exception {
  String str = "Hello World";
  System.out.println("修改前的str:" + str);
  System.out.println("修改前的str的記憶體地址" + System.identityHashCode(str));
  // 獲取String類中的value欄位
  Field valueField = String.class.getDeclaredField("value");
  // 改變value屬性的訪問許可權
  valueField.setAccessible(true);
  // 獲取str物件上value屬性的值
  char[] value = (char[]) valueField.get(str);
  // 改變value所引用的陣列中的字元
  value[3] = '?';
  System.out.println("修改後的str:" + str);
  System.out.println("修改前的str的記憶體地址" + System.identityHashCode(str));
}
// 執行結果
// 可以看到str的字串序列已經被改變了,但是str的記憶體地址還是沒有改變。
修改前的str:Hello World
修改前的str的記憶體地址1922154895
修改後的str:Hel?o World
修改前的str的記憶體地址1922154895

四、String設計成不可變性的原因

  在Java中,將String設計成不可變的是綜合考慮到記憶體、同步、資料結構及安全等各種因素的結果,下文將為各種因素做一個小結。

  1、執行時常量池的需要

  比如執行 String s = "abc";執行上述程式碼時,JVM首先在執行時常量池中檢視是否存在String物件“abc”,如果已存在該物件,則不用建立新的String物件“abc”,而是將引用s直接指向執行時常量池中已存在的String物件“abc”;如果不存在該物件,則先在執行時常量池中建立一個新的String物件“abc”,然後將引用s指向執行時常量池中建立的新String物件。
這樣在執行時常量池中只會建立一個String物件"abc",這樣就節省了記憶體空間。

    2、同步

  因為String物件是不可變的,所以是多執行緒安全的,同一個String例項可以被多個執行緒共享。這樣就不用因為執行緒安全問題而使用同步。

  3、允許String物件快取hashcode

  檢視上文JDK1.8中String類原始碼,可以發現其中有一個欄位hash,String類的不可變性保證了hashcode的唯一性,所以可以用hash欄位對String物件的hashcode進行快取,就不需要每次重新計算hashcode。所以Java中String物件經常被用來作為HashMap等容器的鍵。

  4、安全性

  如果String物件是可變的,那麼會引起很嚴重的安全問題。比如,資料庫的使用者名稱、密碼都是以字串的形式傳入來獲得資料庫的連線,或者在socket程式設計中,主機名和埠都是以字串的形式傳入。因為String物件是不可變的,所以它的值是不可改變的,否則黑客們可以鑽到空子,改變String引用指向的物件的值,造成安全漏洞。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。