Java中的不可變數據結構

做爲我最近一直在進行的一些編碼訪談的一部分,有時會出現不變性問題。我本身並不過度教條,但每當不須要可變狀態時,我會試圖擺脫致使可變性的代碼,這在數據結構中一般是最明顯的。然而,彷佛對不可變性的概念存在一些誤解,開發人員一般認爲擁有final引用,或者val在Kotlin或Scala中,足以使對象不可變。這篇博客文章深刻研究了不可變引用和不可變數據結構java

不可變數據結構的好處

不可變數據結構具備顯着優點,例如:安全

  • 沒有無效的狀態
  • 線程安全
  • 易於理解的代碼
  • 更容易測試代碼
  • 可用於值類型

沒有無效的狀態

當一個對象是不可變的時,很難讓對象處於無效狀態。該對象只能經過其構造函數實例化,這將強制對象的有效性。這樣,能夠強制執行有效狀態所需的參數。一個例子:bash

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.
複製代碼

線程安全

因爲沒法更改對象,所以能夠在線程之間共享它,而不會出現競爭條件或數據突變問題。數據結構

易於理解的代碼

與無效狀態的代碼示例相似,使用構造函數一般比初始化方法更容易。這是由於構造函數強制執行必需的參數,而setter或initializer方法在編譯時不會強制執行。app

更易於測試的代碼

因爲對象更具可預測性,所以沒必要測試初始化​​方法的全部排列,即在調用類的構造函數時,該對象有效或無效。使用這些類的代碼的其餘部分變得更可預測,具備更少的NullPointerException 機會。有時,當傳遞對象時,有些方法可能會改變對象的狀態。例如:ide

public boolean isOverseas(Address address) {
    if(address.getCountry().equals("Australia") == false) {
        address.setOverseas(true); // address has now been mutated!
        return true;
    } else {
        return false;
    }
}
複製代碼

通常來講,上面的代碼是很差的作法。它返回一個布爾值,並可能改變對象的狀態。這使得代碼更難理解和測試。更好的解決方案是從Address 類中刪除setter ,並經過測試國家名稱返回一個布爾值。更好的方法是將此邏輯移動到 Address 類自己(address.isOverseas())。當確實須要設置狀態時,在不改變輸入的狀況下製做原始對象的副本。函數

可用於值類型

想象一下金額,好比10美圓。10美圓將永遠是10美圓。在代碼中,這可能看起來像 public Money(final BigInteger amount, final Currency currency)。正如您在此代碼中看到的那樣,不可能將10美圓的值更改成除此以外的任何值,所以,上述內容能夠安全地用於值類型。測試

最終引用不要使對象不可變

如前所述,我常常遇到的問題之一是這些開發人員中的很大一部分並不徹底理解最終引用和不可變對象之間的區別。彷佛這些開發人員的共同理解是,變量成爲最終的那一刻,數據結構變得不可變。不幸的是,這並非那麼簡單,我想一勞永逸地把這種誤解帶出世界:ui

A final reference does not make your objects immutable!this

換句話說,下面的代碼並沒有使對象不變:

final Person person = new Person("John");
複製代碼

爲何不?好吧,雖然person是最後一個字段並且沒法從新分配,可是 Person類可能有一個setter方法或其餘mutator方法,能夠執行以下操做:

person.setName("Cindy");
複製代碼

不管最終修飾符如何,這都是一件很是容易的事情。或者, Person類可能會公開這樣的地址列表。訪問此列表容許您向其添加地址,所以,以下所示改變 person對象:

person.getAddresses().add(new Address("Sydney"));
複製代碼

好了,既然咱們已經解決了這個問題,那麼讓咱們深刻了解一下咱們如何使類不可變。在設計咱們的類時,咱們須要記住幾件事:

  • 不要以可變的方式暴露內部狀態
  • 不要在內部改變狀態
  • 確保子類不會覆蓋上述行爲

根據如下準則,讓咱們設計一個更好的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;
    }
}
複製代碼

如今,可使用如下代碼:

import java.util.List;
final Person person = new Person("John", List.of(new Address(「Sydney」, "Australia"));
複製代碼

如今,上面的代碼是不可變的,可是因爲PersonAddress 類的設計 ,同時還有最終引用,所以沒法將person變量從新分配給其餘任何東西。

更新:正如有些人提到的,上面的代碼仍然是可變的,由於我沒有在構造函數中複製地址列表。所以,若是不在ArrayList() 構造函數中調用new ,仍然能夠執行如下操做:

final List<Address> addresses = new ArrayList<>();
addresses.add(new Address("Sydney", "Australia"));
final Person person = new Person("John", addressList);
addresses.clear();
複製代碼

可是,因爲在構造函數中建立了一個新副本,上面的代碼將再也不影響類中複製的地址列表引用 Person ,從而使代碼安全。

我但願上述內容有助於理解最終和不變性之間的差別。若是您有任何意見或反饋,請在下面的評論中告訴我。

再次,很是感謝個人同事Winston花時間校對和審閱這篇博文!

英文原文:dzone.com/articles/im…

查看更多文章

公衆號:銀河系1號

聯繫郵箱:public@space-explore.com

(未經贊成,請勿轉載)

相關文章
相關標籤/搜索