我读到要使Java中的类不可变,我们应该执行以下操作:
- 不提供任何二传手
- 将所有字段标记为私有
- 最终上课
为什么需要步骤3?我为什么要上课final
呢?
BigInteger
,则不知道它是否是不变的。这是一个混乱的设计。/java.io.File
是一个更有趣的示例。
File
之所以有趣是因为它很可能是不可变的,因此会导致TOCTOU(检查时间/使用时间)攻击。受信任的代码检查File
路径是否可接受,然后使用该路径。子类File
可以在检查和使用之间更改值。
Make the class final
或确保所有子类都是不可变的
Answers:
如果您不标记该类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
可以防止这种情况的发生。
希望这可以帮助!
Immutable
参数的函数。我可以将一个Mutable
对象传递给该函数,因为Mutable extends Immutable
。在该函数内部,虽然您认为对象是不可变的,但我可以拥有一个辅助线程,该线程可以在函数运行时更改其值。我还可以给您一个Mutable
函数存储的对象,然后稍后在外部更改其值。换句话说,如果您的函数假定该值是不可变的,则它很容易中断,因为我可以给您一个可变的对象并在以后更改它。那有意义吗?
Mutable
对象的方法不会更改对象。令人担忧的是,该方法可能假设对象实际上不是不可变的。例如,该方法可能假定,因为它认为对象是不可变的,因此可以将其用作a中的键HashMap
。然后,我可以传入一个Mutable
,以等待该函数将对象存储为键,然后更改该Mutable
对象,从而破坏该功能。现在,HashMap
由于我更改了与对象关联的键,因此查找将失败。那有意义吗?
相反的是,许多人认为,做一个不变类final
是不必需的。
制作不可变类的标准论点final
是,如果不这样做,则子类会增加可变性,从而违反了超类的约定。班级的客户将承担不变性,但是当某些东西从他们下面变出来时,他们会感到惊讶。
如果将此参数推至逻辑极限,则应采用所有方法final
,否则子类可能会以不符合其超类协定的方式覆盖方法。有趣的是,大多数Java程序员都认为这很荒谬,但是对于不可变的类应该是的想法还是可以接受的final
。我怀疑这与Java程序员有关,总的来说,它并不完全与不变性的概念有关,也许与final
Java关键字的多种含义有关的某种模糊思维。
符合您的超类的约定不是编译器可以或应该始终执行的。编译器可以执行合同的某些方面(例如:最少的一组方法及其类型签名),但是编译器无法执行典型合同的许多部分。
不变性是集体合同的一部分。这与人们更习惯的某些操作有所不同,因为它说明了类(以及所有子类)不能做什么,而我认为大多数Java(通常是OOP)程序员倾向于将合同与一个班级可以做什么,而不是它不能做什么。
不变性不仅会影响单个方法,而且会影响整个实例,但这equals
与hashCode
Java的工作方式和方式并没有太大不同。这两种方法在中都有特定的合同Object
。该合同非常仔细地列出了这些方法无法完成的工作。该合同在子类中更加具体。这很容易被覆盖equals
或hashCode
违反合同。实际上,如果您仅覆盖这两种方法中的一种而没有另一种,则很可能违反了合同。所以应该equals
和hashCode
已经宣布final
在Object
避免这种情况?我认为大多数人会认为他们不应该这样做。同样,也不必制作不可变的类final
。
也就是说,您的大多数类(无论是否不变)都应该是final
。请参阅有效的Java第二版项目17:“设计和记录继承,否则禁止继承”。
因此,第3步的正确版本是:“将类定型,或者在设计子类时,明确记录所有子类必须继续不可变。”
X
和Y
,其值X.equals(Y)
将是不可变的(只要X
并Y
继续引用相同的对象)。对于哈希码,它也有类似的期望。显然,没有人应该期望编译器强制实现等价关系和哈希码的不变性(因为它不能做到)。我认为人们没有理由期望该类型的其他方面会强制实施它。
double
。一个人可能会派生一个GeneralImmutableMatrix
使用数组作为后备存储的a,但也可能有一个ImmutableDiagonalMatrix
例子,它仅沿对角线存储一个项目数组(读取项X,Y如果X == y将产生Arr [X],否则返回零) 。
final
限制其用途,尤其是在设计要扩展的API时。为了线程安全,使您的类尽可能不变,但是保持其可扩展性是有意义的。您可以将字段设置为protected final
而不是private final
。然后,在子文档中显式地建立关于遵守不变性和线程安全性保证的子类的契约。
final
。仅仅因为equals
并且hashcode
不可能是最终的,并不意味着我可以容忍不可变的类,除非我有正当的理由,在这种情况下,我可以使用文档或测试作为防御线。
不要将整个班级都标记为决赛。
有充分的理由允许如其他答案中所述扩展不可变的类,因此将类标记为final并非总是一个好主意。
最好将属性标记为私有和最终标记,如果您想保护“合同”,则将获取者标记为最终标记。
这样,您可以允许扩展类(即使是可变的类也可以),但是可以保护类的不变性。属性是私有的,无法访问,这些属性的吸气剂是最终的,不能被覆盖。
使用您的不可变类实例的任何其他代码都将能够依赖于类的不可变方面,即使所传递的子类在其他方面是可变的。当然,由于它需要一个类的实例,所以甚至不知道这些其他方面。
如果不是最终的,那么任何人都可以扩展该类并做他们想做的任何事情,例如提供设置器,隐藏私有变量并基本上使其可变。
这限制了其他类的扩展。
最终课程不能被其他课程扩展。
如果一个类将您要使其变为不可变的类进行扩展,则由于继承原则,它可能会更改该类的状态。
请澄清“它可能会改变”。子类可以覆盖超类行为,例如使用方法覆盖(例如templatetypedef / Ted Hop答案)
final
,否则我看不出这个答案有什么帮助。
如果您未将其定型,则可以对其进行扩展并使其不可变。
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传递给任何期望不可变的类,并且它不会表现为期望的契约。
对于创建不可变的类,并非强制性地将该类标记为最终类。
让我从Java类本身中获取一个这样的例子。“ BigInteger”类是不可变的,但不是最终的。
实际上,不变性是一个概念,根据该概念,创建的对象不能被修改。
让我们从JVM的角度考虑,从JVM的角度来看,所有线程都必须共享对象的相同副本,并且在任何线程访问它之前,它都已完全构建,并且对象的状态在其构建后不会改变。
不变性意味着一旦创建了对象就无法更改其状态,这是通过三个经验法则实现的,这三个规则使编译器认识到类是不变的,它们如下:
有关更多信息,请参见下面的URL
http://javaunturnedtopics.blogspot.in/2016/07/can-we-create-immutable-class-without.html
假设您有以下课程:
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
:
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
。
如果正确使用接口,则可以通过始终使用接口,然后在需要时添加可变实现,来避免出现“我需要使此可变”问题。
结论:此答案没有“最佳”解决方案。这取决于您愿意支付的价格以及必须支付的价格。
java.math.BigInteger
class是示例,它的值是不可变的,但不是最终值。