簡體   English   中英

為什么要在 Java 中聲明一個不可變的類 final?

[英]Why would one declare an immutable class final in Java?

我讀到要在 Java 中使類不可變,我們應該執行以下操作,

  1. 不提供任何 setter
  2. 將所有字段標記為私有
  3. 使課程最終

為什么需要第 3 步? 為什么我要把班級標記為final

如果您不將 class 標記為final ,我可能會突然使您看似不可變的類實際上是可變的。 例如,考慮以下代碼:

public class Immutable {
     private final int value;

     public Immutable(int value) {
         this.value = value;
     }

     public int getValue() {
         return value;
     }
}

現在,假設我執行以下操作:

public class Mutable extends Immutable {
     private int realValue;

     public Mutable(int value) {
         super(value);

         realValue = value;
     }

     public int getValue() {
         return realValue;
     }
     public void setValue(int newValue) {
         realValue = newValue;
     }

    public static void main(String[] arg){
        Mutable obj = new Mutable(4);
        Immutable immObj = (Immutable)obj;              
        System.out.println(immObj.getValue());
        obj.setValue(8);
        System.out.println(immObj.getValue());
    }
}

請注意,在我的Mutable子類中,我覆蓋了getValue的行為以讀取在我的子類中聲明的新的可變字段。 因此,最初看起來不可變的類實際上並不是不可變的。 我可以在任何需要Immutable對象的地方傳遞這個Mutable對象,假設該對象是真正不可變的,這可能會對代碼造成非常糟糕的事情。 將基類標記為final可以防止這種情況發生。

希望這可以幫助!

相反的是,許多人認為,做一個不變類final不需要的。

使不可變類成為final的標准論點是,如果您不這樣做,則子類可以添加可變性,從而違反超類的約定。 該類的客戶將假定不變性,但當某些東西從他們之下發生變異時會感到驚訝。

如果你把這個論點帶到它的邏輯極端,那么所有方法都應該是final ,否則子類可能會以不符合其超類契約的方式覆蓋方法。 有趣的是,大多數 Java 程序員認為這是荒謬的,但在某種程度上同意不可變類應該是final的想法。 我懷疑這與 Java 程序員一般不完全接受不變性概念有關,也許與 Java 中final關鍵字的多重含義有關的某種模糊思維有關。

遵守超類的契約不是編譯器可以或應該始終強制執行的。 編譯器可以強制執行合同的某些方面(例如:最少的一組方法及其類型簽名),但是編譯器無法強制執行典型合同的許多部分。

不變性是類契約的一部分。 它與人們更習慣的一些事情有點不同,因為它說明了類(和所有子類)不能做什么,而我認為大多數 Java(以及通常的 OOP)程序員傾向於將契約視為與一個類可以做什么,而不是它不能做什么。

不變性不僅影響單個方法——它影響整個實例——但這與 Java 中的equalshashCode的工作方式並沒有太大不同。 這兩種方法在Object有一個特定的契約。 這份合約非常仔細地列出了這些方法無法做到的事情。 這個契約在子類中更加具體。 以違反合同的方式覆蓋equalshashCode非常容易。 事實上,如果您只覆蓋這兩種方法中的一種而沒有另一種,則您很可能違反了合同。 那么是否應該在Object中將equalshashCode聲明為final以避免這種情況? 我想大多數人會爭辯說他們不應該。 同樣,沒有必要將不可變類設為final

也就是說,您的大多數類,無論是否不可變,都應該final 請參閱Effective Java Second Edition Item 17:“設計和文檔繼承或禁止它”。

因此,步驟 3 的正確版本應該是:“使類成為最終的,或者在為子類進行設計時,明確記錄所有子類必須繼續保持不變。”

不要標記整個班級的期末考試。

正如其他一些答案中所述,允許擴展不可變類是有正當理由的,因此將類標記為 final 並不總是一個好主意。

最好將您的屬性標記為私有和最終屬性,如果您想保護“合同”,請將您的 getter 標記為最終屬性。

通過這種方式,您可以允許擴展類(是的,甚至可能是可變類),但是類的不可變方面受到保護。 屬性是私有的,不能被訪問,這些屬性的 getter 是最終的,不能被覆蓋。

任何使用不可變類實例的其他代碼都將能夠依賴類的不可變方面,即使它傳遞的子類在其他方面是可變的。 當然,因為它需要你的類的一個實例,它甚至不知道這些其他方面。

如果它不是最終的,那么任何人都可以擴展該類並做他們喜歡的任何事情,例如提供 setter、隱藏您的私有變量以及基本上使其可變。

這限制了擴展您的類的其他類。

final 類不能被其他類擴展。

如果一個類擴展了您要使其成為不可變的類,則由於繼承原則,它可能會更改類的狀態。

只需澄清“它可能會改變”。 子類可以覆蓋超類行為,例如使用方法覆蓋(例如 templatetypedef/Ted Hop 答案)

如果你不讓它成為最終的,我可以擴展它並使它不可變。

public class Immutable {
  privat final int val;
  public Immutable(int val) {
    this.val = val;
  }

  public int getVal() {
    return val;
  }
}

public class FakeImmutable extends Immutable {
  privat int val2;
  public FakeImmutable(int val) {
    super(val);
  }

