創建一個Java的不可變對象
前言:
為什麼 String
是 immutable
類(不可變對象)嗎?我想研究它,想知道為什麼它就不可變瞭,這種強烈的願望就像想研究浩瀚的星空一樣。但無奈自身功力有限,始終覺得霧裡看花終隔一層。二哥你的文章總是充滿趣味性,我想一定能夠說明白,我也一定能夠看明白,能在接下來寫一寫嗎?
https://github.com/itwanger/toBeBetterJavaer
01、什麼是不可變類
一個類的對象在通過構造方法創建後如果狀態不會再被改變,那麼它就是一個不可變(immutable
)類。它的所有成員變量的賦值僅在構造方法中完成,不會提供任何 setter
方法供外部類去修改。
還記得《神雕俠侶》中小龍女的古墓嗎?隨著那一聲巨響,僅有的通道就被無情地關閉瞭。別較真那個密道,我這麼說隻是為瞭打開你的想象力,讓你對不可變類有一個更直觀的印象。
自從有瞭多線程,生產力就被無限地放大瞭,所有的程序員都愛它,因為強大的硬件能力被充分地利用瞭。但與此同時,所有的程序員都對它心生忌憚,因為一不小心,多線程就會把對象的狀態變得混亂不堪。
為瞭保護狀態的原子性、可見性、有序性,我們程序員可以說是竭盡所能。其中,synchronized(同步)關鍵字是最簡單最入門的一種解決方案。
假如說類是不可變的,那麼對象的狀態就也是不可變的。這樣的話,每次修改對象的狀態,就會產生一個新的對象供不同的線程使用,我們程序員就不必再擔心並發問題瞭。
02、常見的不可變類
提到不可變類,幾乎所有的程序員第一個想到的,就是 String
類。那為什麼 String
類要被設計成不可變的呢?
1)常量池的需要
字符串常量池是 Java
堆內存中一個特殊的存儲區域,當創建一個 String
對象時,假如此字符串在常量池中不存在,那麼就創建一個;假如已經存,就不會再創建瞭,而是直接引用已經存在的對象。這樣做能夠減少 JVM 的內存開銷,提高效率。
2)hashCode 的需要
因為字符串是不可變的,所以在它創建的時候,其 hashCode
就被緩存瞭,因此非常適合作為哈希值(比如說作為 HashMap 的鍵),多次調用隻返回同一個值,來提高效率。
3)線程安全
就像之前說的那樣,如果對象的狀態是可變的,那麼在多線程環境下,就很容易造成不可預期的結果。而 String 是不可變的,就可以在多個線程之間共享,不需要同步處理。
因此,當我們調用 String
類的任何方法(比如說 trim()
、substring()
、toLowerCase())
時,總會返回一個新的對象,而不影響之前的值。
String cmower = "沉默王二,一枚有趣的程序員"; cmower.substring(0,4); System.out.println(cmower);// 沉默王二,一枚有趣的程序員
雖然調用 substring()
方法對 cmower
進行瞭截取,但 cmower
的值沒有改變。
除瞭 String
類,包裝器類 Integer
、Long
等也是不可變類。
03、手擼不可變類
看懂一個不可變類也許容易,但要創建一個自定義的不可變類恐怕就有點難瞭。但知難而進是我們作為一名優秀的程序員不可或缺的品質,正因為不容易,我們才能真正地掌握它。
接下來,就請和我一起,來自定義一個不可變類吧。一個不可變誒,必須要滿足以下 4 個條件:
- 1)確保類是
final
的,不允許被其他類繼承。 - 2)確保所有的成員變量(字段)是
final
的,這樣的話,它們就隻能在構造方法中初始化值,並且不會在隨後被修改。 - 3)不要提供任何
setter
方法。 - 4)如果要修改類的狀態,必須返回一個新的對象。
按照以上條件,我們來自定義一個簡單的不可變類 Writer
。
public final class Writer { private final String name; private final int age; public Writer(String name, int age) { this.name = name; this.age = age; } public int getAge() { return age; } public String getName() { return name; } }
Writer
類是 final
的,name
和 age
也是 final
的,沒有 setter
方法。
OK,據說這個作者分享瞭很多博客,廣受讀者的喜愛,因此某某出版社找他寫瞭一本書(Book)。Book 類是這樣定義的:
public class Book { private String name; private int price; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } @Override public String toString() { return "Book{" + "name='" + name + '\'' + ", price=" + price + '}'; } }
2 個字段,分別是 name
和 price
,以及 getter
和 setter
,重寫後的 toString()
方法。然後,在 Writer
類中追加一個可變對象字段 book
。
public final class Writer { private final String name; private final int age; private final Book book; public Writer(String name, int age, Book book) { this.name = name; this.age = age; this.book = book; } public int getAge() { return age; } public String getName() { return name; } public Book getBook() { return book; } }
並在構造方法中追加瞭 Book
參數,以及 Book
的 getter
方法。
完成以上工作後,我們來新建一個測試類,看看 Writer 類的狀態是否真的不可變。
public class WriterDemo { public static void main(String[] args) { Book book = new Book(); book.setName("Web全棧開發進階之路"); book.setPrice(79); Writer writer = new Writer("沉默王二",18, book); System.out.println("定價:" + writer.getBook()); writer.getBook().setPrice(59); System.out.println("促銷價:" + writer.getBook()); } }
程序輸出的結果如下所示:
定價:Book{name=’Web全棧開發進階之路’, price=79}
促銷價:Book{name=’Web全棧開發進階之路’, price=59}
糟糕,Writer 類的不可變性被破壞瞭,價格發生瞭變化。為瞭解決這個問題,我們需要為不可變類的定義規則追加一條內容:
如果一個不可變類中包含瞭可變類的對象,那麼就需要確保返回的是可變對象的副本。也就是說,Writer
類中的 getBook()
方法應該修改為:
public Book getBook() { Book clone = new Book(); clone.setPrice(this.book.getPrice()); clone.setName(this.book.getName()); return clone; }
這樣的話,構造方法初始化後的 Book
對象就不會再被修改瞭。此時,運行 WriterDemo
,就會發現價格不再發生變化瞭。
定價:Book{name=’Web全棧開發進階之路’, price=79}
促銷價:Book{name=’Web全棧開發進階之路’, price=79}
04、總結
不可變類有很多優點,就像之前提到的 String
類那樣,尤其是在多線程環境下,它非常的安全。盡管每次修改都會創建一個新的對象,增加瞭內存的消耗,但這個缺點相比它帶來的優點,顯然是微不足道的——無非就是撿瞭西瓜,丟瞭芝麻。
到此這篇關於創建一個Java的不可變對象的文章就介紹到這瞭,更多相關Java的不可變對象內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Java順序表實現圖書管理系統
- springboot讀取自定義配置文件時出現亂碼解決方案
- java集合collection接口與子接口及實現類
- Java的深拷貝與淺拷貝的幾種實現方式
- 淺談Java封裝、繼承、多態特性