如何通过可变引用维护类的不可变性

2022-09-02 23:59:41

我知道使我们的类不可变的所有基本规则,但是当有另一个类引用时,我有点困惑。我知道如果有集合而不是集合,那么我们可以利用,然后我们可以使我们的类不可变。但在以下情况下,我仍然无法理解这个概念。AddressCollections.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;
    }
}

答案 1

好吧,这个概念是阅读JLS并理解它。JLS的第17章“线程和锁”描述了内存可见性和同步。第 17.5 节 “最终字段语义” 描述了最终字段的内存可见性语义。该部分部分说:

final 字段还允许程序员在没有同步的情况下实现线程安全的不可变对象。线程安全的不可变对象被所有线程视为不可变对象,即使使用数据争用在线程之间传递对不可变对象的引用也是如此。这可以提供安全保证,防止不正确或恶意代码滥用不可变类。必须正确使用 final 字段以提供不可变性的保证。

最终字段的使用模型很简单:在对象的构造函数中设置对象的最终字段;并且不要在对象的构造函数完成之前,在另一个线程可以看到它的位置写入对正在构造的对象的引用。如果遵循此命令,则当另一个线程看到该对象时,该线程将始终看到该对象的最终字段的正确构造版本。它还将看到由最终字段引用的任何对象或数组的版本,这些字段至少与最终字段一样最新。

因此,您需要:

  1. 使最终和私有。address
  2. 对于任何可变对象,必须防止在外部看到对该对象的引用。

在这种情况下,#2 可能意味着您无法像 返回对 地址的引用。你必须在构造函数中制作一个防御性副本。即,制作任何可变参数的副本,并将副本存储在 Employee 中。如果你不能制作一个防御性的副本,真的没有办法让员工不可变。getAddress()

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() );
    }
    public int getId(){
        return id;
    }
    public Address getAddress() {
        Address nuAdd = new Address(); // must copy here too
        nuAdd.setStreet( address.getStreet() );
        return nuAdd;
}

实现或类似的东西(复制ctor)将使复杂类更容易创建防御对象。但是,我认为最好的建议是使不可变。一旦你这样做了,你可以自由地传递它的引用,而没有任何线程安全问题。clone()Address

在此示例中,请注意,我不必复制 的值。 是一个字符串,字符串是不可变的。如果由可变字段(例如整数街道号)组成,那么我将不得不无限期地复制,依此类推。这就是为什么不可变对象如此有价值,它们打破了“无限复制”链。streetStreetstreetstreet

由于这个问题越来越受欢迎,我还应该提到Brian Goetz的书,Java Concurrency in Practice,这就是我学习这些技术的方式,我基本上是在上面转述这本书。


答案 2

好吧,Java文档提供了步骤

定义不可变对象的策略

以下规则定义了用于创建不可变对象的简单策略。并非所有记录为“不可变”的类都遵循这些规则。这并不一定意味着这些类的创建者是草率的 - 他们可能有充分的理由相信它们的类的实例在构造后永远不会改变。但是,这些策略需要复杂的分析,不适合初学者。

  • 不要提供“setter”方法,即修改字段或字段引用的对象的方法。
  • 使所有字段成为最终字段并设为私有字段。
  • 不允许子类重写方法。执行此操作的最简单方法是将类声明为 final。更复杂的方法是使构造函数私有并在工厂方法中构造实例。
  • 如果实例字段包含对可变对象的引用,则不允许更改这些对象:
    • 不要提供修改可变对象的方法。
    • 不要共享对可变对象的引用。切勿存储对传递给构造函数的外部可变对象的引用;如有必要,请创建副本,并存储对副本的引用。同样,必要时创建内部可变对象的副本,以避免在方法中返回原始文件。

地址类是可变的,因为您可以使用 setStreet 方法对其进行修改。因此,其他类可以修改此类。

我们可以通过在传入地址实例时获取其副本来防止这种情况,而不是信任对我们给定的实例的引用。

使地址对象成为最终对象

private final Address address;

其次

this.address = new Address(address.getStreet());

在地址类中创建设置街道的构造函数。删除街道的 setter 方法。

最后,而不是

public Address getAddress(){
    return address;
} 

public Address getAddress(){
    return new Address(address.getStreet());
}