天天看點

Java中的不可變資料結構

作為我最近一直在進行的一些編碼訪談的一部分,有時會出現不變性問題。我自己并不過分教條,但每當不需要可變狀态時,我會試圖擺脫導緻可變性的代碼,這在資料結構中通常是最明顯的。然而,似乎對不可變性的概念存在一些誤解,開發人員通常認為擁有

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"));           

複制

現在,上面的代碼是不可變的,但是由于

Person

Address

類的設計 ,同時還有最終引用,是以無法将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/immu