泛談Java中的不可變數(shù)據(jù)結(jié)構(gòu)
作為我最近一直在進(jìn)行的一些編碼訪談的一部分,有時(shí)會(huì)出現(xiàn)不變性問題。我自己并不過分教條,但每當(dāng)不需要可變狀態(tài)時(shí),我會(huì)試圖擺脫導(dǎo)致可變性的代碼,這在數(shù)據(jù)結(jié)構(gòu)中通常是最明顯的。然而,似乎對(duì)不可變性的概念存在一些誤解,開發(fā)人員通常認(rèn)為擁有final引用,或者val在Kotlin或Scala中,足以使對(duì)象不可變。這篇博客文章深入研究了不可變引用和不可變數(shù)據(jù)結(jié)構(gòu)。
不可變數(shù)據(jù)結(jié)構(gòu)的好處
不可變數(shù)據(jù)結(jié)構(gòu)具有顯著優(yōu)勢(shì),例如:
- 沒有無效的狀態(tài)
- 線程安全
- 易于理解的代碼
- 更容易測(cè)試代碼
- 可用于值類型
沒有無效的狀態(tài)
當(dāng)一個(gè)對(duì)象是不可變的時(shí),很難讓對(duì)象處于無效狀態(tài)。該對(duì)象只能通過其構(gòu)造函數(shù)實(shí)例化,這將強(qiáng)制對(duì)象的有效性。這樣,可以強(qiáng)制執(zhí)行有效狀態(tài)所需的參數(shù)。一個(gè)例子:
Address address = new Address(); address.setCity("Sydney"); // address is in invalid state now, since the country hasn't been set. Address address = new Address("Sydney", "Australia"); // Address is valid and doesn't have setters, so the address object is always valid.
線程安全
由于無法更改對(duì)象,因此可以在線程之間共享它,而不會(huì)出現(xiàn)競(jìng)爭(zhēng)條件或數(shù)據(jù)突變問題。
易于理解的代碼
與無效狀態(tài)的代碼示例類似,使用構(gòu)造函數(shù)通常比初始化方法更容易。這是因?yàn)闃?gòu)造函數(shù)強(qiáng)制執(zhí)行必需的參數(shù),而setter或initializer方法在編譯時(shí)不會(huì)強(qiáng)制執(zhí)行。
更易于測(cè)試的代碼
由于對(duì)象更具可預(yù)測(cè)性,因此不必測(cè)試初始化方法的所有排列,即在調(diào)用類的構(gòu)造函數(shù)時(shí),該對(duì)象有效或無效。使用這些類的代碼的其他部分變得更可預(yù)測(cè),具有更少的NullPointerException機(jī)會(huì)。有時(shí),當(dāng)傳遞對(duì)象時(shí),有些方法可能會(huì)改變對(duì)象的狀態(tài)。例如:
public boolean isOverseas(Address address) { if(address.getCountry().equals("Australia") == false) { address.setOverseas(true); // address has now been mutated! return true; } else { return false; } }
一般來說,上面的代碼是不好的做法。它返回一個(gè)布爾值,并可能改變對(duì)象的狀態(tài)。這使得代碼更難理解和測(cè)試。更好的解決方案是從Address 類中刪除setter ,并通過測(cè)試國(guó)家名稱返回一個(gè)布爾值。更好的方法是將此邏輯移動(dòng)到 Address 類本身(address.isOverseas())。當(dāng)確實(shí)需要設(shè)置狀態(tài)時(shí),在不改變輸入的情況下制作原始對(duì)象的副本。
可用于值類型
想象一下金額,比如10美元。10美元將永遠(yuǎn)是10美元。在代碼中,這可能看起來像 public Money(final BigInteger amount, final Currency currency)。正如您在此代碼中看到的那樣,不可能將10美元的值更改為除此之外的任何值,因此,上述內(nèi)容可以安全地用于值類型。
最終引用不要使對(duì)象不可變
如前所述,我經(jīng)常遇到的問題之一是這些開發(fā)人員中的很大一部分并不完全理解最終引用和不可變對(duì)象之間的區(qū)別。似乎這些開發(fā)人員的共同理解是,變量成為最終的那一刻,數(shù)據(jù)結(jié)構(gòu)變得不可變。不幸的是,這并不是那么簡(jiǎn)單,我想一勞永逸地把這種誤解帶出世界:
A final reference does not make your objects immutable!
換句話說,下面的代碼并沒有使對(duì)象不變:
final Person person = new Person("John");
為什么不?好吧,雖然person是最后一個(gè)字段而且無法重新分配,但是 Person類可能有一個(gè)setter方法或其他mutator方法,可以執(zhí)行如下操作:
person.setName("Cindy");
無論最終修飾符如何,這都是一件非常容易的事情?;蛘?, Person類可能會(huì)公開這樣的地址列表。訪問此列表允許您向其添加地址,因此,如下所示改變 person對(duì)象:
person.getAddresses().add(new Address("Sydney"));
好了,既然我們已經(jīng)解決了這個(gè)問題,那么讓我們深入了解一下我們?nèi)绾问诡惒豢勺?。在設(shè)計(jì)我們的類時(shí),我們需要記住幾件事:
- 不要以可變的方式暴露內(nèi)部狀態(tài)
- 要在內(nèi)部改變狀態(tài)
- 確保子類不會(huì)覆蓋上述行為
根據(jù)以下準(zhǔn)則,讓我們?cè)O(shè)計(jì)一個(gè)更好的Person class 版本 。
public final class Person {// final class, can't be overridden by subclasses private final String name; // final for safe publication in multithreaded applications private final List<Address> addresses; public Person(String name, List<Address> addresses) { this.name = name; this.addresses = List.copyOf(addresses); // makes a copy of the list to protect from outside mutations (Java 10+). // Otherwise, use Collections.unmodifiableList(new ArrayList<>(addresses)); } public String getName() { return this.name; // String is immutable, okay to expose } public List<Address> getAddresses() { return addresses; // Address list is immutable } } public final class Address { // final class, can't be overridden by subclasses private final String city; // only immutable classes private final String country; public Address(String city, String country) { this.city = city; this.country = country; } public String getCity() { return city; } public String getCountry() { return country; } }
現(xiàn)在,可以使用以下代碼:
import java.util.List; final Person person = new Person("John", List.of(new Address(“Sydney”, "Australia"));
現(xiàn)在,上面的代碼是不可變的,但是由于Person 和 Address 類的設(shè)計(jì) ,同時(shí)還有最終引用,因此無法將person變量重新分配給其他任何東西。
更新:正如有些人提到的,上面的代碼仍然是可變的,因?yàn)槲覜]有在構(gòu)造函數(shù)中復(fù)制地址列表。因此,如果不在ArrayList() 構(gòu)造函數(shù)中調(diào)用new ,仍然可以執(zhí)行以下操作:
final List<Address> addresses = new ArrayList<>(); addresses.add(new Address("Sydney", "Australia")); final Person person = new Person("John", addressList); addresses.clear();
但是,由于在構(gòu)造函數(shù)中創(chuàng)建了一個(gè)新副本,上面的代碼將不再影響類中復(fù)制的地址列表引用Person ,從而使代碼安全。
我希望上述內(nèi)容有助于理解最終和不變性之間的差異。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Java PriorityQueue數(shù)據(jù)結(jié)構(gòu)接口原理及用法
- Java 單鏈表數(shù)據(jù)結(jié)構(gòu)的增刪改查教程
- Java數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)折半查找的算法過程解析
- Java版數(shù)據(jù)結(jié)構(gòu)插入數(shù)據(jù)時(shí)遇到的結(jié)點(diǎn)為空的問題詳解
- java數(shù)據(jù)結(jié)構(gòu)和算法中哈希表知識(shí)點(diǎn)詳解
- JAVA數(shù)據(jù)結(jié)構(gòu)之漢諾塔代碼實(shí)例
- 詳解Java集合中的基本數(shù)據(jù)結(jié)構(gòu)
相關(guān)文章
java向mysql插入數(shù)據(jù)亂碼問題的解決方法
這篇文章主要為大家詳細(xì)介紹了java向mysql插入數(shù)據(jù)亂碼問題的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-09-09java中對(duì)象的比較equal、Comparble、Comparator的區(qū)別
本文主要介紹了java中對(duì)象的比較equal、Comparble、Comparator的區(qū)別,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10用SpringBoot Admin監(jiān)控SpringBoot程序
這篇文章主要介紹了用SpringBoot Admin監(jiān)控SpringBoot程序,幫助大家更好的理解和使用springboot框架,感興趣的朋友可以了解下2020-10-10JAVA實(shí)現(xiàn)第三方短信發(fā)送過程詳解
這篇文章主要介紹了JAVA實(shí)現(xiàn)第三方短信發(fā)送過程詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-09-09Java線程中斷機(jī)制interrupt、isInterrupted、interrupted方法詳解
這篇文章主要介紹了Java線程中斷機(jī)制interrupt、isInterrupted、interrupted方法詳解,一個(gè)線程不應(yīng)該由其他線程來強(qiáng)制中斷或停止,而是應(yīng)該由線程自己自行停止,所以,Thread.stop、Thread.suspend、Thread. resume都已經(jīng)被廢棄了,需要的朋友可以參考下2024-01-01springboot集成gzip和zip數(shù)據(jù)壓縮傳輸(適用大數(shù)據(jù)信息傳輸)
?在大數(shù)據(jù)量的傳輸中,壓縮數(shù)據(jù)后進(jìn)行傳輸可以一定程度的解決速度問題,本文主要介紹了springboot集成gzip和zip數(shù)據(jù)壓縮傳輸,具有一定的參考價(jià)值,感興趣的可以了解一下2023-09-09使用springboot 獲取控制器參數(shù)的幾種方法小結(jié)
這篇文章主要介紹了使用springboot 獲取控制器參數(shù)的幾種方法小結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12Java繼承extends與super關(guān)鍵字詳解
本篇文章給大家詳細(xì)講述了Java繼承extends與super關(guān)鍵字的相關(guān)知識(shí)點(diǎn),需要的朋友們可以參考學(xué)習(xí)下。2018-02-02