Java中的不可變資料結構 - Jworks.io

banq發表於2019-03-27

開發人員通常認為擁有final引用,或者val在Kotlin或Scala中,足以使物件不可變。這篇部落格文章深入研究了不可變引用和不可變資料結構。

不可變資料結構的好處
不可變資料結構具有一些顯著的好處,例如:

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

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

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.


2.執行緒安全
由於無法更改物件,因此可以線上程之間共享它,而不會出現競爭條件或資料突變問題。

3.更容易理解程式碼
與無效狀態中的程式碼示例類似,使用建構函式通常比使用初始化方法更容易。這是因為建構函式強制執行必需的引數,而在編譯時不強制執行setter或initialiser方法。

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

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())。當確實需要設定狀態時,在不改變輸入的情況下製作原始物件的副本。

5. 可用於值型別
想象一下金額,比如10美元。10美元總是10美元。在程式碼中,這可能看起來像公共貨幣(最終BigInteger金額,最終貨幣貨幣)。正如您在此程式碼中所看到的,不可能將10美元的值更改為除此之外的任何值,因此上述內容可以安全地用於值型別。

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

final引用不會使您的物件不可變!

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

final Person person = new Person("John");


為什麼不?好吧,雖然`person`是一個final欄位,並且無法重新分配,但Person類可能有一個setter方法或其他可變mutator方法,可以執行如下操作:

person.setName("Cindy");


無論最終修飾符如何,都可以輕鬆完成。或者,Person類可能會公開這樣的地址列表。訪問此列表允許您向其新增地址,因此會改變person物件,如下所示:

person.getAddresses().add(new Address("Sydney"));


我們的final引用沒有幫助我們阻止我們改變人物物件。
好的,現在我們已經解決了這個問題,讓我們深入瞭解一下我們如何使一個類不可變。在設計我們的類時,我們需要記住幾件事:
  • 不要以可變的方式暴露內部狀態
  • 不要在內部改變狀態
  • 確保子類不會覆蓋上述行為

根據以下準則,讓我們設計一個更好的Person類版本。 

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類的設計,上面的程式碼是不可變的,同時還有final的引用,因此無法將person變數重新分配給其他任何東西。

更新:正如有些人提到的,上面的程式碼仍然是可變的,因為我沒有在建構函式中複製地址列表。因此,如果不在建構函式中呼叫新的ArrayList(),仍然可以執行以下操作:

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


但是,由於現在在建構函式中建立了一個副本,上面的程式碼將不再影響Person類中複製的地址列表引用,從而使程式碼安全。謝謝大家的好評!
​​​​​​​

相關文章