对象应该是不可变的
在面向對象的編程中,如果對象的狀態在創建后無法修改,則該對象是不可變的 。
在Java中,不可變對象的一個??很好的例子是String 。 創建完成后,我們將無法修改其狀態。 我們可以要求它創建新的字符串,但是它自己的狀態永遠不會改變。
但是,JDK中沒有那么多不可變的類。 以類Date為例。 可以使用setTime()修改其狀態。
我不知道為什么JDK設計師決定以不同的方式來制作這兩個非常相似的類。 但是,我認為可變Date的設計有許多缺陷,而不變的String則更多地體現了面向對象范例的精神。
而且,我認為在一個完美的面向對象的世界中所有類都應該是不變的 。 不幸的是,有時由于JVM的限制在技術上是不可能的。 盡管如此,我們應該始終追求最佳。
這是支持不變性的參數的不完整列表:
- 不變的對象更易于構造,測試和使用
- 真正不可變的對象始終是線程安全的
- 它們有助于避免時間耦合
- 它們的使用無副作用(無防御性副本)
- 避免身份變異性問題
- 他們總是有失敗原子性
- 它們更容易緩存
- 他們防止NULL引用, 這是不好的
讓我們一一討論最重要的論點。
線程安全
第一個也是最明顯的論點是,不可變對象是線程安全的。 這意味著多個線程可以同時訪問同一對象,而不會與另一個線程發生沖突。
如果沒有對象方法可以修改其狀態,那么無論它們有多少個以及被調用的頻率是多少,它們都將在自己的堆棧內存空間中工作。
Goetz等。 在非常著名的《 Java Concurrency in Practice》 (強烈推薦)一書中,更詳細地介紹了不可變對象的優點。
避免時間耦合
這是時間耦合的示例(代碼發出兩個連續的HTTP POST請求,其中第二個包含HTTP正文):
Request request = new Request("http://example.com"); request.method("POST"); String first = request.fetch(); request.body("text=hello"); String second = request.fetch();此代碼有效。 但是,您必須記住,應該在配置第二個請求之前配置第一個請求。 如果我們決定從腳本中刪除第一個請求,則將刪除第二行和第三行,并且不會從編譯器中得到任何錯誤:
Request request = new Request("http://example.com"); // request.method("POST"); // String first = request.fetch(); request.body("text=hello"); String second = request.fetch();現在,該腳本已損壞,盡管它編譯時沒有錯誤。 這就是時間耦合的意義所在—代碼中總是有一些程序員必須記住的隱藏信息。 在此示例中,我們必須記住,第一個請求的配置也用于第二個請求。
我們必須記住,第二個請求應始終保持在一起,并在第一個請求之后執行。
如果Request類是不可變的,則第一個代碼段將一開始就無法工作,并且將被重寫為:
final Request request = new Request(""); String first = request.method("POST").fetch(); String second = request.method("POST").body("text=hello").fetch();現在,這兩個請求沒有耦合。 我們可以安全地刪除第一個,第二個仍然可以正常工作。 您可能會指出存在代碼重復。 是的,我們應該擺脫它,然后重新編寫代碼:
final Request request = new Request(""); final Request post = request.method("POST"); String first = post.fetch(); String second = post.body("text=hello").fetch();瞧,重構并沒有破壞任何東西,我們仍然沒有時間耦合。 可以安全地從代碼中刪除第一個請求,而不會影響第二個請求。
我希望這個例子能證明操作不可變對象的代碼更具可讀性和可維護性,因為它沒有時間上的耦合。
避免副作用
讓我們嘗試在新方法中使用我們的Request類(現在它是可變的):
public String post(Request request) {request.method("POST");return request.fetch(); }讓我們嘗試發出兩個請求-第一個請求使用GET方法,第二個請求使用POST:
Request request = new Request("http://example.com"); request.method("GET"); String first = this.post(request); String second = request.fetch();方法post()具有“副作用”,它對可變對象request進行了更改。 在這種情況下,這些更改并不是真正預期的。 我們希望它發出POST請求并返回其主體。 我們不想閱讀其文檔只是為了發現它還在后臺修改了我們作為參數傳遞給它的請求。
不用說,這種副作用會導致錯誤和可維護性問題。 使用不可變的Request會更好:
public String post(Request request) {return request.method("POST").fetch(); }在這種情況下,我們可能沒有任何副作用。 任何人都不能修改我們的request對象,無論它在何處使用以及方法調用傳遞給調用堆棧的深度如何:
Request request = new Request("http://example.com").method("GET"); String first = this.post(request); String second = request.fetch();此代碼是絕對安全且無副作用的。
避免身份變異
通常,如果對象的內部狀態相同,我們希望它們相同。 Date類是一個很好的例子:
Date first = new Date(1L); Date second = new Date(1L); assert first.equals(second); // true有兩個不同的對象。 但是,它們彼此相等,因為它們的封裝狀態相同。 通過自定義的equals()和hashCode()方法的重載實現,可以實現這一點。
這種方便的方法與可變對象一起使用的結果是,每次我們修改對象的狀態時,它都會更改其身份:
Date first = new Date(1L); Date second = new Date(1L); first.setTime(2L); assert first.equals(second); // false在您開始將可變對象用作地圖中的鍵之前,這看起來很自然:
Map<Date, String> map = new HashMap<>(); Date date = new Date(); map.put(date, "hello, world!"); date.setTime(12345L); assert map.containsKey(date); // false修改date狀態對象時,我們不希望它更改其身份。 我們不希望僅因為其鍵的狀態已更改而在映射中丟失條目。 但是,這正是上面示例中發生的情況。
當我們向地圖添加對象時,其hashCode()返回一個值。 HashMap使用此值將條目放置到內部哈希表中。 當我們調用containsKey()時,對象的哈希碼是不同的(因為它基于其內部狀態),并且HashMap在內部哈希表中找不到它。
調試可變對象的副作用非常煩人且困難。 不可變的對象完全避免了它。
失效原子性
這是一個簡單的示例:
public class Stack {private int size;private String[] items;public void push(String item) {size++;if (size > items.length) {throw new RuntimeException("stack overflow");}items[size] = item;} }很明顯,如果Stack類在溢出時引發運行時異常,則該對象將處于斷開狀態。 它的size屬性將增加,而items將不會獲得新元素。
不變性可以防止此問題。 對象永遠不會處于損壞狀態,因為僅在其構造函數中修改了對象的狀態。 構造函數將失敗,拒絕對象實例化,或者成功,則將生成有效的固態對象,該對象永遠不會更改其封裝狀態。
有關此主題的更多信息,請閱讀Joshua Bloch撰寫的有效Java,第二版 。
反對不變性的爭論
有許多反對不變性的論點。
如果您還有其他論點,請在下面發表,我將嘗試發表評論。
翻譯自: https://www.javacodegeeks.com/2014/09/objects-should-be-immutable.html
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
- 上一篇: 华为 MateBook E 二合一笔记本
- 下一篇: 其他一些单元测试技巧