天天看點

java基礎——String的不可變性String & Immutable

String & Immutable

1.概念

String 類被 final 修飾,類似的被 final 修飾的類還有 Integer、Double等等包裝類。我們國小二年級時候都學過,被 final 修飾的類是不可被繼承的,在 Java 中,被 final 修飾的類被稱為 “不可變類”。好奇的同學可能會問了,1.這些類為什麼需要被 final 修飾呢?2.為什麼不能改變呢,不可變有什麼好處?我們以 String 為例,進行探讨。

2.不可變 (immutable)

如果你閱讀了 String 方法的源碼,你知道 String 的核心是它的成員變量 value

String 的所有方法都圍繞着這個 char 類型的數組。

我們國小二年級時候學過,被 final 修飾的成員變量有幾個特點

  1. 如果修飾的是執行個體成員變量,則需要在構造函數執行完畢前完成對成員變量的初始化。如果是靜态成員變量,則需要在類初始化完畢前完成初始化。
  2. 變量的值不能變。如果修飾的是引用類型,值代表的就是對象中存儲的記憶體位址;如果是基本資料類型,則變量的值不能改變。

value 是被 final 修飾的、執行個體的、引用類型的成員變量。就意味着,需要在建立 String 對象時對數組進行初始化。且完成初始化後,對象的記憶體位址不能被改變。

public class StringTest2 {
    private final char[] value;
    
    public StringTest2(){
        value = new char[]{1,2,3};
    }
    
    public void update(){
        value[2] = 100;
    }
}
           

雖然 value 的記憶體位址無法改變,但根據上面例子看來數組中的值是可以改變的。但設計 String 的工程師 1.将 value 數組私有化(private),2.沒有對外提供任何修改 value 數組元素的方法( 沒有 setter 方法),3.在源碼中也沒有任一地方對 value 數組的值進行了修改。

例如 String.replace 方法,實際不修改 value 數組,而是将 value 數組複制一份,在新的數組上修改後,将新數組傳回。測試代碼如下:

String str = "test";
String replace = str.replace('t', 'f');
System.out.println(replace);
System.out.println(str);

//output
fesf
test
           

加上文章開頭說的,設計者用 final 修飾 String 類,使 String 類不能被繼承,不能擁有子類。 也就不能通過繼承的方式實作多态,改變 String 中方法的實作。我們通過下面的例子了解

public class Son{
    @Override
    public String toString() {
        return "哈哈哈";
    }
}

public void sout(String str){
        System.out.println(str);
}
           

我們假設 Son 類是 String 的子類,當調用下方 sout 方法時,需要傳入 String 類型的對象。了解多态的同學知道,多态就是父類的引用指向子類的對象。我們可以利用多态,向 sout 方法中傳入 Son 的執行個體,那麼結果就是 “哈哈哈” 而非輸出 String 對象的值了。

可見設計 String 的工程師是多麼的用心良苦,為了讓我們知道,String 這個類應該是不可變的(immutable)。

為了避免 String 中的方法被擴充 / 修改,為了保證 String 的不可變性,是以将 String 用 final 修飾以表明 String 不可變。這也就回答了文章開頭的第一個問題。下面我們來解答第二個問題:為什麼把 String 設計成不可變,不可變有什麼好處。

3.為什麼把 String 設計成不可變

  • 安全

    我們可以通過以下例子去了解不可變的安全性。

    1. 不可變類的執行個體作為 HashMap 的 key 值
    class Student{
        private String name;
        private Integer id;
    
        public Student(String name, Integer id) {
            this.name = name;
            this.id = id;
        }
    
        //getter.. 篇幅原因,此處省略
        //setter.. 篇幅原因,此處省略
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Student)) return false;
            Student student = (Student) o;
            return Objects.equals(getName(), student.getName()) &&
                    Objects.equals(getId(), student.getId());
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(getName(), getId());
        }
    }
    
    public static void main(String[] args) {
            HashMap<String,Integer> test1 = new HashMap<>();
            HashMap<Student,Integer> test2 = new HashMap<>();
    
            test1.put("1",1);
            test1.put("2",1);
    
            Student student1 = new Student("thd",1);
            Student student2 = new Student("djw",2);
            test2.put(student1,1);
            test2.put(student2,2);
            student2.setName("thd");
            student2.setId(1);
    
            System.out.println(student1.hashCode());
            System.out.println(student2.hashCode());
            System.out.println(student1.equals(student2));
            System.out.println(test2.get(student2));
    }
    
    //output
    3559762
    3559762
    true
    1
               

    當 HashMap 中的 key 值為可變類時,如果按照以上的操作,将 student2 的資訊改變為與 student1 相同後。通過 hashMap.get 方法擷取 value 值,導緻資訊不比對錯誤。(是以在開發中經常使用不可變字元串作為 HashMap 的 key 值)。相反的,不可變類的執行個體存入 HashMap 中,不會再被外部通過任何方式修改,也就保證了 key 的唯一性。

    2.線程安全:因為不可變類的資訊是無法被外部修改的,是以在多線程中不存在對不可變類進行 寫 操作,隻有讀操作,是以不存線上程安全問題。

    3.在網絡連接配接和資料庫連接配接時,字元串常常作為參數。例如,網絡連接配接位址 URL、檔案路徑 Path。(url 以字元串形式傳入後端後,後端可能會對 url 進行字元串操作,string 的不可變性確定了 url 不會被改變)

  • 效率

    1.基于 String 的不可變性,Java 提出了 字元串常量池。實際開發中我們經常需要操作字元串,如果沒有字元串常量池,系統中會存在大量的重複的字元串對象,浪費了很多記憶體空間。有了字元串常量池,可以提高效率并且減少記憶體配置設定,友善重複使用。如果 String 是可變的,那麼也就沒有 字元串常量 的概念,字元串常量池也就無法提出。我們通過下面的例子了解這一點:

    String str = "123";
    str = "456";
    String str1 = "123";
               
    如果 String 是可變的,那麼如下圖:
    java基礎——String的不可變性String &amp; Immutable

    str 是棧中指向堆中對象的引用,如果 String 是可變的,那麼 str 指向的 “123” 對象的值變為 “456”,前後是同一個對象,位址不變。這時 str1 需要指向一個值為 “123” 的對象,在堆中找不到,隻能再次建立一個對象。

    但如果 String 是不可變的,那麼如下圖:

    java基礎——String的不可變性String &amp; Immutable

    不再需要建立新的 String 對象,在程式中出現過的字元串對象都儲存在字元串常量池中統一管理。

    2.String 對象中私有化封裝了一個 hash 用于緩存哈希值,String對象的哈希碼被頻繁地使用, 比如在 hashMap 等容器中。String 不可變性保證了 hashCode 的唯一性,是以可以放心地進行緩存.這也是一種性能優化手段,意味着不必每次都去計算新的哈希碼。

    /** Cache the hash code for the string */
        private int hash; // Default to 0
               
    3.每次對字元串進行增删改查之前其實 jvm 需要檢查一下這個String對象的安全性,就是通過 hashcode,當設計成不可變對象時候,就保證了每次增删改查的hashcode的唯一性,也就可以放心的操作。

4.總結

1.如何讓一個類不可變

  • 使用 final 修飾類,使得類不能被繼承
  • 将成員變量私有化
  • 使用 final 修飾成員變量
  • 在構造函數中完成對成員變量的初始化
  • 不對外提供修改成員變量的方法
  • 當類中含有可變的屬性時,為了防止屬性被修改,getter 方法不應該傳回該對象的引用而應該傳回一個包含與對象相同内容的新對象
final class Student1{
    //immutable field
    private final String name;
    //mutable field
    private final Date date;

    public Student1(String name,Date date) {
        this.name = name;
        this.date = date;
    }

    public String getName(){
        return name;
    }

    public Date getDate(){
        return new Date(this.date.getTime());
    }
}
           

2.不可變有什麼好處

  • 安全
    1. 可以作為 HashMap 的 key 值
    2. 線程安全
  • 高效
    1. 可以緩存 hashCode,每次使用時不需要再次計算
    2. 可以實作緩存池