我们如何通过可变引用来维护类的不变性
我知道使我们的类不可变的所有基本规则但是当有另一个类引用时我有点困惑。 我知道如果有集合而不是Address
那么我们可以使用Collections.unmodifiableList(new ArrayList(modifiable));
然后我们可以使我们的类不可变。 但在下面的情况下,我仍然无法得到这个概念。
public final class Employee{ private final int id; private Address address; public Employee(int id, Address address) { this.id = id; this.address=address; } public int getId(){ return id; } public Address getAddress(){ return address; } } public class Address{ private String street; public String getStreet(){ return street; } public void setStreet(String street){ this.street = street; } }
好吧,这个概念是阅读JLS并理解它。 在这种情况下,JLS说:
final字段还允许程序员在没有同步的情况下实现线程安全的不可变对象。 线程安全的不可变对象被所有线程视为不可变对象,即使使用数据争用传递线程之间的不可变对象的引用也是如此。 这可以提供安全保证,防止错误或恶意代码滥用不可变类。 必须正确使用最终字段以提供不可变性的保证。
final字段的用法模型很简单:在该对象的构造函数中设置对象的最终字段; 并且在对象的构造函数完成之前,不要在另一个线程可以看到的地方写入对正在构造的对象的引用。 如果遵循此操作,那么当另一个线程看到该对象时,该线程将始终看到该对象的最终字段的正确构造版本。 它还将看到那些最终字段引用的任何对象或数组的版本,这些字段至少与最终字段一样是最新的。
所以你需要:
- 将
address
设为最终和私人。 - 对于任何可变对象,必须防止从外部看到对该对象的引用。
在这种情况下,#2可能意味着您不能像使用getAddress()
那样返回对Address的引用。 而且你必须在构造函数中制作一个防御性副本。 即,复制任何可变参数,并将副本存储在Employee中。 如果你不能制作防御性副本,那么就没有办法使员工不可变。
public final class Employee{ private final int id; private final Address address; public Employee(int id, Address address) { this.id = id; this.address=new Address(); // defensive copy this.address.setStreet( address.getStreet() ); } pulbic int getId(){ return id; } public Address getAddress() { Address nuAdd = new Address(); // must copy here too nuAdd.setStreet( address.getStreet() ); return nuAdd; }
实现clone()
或类似的东西(复制ctor)将使复杂类更容易创建防御对象。 但是,我认为最好的建议是使Address
不可变。 一旦你这样做,你可以自由地传递它的参考,没有任何线程安全问题。
在这个例子中,请注意我不必复制street
的值。 Street
是一个String,字符串是不可变的。 如果street
由可变字段(例如整数街道号码)组成,那么我也必须制作street
副本,等等无限制地。 这就是为什么不可变对象如此有价值,它们打破了“无限复制”链。
那么Java文档提供了一些步骤
一种定义不可变对象的策略
以下规则定义了用于创建不可变对象的简单策略。 并非所有记录为“不可变”的类都遵循这些规则。 这并不一定意味着这些课程的创造者是草率的 – 他们可能有充分的理由相信他们的课程实例在建造后永远不会改变。 但是,这种策略需要复杂的分析,不适合初学者。
- 不要提供“setter”方法 – 修改字段引用的字段或对象的方法。
- 使所有字段成为最终和私有。
- 不允许子类重写方法。 最简单的方法是将类声明为final。 更复杂的方法是使构造函数私有并在工厂方法中构造实例。
- 如果实例字段包含对可变对象的引用,则不允许更改这些对象:
- 不要提供修改可变对象的方法。
- 不要共享对可变对象的引用。 永远不要存储对传递给构造函数的外部可变对象的引用; 如有必要,创建副本并存储对副本的引用。 同样,必要时创建内部可变对象的副本,以避免在方法中返回原始对象。
地址类是可变的,因为您可以使用setStreet方法对其进行修改。 所以其他类可以修改这个类。
我们可以通过传入Address实例的副本来防止这种情况,而不是信任对我们给出的实例的引用。
使Address对象最终
private final Address address;
其次,
this.address = new Address(address.getStreet());
在Address类中创建构造函数,为Street设置Street.Remove setter方法。
最后代替
public Address getAddress(){ return address; }
使用
public Address getAddress(){ return new Address(address.getStreet()); }
如果要将可变对象封装到不可变对象中 ,则需要:
- 创建可变对象的副本 (即通过复制构造函数,克隆,序列化/反序列化等); 永远不会存储对原始可变对象的引用。
- 永远不要返回可变对象。 如果必须,则返回该对象的副本 。
- 避免使用可以改变可变对象的方法。
public Employee(int id,Address address){
this.id = id; this.address=new Address(); this.address.setStreet( address.getStreet() ); } public Address getAddress() { Address nuAdd = new Address(); // must copy here too nuAdd.setStreet( address.getStreet() ); return nuAdd; }
因此在您的示例中, Employee
类是不可变的,因为一旦创建它,您就无法更改其状态,因为它只有getter方法。
Address
类是mutable
因为您可以使用setStreet
方法对其进行修改。
因此,如果您有其他使用Address
对象的类,则可以确定该类无法修改对象状态。
您还可以使用克隆使用浅拷贝
public final class Employee{ private final int id; private Address address; public Employee(int id, Address address) { this.id = id; this.address=address.clone(); } public int getId(){ return id; } public Address getAddress(){ return address.clone(); } }
使用它将在Employee类中创建一个单独的Address对象,因此在这种情况下,对作为Employee构造函数中的参数传递的Address对象所做的任何更改都不会更改Employee类的成员变量Address对象。
getAddress()方法也返回一个克隆对象,因此对此方法提取的对象所做的任何更改都不会影响Employee类的地址对象。
注意:要使用此地址类Cloneable。