這次要說不明白 immutable 類,我就怎麼地

這次要說不明白 immutable 類,我就怎麼地

作者 | 沉默王二

頭圖 | CSDN 下載自視覺中國

二哥,你能給我說說為什麼 String 是 immutable 類嗎?我想研究它,想知道為什麼它就不可變了,這種強烈的願望就像想研究浩瀚的星空一樣。但無奈自身功力有限,始終覺得霧裡看花終隔一層。二哥你的文章總是充滿趣味性,我想一定能夠說明白,我也一定能夠看明白,能在接下來寫一寫嗎?

收到讀者小 R 的私信後,我就總感覺自己有一種義不容辭的責任,非要把 immutable 類說明白,否則我就怎麼地——你說了算!

这次要说不明白 immutable 类,我就怎么地

什麼是不可變類

一個類的對象在通過構造方法創建後如果狀態不會再被改變,那麼它就是一個不可變(immutable)類。它的所有成員變量的賦值僅在構造方法中完成,不會提供任何 setter 方法供外部類去修改。

還記得《神鵰俠侶》中小龍女的古墓嗎?隨著那一聲巨響,僅有的通道就被無情地關閉了。別較真那個密道,我這麼說只是為了打開你的想象力,讓你對不可變類有一個更直觀的印象。

自從有了多線程,生產力就被無限地放大了,所有的程序員都愛它,因為強大的硬件能力被充分地利用了。但與此同時,所有的程序員都對它心生忌憚,因為一不小心,多線程就會把對象的狀態變得混亂不堪。

為了保護狀態的原子性、可見性、有序性,我們程序員可以說是竭盡所能。其中,synchronized(同步)關鍵字是最簡單最入門的一種解決方案。

假如說類是不可變的,那麼對象的狀態就也是不可變的。這樣的話,每次修改對象的狀態,就會產生一個新的對象供不同的線程使用,我們程序員就不必再擔心併發問題了。

这次要说不明白 immutable 类,我就怎么地

常見的不可變類

提到不可變類,幾乎所有的程序員第一個想到的,就是 String 類。那為什麼 String 類要被設計成不可變的呢?

1)常量池的需要

字符串常量池是 Java 堆內存中一個特殊的存儲區域,當創建一個 String 對象時,假如此字符串在常量池中不存在,那麼就創建一個;假如已經存,就不會再創建了,而是直接引用已經存在的對象。這樣做能夠減少 JVM 的內存開銷,提高效率。

2)hashCode 的需要

因為字符串是不可變的,所以在它創建的時候,其 hashCode 就被緩存了,因此非常適合作為哈希值(比如說作為 HashMap 的鍵),多次調用只返回同一個值,來提高效率。

3)線程安全

就像之前說的那樣,如果對象的狀態是可變的,那麼在多線程環境下,就很容易造成不可預期的結果。而 String 是不可變的,就可以在多個線程之間共享,不需要同步處理。

因此,當我們調用 String 類的任何方法(比如說 trim()、substring()、toLowerCase())時,總會返回一個新的對象,而不影響之前的值。


<code>1String cmower = "沉默王二,一枚有趣的程序員";
2cmower.substring(0,4);
3System.out.println(cmower);// 沉默王二,一枚有趣的程序員
/<code>

雖然調用 substring 方法對 cmower 進行了截取,但 cmower 的值沒有改變。

除了 String 類,包裝器類 Integer、Long 等也是不可變類。

这次要说不明白 immutable 类,我就怎么地

自定義不可變類

看懂一個不可變類也許容易,但要創建一個自定義的不可變類恐怕就有點難了。但知難而進是我們作為一名優秀的程序員不可或缺的品質,正因為不容易,我們才能真正地掌握它。

接下來,就請和我一起,來自定義一個不可變類吧。一個不可變誒,必須要滿足以下 4 個條件:

1)確保類是 final 的,不允許被其他類繼承。

