切換語言為:簡體

什麼是Java中的不可變物件

  • 爱糖宝
  • 2024-10-10
  • 2037
  • 0
  • 0

01、什麼是不可變類

一個類的物件在透過構造方法建立後如果狀態不會再被改變,那麼它就是一個不可變(immutable)類。它的所有成員變數的賦值僅在構造方法中完成,不會提供任何 setter 方法供外部類去修改。

自從有了多執行緒,生產力就被無限地放大了,所有的程式設計師都愛它,因為強大的硬體能力被充分地利用了。但與此同時,所有的程式設計師都對它心生忌憚,因為一不小心,多執行緒就會把物件的狀態變得混亂不堪。

爲了保護狀態的原子性、可見性、有序性,我們程式設計師可以說是竭盡所能。其中,synchronized(同步)關鍵字是最簡單最入門的一種解決方案。

假如說類是不可變的,那麼物件的狀態就也是不可變的。這樣的話,每次修改物件的狀態,就會產生一個新的物件供不同的執行緒使用,我們程式設計師就不必再擔心併發問題了。

02、常見的不可變類

提到不可變類,幾乎所有的程式設計師第一個想到的,就是 String 類。那為什麼 String 類要被設計成不可變的呢?

1)常量池的需要

字串常量池是 Java 堆記憶體中一個特殊的儲存區域,當建立一個 String 物件時,假如此字串在常量池中不存在,那麼就建立一個;假如已經存,就不會再建立了,而是直接引用已經存在的物件。這樣做能夠減少 JVM 的記憶體開銷,提高效率。

2)hashCode 需要

因為字串是不可變的,所以在它建立的時候,其 hashCode 就被快取了,因此非常適合作為雜湊值(比如說作為 HashMap 的鍵),多次呼叫只返回同一個值,來提高效率。

3)執行緒安全

就像之前說的那樣,如果物件的狀態是可變的,那麼在多執行緒環境下,就很容易造成不可預期的結果。而 String 是不可變的,就可以在多個執行緒之間共享,不需要同步處理。

因此,當我們呼叫 String 類的任何方法(比如說 trim()substring()toLowerCase())時,總會返回一個新的物件,而不影響之前的值。

String cmower = "張三,一枚有趣的程式設計師";
cmower.substring(0,4);
System.out.println(cmower);// 張三,一枚有趣的程式設計師

雖然呼叫 substring() 方法對 cmower 進行了擷取,但 cmower 的值沒有改變。

除了 String 類,包裝器類 Integer、Long 等也是不可變類。

03、手擼一個不可變類

一個不可變類,必須要滿足以下 4 個條件:

1)確保類是 final 的,不允許被其他類繼承。

2)確保所有的成員變數(欄位)是 final 的,這樣的話,它們就只能在構造方法中初始化值,並且不會在隨後被修改。

3)不要提供任何 setter 方法

4)如果要修改類的狀態,必須返回一個新的物件

按照以上條件,我們來自定義一個簡單的不可變類 Writer。

public final class Writer {
    private final String name;
    private final int age;

    public Writer(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }
}

Writer 類是 final 的,name 和 age 也是 final 的,沒有 setter 方法。

OK,據說這個作者分享了很多部落格,廣受讀者的喜愛,因此某某出版社找他寫了一本書(Book)。Book 類是這樣定義的:

public class Book {
    private String name;
    private int price;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
}

2 個欄位,分別是 name 和 price,以及 getter 和 setter,重寫後的 toString() 方法。然後,在 Writer 類中追加一個可變物件欄位 book。

public final class Writer {
    private final String name;
    private final int age;
    private final Book book;

    public Writer(String name, int age, Book book) {
        this.name = name;
        this.age = age;
        this.book = book;
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    public Book getBook() {
        return book;
    }
}

並在構造方法中追加了 Book 引數,以及 Book 的 getter 方法。

完成以上工作後,我們來新建一個測試類,看看 Writer 類的狀態是否真的不可變。

public class WriterDemo {
    public static void main(String[] args) {
        Book book = new Book();
        book.setName("CodeUpHub的 Java 進階之路");
        book.setPrice(79);

        Writer writer = new Writer("張三",18, book);
        System.out.println("定價:" + writer.getBook());
        writer.getBook().setPrice(59);
        System.out.println("促銷價:" + writer.getBook());
    }
}

程式輸出的結果如下所示:

定價:Book{name='CodeUpHub的 Java 進階之路', price=79}
促銷價:Book{name='CodeUpHub的 Java 進階之路', price=59}

糟糕,Writer 類的不可變性被破壞了,價格發生了變化。爲了解決這個問題,我們需要為不可變類的定義規則追加一條內容:

如果一個不可變類中包含了可變類的物件,那麼就需要確保返回的是可變物件的副本。也就是說,Writer 類中的 getBook() 方法應該修改爲:

public Book getBook() {
    Book clone = new Book();
    clone.setPrice(this.book.getPrice());
    clone.setName(this.book.getName());
    return clone;
}

這樣的話,構造方法初始化後的 Book 物件就不會再被修改了。此時,執行 WriterDemo,就會發現價格不再發生變化了。

定價:Book{name='CodeUpHub的 Java 進階之路', price=79}
促銷價:Book{name='CodeUpHub的 Java 進階之路', price=79}

04、總結

不可變類有很多優點,就像之前提到的 String 類那樣,尤其是在多執行緒環境下,它非常的安全。儘管每次修改都會建立一個新的物件,增加了記憶體的消耗,但這個缺點相比它帶來的優點,顯然是微不足道的——無非就是撿了西瓜,丟了芝麻。

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.