如果對象的狀態在構造后無法更改,則該對象是不可變的。不可變的對象不會讓其他對象修改其狀態。對象的字段在構造函數內部僅初始化一次,以后再也不會更改。
在本文中,我們將定義在JAVA中創建不可變類的典型步驟,并闡明開發人員在創建不可變類時遇到的常見錯誤。
1.不可變類的用法
如今,每個軟件應用程序的“必備”規范都將被分發和使用多線程。多線程應用程序總是使開發人員感到頭疼,因為要求開發人員保護其對象的狀態,以防止同時修改多個線程。同時,為此,開發人員通常在修改對象狀態時使用“ 同步”塊。
對于不可變的類,狀態永遠不會被修改。狀態的每次修改都會產生一個新實例,因此每個線程將使用不同的實例,并且開發人員不必擔心并發修改。
2.一些流行的不可變類
字符串 是Java中最流行的不可變類。初始化后,其值將無法修改。操作像修剪(),子串(),替換()總是返回一個新的實例,并不會影響當前的實例,這就是為什么我們通常所說的修剪() ,如下所示:
String alex = "Alex";
alex = alex.trim();
JDK的另一個示例是包裝器類,例如:Integer,Float,Boolean……這些類不會修改其狀態,但是,每次您嘗試對其進行修改時,它們都會創建一個新實例。
Integer a =3;
a += 3;
調用+ = 3之后,將創建一個新實例,其值保持為:6,并且第一個實例丟失。
3.我們如何創建一個不可變的類
為了創建一個不可變的類,您應該遵循以下步驟:
1.
將您的班級定為最終班,以便其他班級無法對其進行擴展。
2.
3. 將所有字段定為最終字段,以使它們在構造函數中僅初始化一次,之后再也不會修改。
4. 不要公開setter方法。
5. 在公開修改類狀態的方法時,必須始終返回該類的新實例。
6. 如果該類包含一個可變對象:
1. 在構造函數內部,請確保使用傳遞的參數的克隆副本,并且切勿將可變字段設置為通過構造函數傳遞的真實實例,這是為了防止傳遞對象的客戶端事后對其進行修改。
2. 確保始終返回該字段的副本,而不返回真實對象實例。
3.1簡單不可變類
讓我們按照上述步驟操作,并創建自己的不可變類(ImmutableStudent.java)。
package com.programmer.gate.beans;
public final class ImmutableStudent {
private final int id;
private final String name;
public ImmutableStudent(int id, String name) {
this.name = name;
this.id = id;
}
public int getId() {
return id;
public String getName() {
return name;
}
上面的類是一個非常簡單的不可變類,它不包含任何可變對象,并且從不以任何方式公開其字段;這些類型的類通常用于緩存。
3.2將可變對象傳遞給不可變類
現在,讓我們的示例復雜一點,我們創建一個名為Age的可變類,并將其作為字段添加到ImmutableStudent中:
public class Age {
private int day;
private int month;
private int year;
public int getDay() {
return day;
public void setDay(int day) {
this.day = day;
public int getMonth() {
return month;
public void setMonth(int month) {
this.month = month;
public int getYear() {
return year;
public void setYear(int year) {
this.year = year;
private final Age age;
public ImmutableStudent(int id, String name, Age age) {
this.name = name;
this.id = id;
this.age = age;
return id;
return name;
public Age getAge() {
return age;
因此,我們在不可變類中添加了類型為Age的新可變字段,并在構造函數中按常規分配了該字段。
讓我們創建一個簡單的測試類,并驗證ImmutableStudent是否不再不可變:
public static void main(String[] args) {
Age age = new Age();
age.setDay(1);
age.setMonth(1);
age.setYear(1992);
ImmutableStudent student = new ImmutableStudent(1, "Alex", age);
System.out.println("Alex age year before modification = " + student.getAge().getYear());
age.setYear(1993);
System.out.println("Alex age year after modification = " + student.getAge().getYear());
運行上面的測試后,我們得到以下輸出:
Alex age year before modification = 1992
Alex age year after modification = 1993
我們聲稱ImmutableStudent是一個不可變的類,其狀態在構造之后就不會修改,但是在上面的示例中,即使在構造Alex對象之后,我們也能夠修改Alex的年齡。如果我們返回ImmutableStudent構造函數的實現,則會發現age字段已分配給Age參數的實例,因此,只要在類外部修改了引用的Age,該更改就會直接反映在Alex的狀態上。請查看我的“ 按價值傳遞”或“按參考傳遞”文章,以更深入地了解此概念。
為了解決這個問題并使我們的類再次變得不可變,我們遵循上面提到的創建不可變類的步驟中的步驟5。因此,我們修改了構造函數,以克隆傳遞的Age參數并使用其實例。
public ImmutableStudent(int id, String name, Age age) {
Age cloneAge = new Age();
cloneAge.setDay(age.getDay());
cloneAge.setMonth(age.getMonth());
cloneAge.setYear(age.getYear());
this.age = cloneAge;
現在,如果運行測試,將得到以下輸出:
Alex age year after modification = 1992
正如您現在所看到的,Alex的年齡在構建之后再也不會受到影響,我們的班級又回到了不變的狀態。
3.3從不可變類返回可變對象
但是,我們的類仍然有泄漏,并且不是完全不變的,讓我們采取以下測試方案:
student.getAge().setYear(1993);
輸出:
再次根據步驟4,從不可變對象返回可變字段時,您應該返回它們的克隆實例,而不是該字段的真實實例。
因此,我們修改了getAge()以便返回該對象年齡的副本:
public Age getAge() {
cloneAge.setDay(this.age.getDay());
cloneAge.setMonth(this.age.getMonth());
cloneAge.setYear(this.age.getYear());
return cloneAge;
現在,該類變得完全不可變,并且沒有為其他對象提供任何方法或方法來修改其狀態。
4.結論
不可變的類具有很多優點,尤其是在多線程環境中正確使用時。唯一的缺點是它們比傳統類消耗更多的內存,因為每次對其進行修改后,都會在內存中創建一個新對象...但是,與這些類提供的優點相比,開發人員不應高估內存消耗,因為它可以忽略不計類的類型。
最后,如果一個對象僅能向其他對象呈現一種狀態,則無論它們如何以及何時調用其方法,該對象都是不可變的。如果是這樣,則通過任何線程安全的定義都是線程安全的。