2)確保所有的成員變量(字段)是 final 的,這樣的話,它們就只能在構造方法中初始化值,並且不會在隨後被修改。

3)不要提供任何 setter 方法。

4)如果要修改類的狀態,必須返回一個新的對象。

按照以上條件,我們來自定義一個簡單的不可變類 Writer。

<code> 1public final class Writer {
2 private final String name;
3 private final int age;
4
5 public Writer(String name, int age) {
6 this.name = name;
7 this.age = age;
8 }
9
10 public int getAge {
11 return age;
12 }
13
14 public String getName {
15 return name;
16 }
17}
/<code>

Writer 類是 final 的,name 和 age 也是 final 的,沒有 setter 方法。

OK,據說這個作者分享了很多博客,廣受讀者的喜愛,因此某某出版社找他寫了一本書(Book)。Book 類是這樣定義的:


<code> 1public class Book {
2 private String name;
3 private int price;
4
5 public String getName {
6 return name;
7 }
8
9 public void setName(String name) {
10 this.name = name;
11 }
12
13 public int getPrice {
14 return price;
15 }
16
17 public void setPrice(int price) {
18 this.price = price;
19 }
20
21 @Override
22 public String toString {
23 return "Book{" +
24 "name='" + name + '\\'' +
25 ", price=" + price +
26 '}';
27 }
28}
/<code>

2 個字段,分別是 name 和 price,以及 getter 和 setter,重寫後的 toString 方法。然後,在 Writer 類中追加一個可變對象字段 book。


<code> 1public final class Writer {
2 private final String name;
3 private final int age;
4 private final Book book;
5
6 public Writer(String name, int age, Book book) {
7 this.name = name;
8 this.age = age;

9 this.book = book;
10 }
11
12 public int getAge {
13 return age;
14 }
15
16 public String getName {
17 return name;
18 }
19
20 public Book getBook {
21 return book;
22 }
23}
/<code>

並在構造方法中追加了 Book 參數,以及 Book 的 getter 方法。

完成以上工作後,我們來新建一個測試類,看看 Writer 類的狀態是否真的不可變。

<code> 1public class WriterDemo {
2 public static void main(String[] args) {
3 Book book = new Book;
4 book.setName("Web全棧開發進階之路");
5 book.setPrice(79);
6
7 Writer writer = new Writer("沉默王二",18, book);
8 System.out.println("定價:" + writer.getBook);
9 writer.getBook.setPrice(59);
10 System.out.println("促銷價:" + writer.getBook);
11 }
12}
/<code>

程序輸出的結果如下所示:

<code>1定價:Book{name='Web全棧開發進階之路', price=79}
2促銷價:Book{name='Web全棧開發進階之路', price=59}
/<code>

糟糕,Writer 類的不可變性被破壞了,價格發生了變化。為了解決這個問題,我們需要為不可變類的定義規則追加一條內容:

如果一個不可變類中包含了可變類的對象,那麼就需要確保返回的是可變對象的副本。也就是說,Writer 類中的 getBook 方法應該修改為:

<code>1public Book getBook {
2 Book clone = new Book;
3 clone.setPrice(this.book.getPrice);
4 clone.setName(this.book.getName);
5 return clone;
6}
/<code>

這樣的話,構造方法初始化後的 Book 對象就不會再被修改了。此時,運行 WriterDemo,就會發現價格不再發生變化了。

<code>1定價:Book{name='Web全棧開發進階之路', price=79}
2促銷價:Book{name='Web全棧開發進階之路', price=79}
/<code>

總結

不可變類有很多優點,就像之前提到的 String 類那樣,尤其是在多線程環境下,它非常的安全。儘管每次修改都會創建一個新的對象,增加了內存的消耗,但這個缺點相比它帶來的優點,顯然是微不足道的——無非就是撿了西瓜,丟了芝麻。


分享到:


相關文章: