Java中的不可變資料結構

銀河1號發表於2019-04-19

作為我最近一直在進行的一些編碼訪談的一部分,有時會出現不變性問題。我自己並不過分教條,但每當不需要可變狀態時,我會試圖擺脫導致可變性的程式碼,這在資料結構中通常是最明顯的。然而,似乎對不可變性的概念存在一些誤解,開發人員通常認為擁有final引用,或者val在Kotlin或Scala中,足以使物件不可變。這篇部落格文章深入研究了不可變引用和不可變資料結構

不可變資料結構的好處

不可變資料結構具有顯著優勢,例如:

  • 沒有無效的狀態
  • 執行緒安全
  • 易於理解的程式碼
  • 更容易測試程式碼
  • 可用於值型別

沒有無效的狀態

當一個物件是不可變的時,很難讓物件處於無效狀態。該物件只能通過其建構函式例項化,這將強制物件的有效性。這樣,可以強制執行有效狀態所需的引數。一個例子:

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方法在編譯時不會強制執行。

更易於測試的程式碼

由於物件更具可預測性,因此不必測試初始化​​方法的所有排列,即在呼叫類的建構函式時,該物件有效或無效。使用這些類的程式碼的其他部分變得更可預測,具有更少的NullPointerException 機會。有時,當傳遞物件時,有些方法可能會改變物件的狀態。例如:

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美元的值更改為除此之外的任何值,因此,上述內容可以安全地用於值型別。

最終引用不要使物件不可變

如前所述,我經常遇到的問題之一是這些開發人員中的很大一部分並不完全理解最終引用和不可變物件之間的區別。似乎這些開發人員的共同理解是,變數成為最終的那一刻,資料結構變得不可變。不幸的是,這並不是那麼簡單,我想一勞永逸地把這種誤解帶出世界:

A final reference does not make your objects immutable!

換句話說,下面的程式碼並沒有使物件不變:

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

(未經同意,請勿轉載)


相關文章