  public int getVal() {
    return val2;
  }

  public void setVal(int val2) {
    this.val2 = val2;
  }
}

現在,我可以將 FakeImmutable 傳遞給任何需要 Immutable 的類,並且它不會像預期的契約那樣運行。

為了創建一個不可變的類,將類標記為final不是強制性的。

讓我從標准庫本身中舉一個這樣的例子: BigInteger是不可變的,但它不是final

實際上,不變性是一個概念,根據該概念,對象一旦創建就無法修改。

讓我們從 JVM 的角度來思考。 從JVM的角度來看,這個類的對象應該在任何線程可以訪問它之前完全構造,並且對象的狀態在構造之后不應該改變。

不變性意味着一旦創建對象就無法更改其狀態,這是通過三個拇指規則實現的,這些規則使編譯器認識到類是不可變的,它們如下:

  • 所有非private字段都應該是final
  • 確保類中沒有可以直接或間接更改對象字段的方法
  • 類中定義的任何對象引用都不能從類外部修改

有關更多信息,請參閱此 URL

假設您有以下課程:

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class PaymentImmutable {
    private final Long id;
    private final List<String> details;
    private final Date paymentDate;
    private final String notes;

    public PaymentImmutable (Long id, List<String> details, Date paymentDate, String notes) {
        this.id = id;
        this.notes = notes;
        this.paymentDate = paymentDate == null ? null : new Date(paymentDate.getTime());
        if (details != null) {
            this.details = new ArrayList<String>();

            for(String d : details) {
                this.details.add(d);
            }
        } else {
            this.details = null;
        }
    }

    public Long getId() {
        return this.id;
    }

    public List<String> getDetails() {
        if(this.details != null) {
            List<String> detailsForOutside = new ArrayList<String>();
            for(String d: this.details) {
                detailsForOutside.add(d);
            }
            return detailsForOutside;
        } else {
            return null;
        }
    }

}

然后你擴展它並打破它的不變性。

public class PaymentChild extends PaymentImmutable {
    private List<String> temp;
    public PaymentChild(Long id, List<String> details, Date paymentDate, String notes) {
        super(id, details, paymentDate, notes);
        this.temp = details;
    }

    @Override
    public List<String> getDetails() {
        return temp;
    }
}

下面我們測試一下:

public class Demo {

    public static void main(String[] args) {
        List<String> details = new ArrayList<>();
        details.add("a");
        details.add("b");
        PaymentImmutable immutableParent = new PaymentImmutable(1L, details, new Date(), "notes");
        PaymentImmutable notImmutableChild = new PaymentChild(1L, details, new Date(), "notes");

        details.add("some value");
        System.out.println(immutableParent.getDetails());
        System.out.println(notImmutableChild.getDetails());
    }
}

輸出結果將是:

[a, b]
[a, b, some value]

正如您所看到的,雖然原始類保持其不變性,但子類可以是可變的。 因此,在您的設計中,您無法確定您使用的對象是不可變的,除非您將類設為 final。

假設以下課程不是final

public class Foo {
    private int mThing;
    public Foo(int thing) {
        mThing = thing;
    }
    public int doSomething() { /* doesn't change mThing */ }
}

它顯然是不可變的,因為即使是子類也不能修改mThing 但是,子類可以是可變的:

public class Bar extends Foo {
    private int mValue;
    public Bar(int thing, int value) {
        super(thing);
        mValue = value;
    }
    public int getValue() { return mValue; }
    public void setValue(int value) { mValue = value; }
}

現在,可分配給Foo類型變量的對象不再保證是可變的。 這可能會導致散列、相等、並發等問題。

設計本身沒有價值。 設計總是用來實現一個目標。 這里的目標是什么? 我們想減少代碼中的意外數量嗎? 我們想防止錯誤嗎? 我們是否盲目地遵守規則?

此外,設計總是要付出代價的。 每一個名副其實的設計都意味着你有一個目標沖突

考慮到這一點,您需要找到以下問題的答案:

  1. 這將阻止多少明顯的錯誤?
  2. 這將阻止多少微妙的錯誤?
  3. 這多久會使其他代碼更復雜(= 更容易出錯)?
  4. 這會使測試更容易還是更難?
  5. 您項目中的開發人員有多好? 他們需要多少大錘指導?

假設您的團隊中有許多初級開發人員。 他們會拼命嘗試任何愚蠢的事情,因為他們還不知道解決問題的好方法。 使類成為 final 可以防止錯誤(好的),但也可以使他們想出“聰明”的解決方案,例如將所有這些類復制到代碼中隨處可見的可變類中。

另一方面,在任何地方都使用它之后,很難使一個類成為final ,但是如果您發現需要擴展它,則很容易使final類成為非final類。

如果您正確使用接口,則可以通過始終使用接口,然后在需要時添加可變實現來避免“我需要使此可變”問題。

結論:此答案沒有“最佳”解決方案。 這取決於您願意支付的價格以及您必須支付的價格。

equals() 的默認含義與引用相等相同。 對於不可變數據類型,這幾乎總是錯誤的。 因此,您必須覆蓋 equals() 方法,將其替換為您自己的實現。 關聯

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM