我读到要使 Java 中的类不可变,我们应该执行以下操作,
- 不提供任何设置器
- 将所有字段标记为私有
- 使课程最终
为什么需要第 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
读取子类中声明的新的可变字段的行为。结果,您的类最初看起来是不可变的,但实际上并不是不可变的。我可以将这个Mutable
对象传递到任何需要对象的地方Immutable
,假设该对象是真正不可变的,这可能会对代码造成非常糟糕的事情。标记基类final
可以防止这种情况发生。
希望这可以帮助!
与许多人认为的相反,不需要创建一个不可变的final
类。
制作不可变类的标准论点final
是,如果你不这样做,那么子类可以增加可变性,从而违反了超类的契约。该类的客户将假定不变性,但当某些东西从他们下面发生变异时会感到惊讶。
如果你把这个论点带到它的逻辑极端,那么所有的方法都应该被创建final
,否则一个子类可能会以一种不符合其超类契约的方式覆盖一个方法。有趣的是,大多数 Java 程序员认为这很荒谬,但对不可变类应该是final
. final
我怀疑这与 Java 程序员一般不完全接受不变性的概念有关,也许与Java 中关键字的多重含义有关的某种模糊思维有关。
遵守超类的合同并不是编译器可以或应该始终强制执行的事情。编译器可以强制执行合同的某些方面(例如:最小的方法集及其类型签名),但是编译器无法强制执行典型合同的许多部分。
不变性是类契约的一部分。它与人们更习惯的一些事情有点不同,因为它说明了类(和所有子类)不能做什么,而我认为大多数 Java(通常是 OOP)程序员倾向于将合同视为与一个类可以做什么,而不是它不能做什么。
不变性不仅影响单个方法——它影响整个实例——但这与Java 的工作方式equals
和工作方式并没有太大的不同。hashCode
这两种方法在Object
. 这份合同非常仔细地列出了这些方法不能做的事情。该合同在子类中更加具体。它很容易被覆盖equals
或hashCode
以违反合同的方式。事实上,如果您只覆盖这两种方法中的一种而没有另一种,那么您很可能违反了合同。那么应该equals
并且hashCode
已经被宣布final
以Object
避免这种情况?我想大多数人会争辩说他们不应该。同样,没有必要创建不可变类final
.
也就是说,您的大多数类,无论是否不可变,都可能应该是final
. 请参阅Effective Java Second Edition第 17 条:“设计和记录继承或禁止它”。
因此,第 3 步的正确版本应该是:“使类成为最终类,或者在设计子类时,清楚地记录所有子类必须继续是不可变的。”
不要将整个班级标记为最终。
如其他一些答案中所述,允许扩展不可变类是有正当理由的,因此将类标记为最终类并不总是一个好主意。
最好将您的属性标记为私有和最终的,如果您想保护“合同”,请将您的吸气剂标记为最终的。
通过这种方式,您可以允许扩展类(甚至可能通过可变类),但是您的类的不可变方面受到保护。属性是私有的,不能被访问,这些属性的 getter 是最终的,不能被覆盖。
任何其他使用不可变类实例的代码都将能够依赖类的不可变方面,即使它传递的子类在其他方面是可变的。当然,由于它需要你的类的一个实例,它甚至不会知道这些其他方面。
如果它不是最终的,那么任何人都可以扩展该类并做任何他们喜欢的事情,例如提供设置器、隐藏您的私有变量以及基本上使其可变。
这限制了其他类扩展你的类。
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
如果您正确使用接口,则可以通过始终使用接口,然后在需要时添加可变实现来避免“我需要使这个可变”问题。
结论:这个答案没有“最佳”解决方案。这取决于你愿意付出什么样的代价以及你必须付出什么样的代价。
equals() 的默认含义与引用相等相同。对于不可变数据类型,这几乎总是错误的。所以你必须重写 equals() 方法,用你自己的实现替换它。关联