![](/img/trans.png)
[英]Why would I want to declare a field as final when creating an immutable class without setter
[英]Why would one declare an immutable class final in Java?
如果您不將 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 中的equals
和hashCode
的工作方式並沒有太大不同。 這兩種方法在Object
有一個特定的契約。 這份合約非常仔細地列出了這些方法無法做到的事情。 這個契約在子類中更加具體。 以違反合同的方式覆蓋equals
或hashCode
非常容易。 事實上,如果您只覆蓋這兩種方法中的一種而沒有另一種,則您很可能違反了合同。 那么是否應該在Object
中將equals
和hashCode
聲明為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
類型變量的對象不再保證是可變的。 這可能會導致散列、相等、並發等問題。
設計本身沒有價值。 設計總是用來實現一個目標。 這里的目標是什么? 我們想減少代碼中的意外數量嗎? 我們想防止錯誤嗎? 我們是否盲目地遵守規則?
此外,設計總是要付出代價的。 每一個名副其實的設計都意味着你有一個目標沖突。
考慮到這一點,您需要找到以下問題的答案:
假設您的團隊中有許多初級開發人員。 他們會拼命嘗試任何愚蠢的事情,因為他們還不知道解決問題的好方法。 使類成為 final 可以防止錯誤(好的),但也可以使他們想出“聰明”的解決方案,例如將所有這些類復制到代碼中隨處可見的可變類中。
另一方面,在任何地方都使用它之后,很難使一個類成為final
,但是如果您發現需要擴展它,則很容易使final
類成為非final
類。
如果您正確使用接口,則可以通過始終使用接口,然后在需要時添加可變實現來避免“我需要使此可變”問題。
結論:此答案沒有“最佳”解決方案。 這取決於您願意支付的價格以及您必須支付的價格。
equals() 的默認含義與引用相等相同。 對於不可變數據類型,這幾乎總是錯誤的。 因此,您必須覆蓋 equals() 方法,將其替換為您自己的實現。 關聯
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.