因此,在我的第一篇文章中,我談到了一些構建器模式,并提到了一個非常強大但卻被忽視的概念:不變性。
什么是不可變類? 這只是一個其實例無法修改的類。 類屬性的每個值都在其聲明或其構造函數中設置,并在對象的整個生命周期中保留這些值。 Java有很多不變的類,例如String ,所有帶框的原語( Double , Integer , Float等), BigInteger和BigDecimal等。 這有一個很好的理由:不可變類比可變類更容易設計,實現和使用。 一旦實例化它們,它們只能處于一種狀態,因此它們不易出錯,并且,正如我們在本文后面會看到的那樣,它們更加安全。
您如何確保類是不變的? 只需遵循以下5個簡單步驟:
- 不要提供任何可修改對象狀態的公共方法 ,也稱為增變器(例如setter)。
- 防止擴展類 。 這不允許任何惡意或粗心的類擴展我們的類并損害其不變的行為。 這樣做的通常且更簡單的方法是將類標記為final ,但是我將在本文中提及另一種方法。
- 將所有字段定為最終值 。 這是讓編譯器為您強制執行第1點的方法。 此外,它清楚地使任何看到您的代碼的人都知道,您不希望這些字段在設置后更改其值。
- 將所有字段設為私有 。 無論您是否考慮不變性,這一點都應該很明顯,并且您應該遵循它 ,但是我只是為了以防萬一。
- 永遠不要提供對任何可變屬性的訪問 。 如果您的類具有一個可變對象作為其屬性之一(例如List , Map或您的域問題中的任何其他可變對象),請確保該類的客戶端永遠無法獲得對該對象的引用。 這意味著您永遠不要從訪問器(例如,getter)直接返回對它們的引用,并且永遠不要在構造函數中使用從客戶端作為參數傳遞的引用來初始化它們。 在這種情況下,您應該始終制作防御性副本。
有很多理論,沒有代碼,因此讓我們看一個簡單的不可變類是什么樣子,以及它如何處理我之前提到的5個步驟:
public class Book {private final String isbn;private final int publicationYear;private final List reviews;private Book(BookBuilder builder) {this.isbn = builder.isbn;this.publicationYear = builder.publicationYear;this.reviews = Lists.newArrayList(builder.reviews);}public String getIsbn() {return isbn;}public int getPublicationYear() {return publicationYear;}public List getReviews() {return Lists.newArrayList(reviews);}public static class BookBuilder {private String isbn;private int publicationYear;private List reviews;public BookBuilder isbn(String isbn) {this.isbn = isbn;return this;}public BookBuilder publicationYear(int year) {this.publicationYear = year;return this;}public BookBuilder reviews(List reviews) {this.reviews = reviews == null ? new ArrayList() : reviews;return this;}public Book build() {return new Book(this);}}
}
我們將在這個非常簡單的課程中講解重點。 首先,您可能已經注意到,我再次使用了構建器模式。 這不僅是因為我是它的忠實擁護者,而且還因為我想說明一些我不想在之前的帖子中沒有首先對不變性概念有基本了解的觀點。 現在,讓我們看一下我提到的5個步驟,您需要遵循這些步驟使一個類不可變,并查看它們是否對本書示例有效:
- 不要提供任何修改對象狀態的公共方法 。 請注意,該類上的唯一方法是其私有構造函數和其屬性的獲取器,但沒有更改對象狀態的方法。
- 防止擴展類 。 這個很棘手。 我提到確保這一點的最簡單方法是將班級定為最終班,但Book班顯然不是最終班。 但是,請注意,唯一可用的構造函數是private 。 編譯器確保沒有公共或受保護的構造函數的類不能被子類化。 因此,在這種情況下,不必在類聲明中使用final關鍵字,但無論如何將其包括在內只是一個好主意,以使看到您代碼的任何人都可以清楚地知道。
- 將所有字段定為最終值 。 非常簡單,該類上的所有屬性都聲明為final 。
- 永遠不要提供對任何可變屬性的訪問 。 這實際上很有趣。 注意Book類如何具有一個List <String>屬性,該屬性被聲明為final,并且其值在類構造函數上設置。 但是,此列表是可變對象。 也就是說,雖然評論參考一旦設置就無法更改,但列表的內容可以更改。 引用相同列表的客戶端可以添加或刪除元素,結果,在創建Book對象后更改其狀態。 因此,請注意,在Book構造函數中,我們不直接分配引用。 相反,我們使用Guava庫通過調用“
this.reviews = Lists.newArrayList(builder.reviews);
”來復制列表this.reviews = Lists.newArrayList(builder.reviews);
”。 在getReviews
方法上可以看到相同的情況,在該方法中,我們返回列表的副本而不是直接引用。 值得注意的是,此示例可能有點過于簡化,因為評論列表只能包含不可變的字符串。 如果列表的類型是可變的類,那么您還必須復制列表中的每個對象,而不僅僅是列表本身。
最后一點說明了為什么不可變的類導致更簡潔的設計和更易于閱讀的代碼。 您只需共享那些不可變的對象,而不必擔心防御性副本。 實際上,絕對不要制作任何副本,因為對象的任何副本都將永遠等于原始副本。 一個必然的結論是,不變的對象僅僅是簡單的。 他們只能處于一種狀態,并且一生都保持這種狀態。 您可以使用類構造函數來檢查任何不變量(即,需要在該類上有效的條件,例如其屬性之一的值范圍),然后可以確保這些不變量保持真實狀態而無需付出任何努力您或您的客戶。
不變對象的另一個巨大好處是它們本質上是線程安全的。 它們不能被同時訪問對象的多個線程破壞。 到目前為止,這是在應用程序中提供線程安全性的最簡單且不易出錯的方法。
但是,如果您已經有一個Book實例并且想要更改其屬性之一的值怎么辦? 換句話說,您想要更改對象的狀態。 在不可變的類上,根據定義,這是不可能的。 但是,就像軟件中的大多數事情一樣,總有一種解決方法。 在這種情況下,實際上有兩個。
第一種選擇是在Book類上使用Fluent Interface技術,并具有類似于setter的方法,這些方法實際上創建一個對象,該對象的所有屬性都具有相同的值,但要更改的對象除外。 在我們的示例中,我們將必須在Book類中添加以下內容:
private Book(BookBuilder builder) {this(builder.isbn, builder.publicationYear, builder.reviews);}private Book(String isbn, int publicationYear, List reviews) {this.isbn = isbn;this.publicationYear = publicationYear;this.reviews = Lists.newArrayList(reviews);}public Book withIsbn(String isbn) {return new Book(isbn,this.publicationYear, this.reviews);}
請注意,我們添加了一個新的私有構造函數,可以在其中指定每個屬性的值,并修改舊的構造函數以使用新的構造函數。 另外,我們添加了一個新方法,該方法返回一個新的Book對象,該對象具有我們想要的isbn屬性值。 相同的概念適用于該類的其余屬性。 之所以稱為功能方法,是因為方法無需修改即可返回對其參數進行操作的結果。 這與程序或命令式方法形成對比,在方法式或命令式方法中,方法將一個過程應用于其操作數,從而更改其狀態。
這種生成新對象的方法顯示了不可變類的唯一真正缺點:它們要求我們為所需的每個不同值創建一個新對象,這會在性能和內存消耗方面產生可觀的開銷。 如果要更改對象的幾個屬性,則會放大此問題,因為在每個步驟中都將生成一個新對象,并且最終將丟棄所有中間對象并僅保留最后一個結果。
我們可以在構建器模式的幫助下為多步操作提供更好的選擇,例如我在上一段中描述的操作。 基本上,我們向構建器添加一個新的構造函數,該構造函數采用一個已經創建的實例來設置其所有初始值。 然后,客戶端可以以通常的方式使用構建器來設置所有所需的值,然后使用build方法來創建最終對象。 這樣,我們避免只使用我們需要的某些值來創建中間對象。 在我們的示例中,此技術在生成器方面看起來像這樣:
public BookBuilder(Book book) {this.isbn = book.getIsbn();this.publicationYear = book.getPublicationYear();this.reviews = book.getReviews();
}
然后,在我們的客戶上,我們可以:
Book originalBook = getRandomBook();Book modifiedBook = new BookBuilder(originalBook).isbn('123456').publicationYear(2011).build();
現在,顯然該構建器不是線程安全的,因此您必須采取所有常用的預防措施,例如不與多個線程共享一個構建器。
我提到過這樣一個事實,即我們必須為狀態的每個變化都創建一個新對象,這可能會增加性能,這是不可變類的唯一真正的缺點。 但是,對象創建是JVM不斷改進的方面之一。 實際上,除特殊情況外,對象創建比您想象的要高效得多。 無論如何,提出一個簡單明了的設計通常是一個好主意,然后僅在進行測量后才重構性能。 當您嘗試猜測代碼在何處花費大量時間時,十分之九會發現您錯了。 此外,不變對象可以自由共享而不必擔心后果,這一事實使您有機會鼓勵客戶端盡可能重用現有實例,從而大大減少了創建對象的數量。 一種常見的方法是為最常見的值提供公共靜態最終常量。 此技術在JDK上大量使用,例如在Boolean.FALSE或BigDecimal.ZERO中 。
總結一下這篇文章,如果您想從中學到一些東西,那就這樣吧: 除非有充分的理由使類可變,否則類應該是不可變的 。 不要為每個類屬性自動添加設置器。 如果由于某種原因您絕對不能使您的類不可變,那么請盡可能限制其可變性。 一個對象可以處于的狀態越少,就越容易考慮該對象及其不變量。 而且不必擔心不變性的性能開銷,很有可能您不必擔心它。
參考: JCG合作伙伴 Jose Luis在開發上的 不變性的來龍去脈 。
翻譯自: https://www.javacodegeeks.com/2013/01/the-ins-and-outs-of-immutability.html