如何安全地发布可变对象?

How to safely publish a mutable object?

我不想做深拷贝的方式

比如说,我有一个可变类型的字段,例如 x,y,z 坐标。偶尔,我需要把这个领域暴露给一些观众。我希望它是只读的。我记得读过类似包装纸的东西来做这些事情,但我不记得细节了。

x,y,z 坐标示例可能过于简单,因为 x,y,z 是基本类型。所以 getX() 总是 return 一个副本。

我想要一个通用的解决方案,即使 x、y、z 字段是另一种可变类型。

有人可以帮忙吗?


编辑:

public class Client
{

    public static final Holder holder = new Holder();


    public static void main(String[] args)
    {

        UserWrapper user = holder.getUser();
        System.out.println(user);               //UserWrapper{user=User{address=Address{street='street 101'}}}
        user.getAddress().setStreet("mars");    //UserWrapper{user=User{address=Address{street='mars'}}}
        System.out.println(user);

    }
}

public class Holder
{

    private User user;

    public Holder()
    {
        user = new User();
        Address address = new Address();
        address.setStreet("street 101");
        user.setAddress(address);
    }

    public UserWrapper getUser()
    {
        return new UserWrapper(user);
    }

}

public class User
{

    private Address address;

    public Address getAddress()
    {
        return address;
    }

    public void setAddress(Address address)
    {
        this.address = address;
    }
}

public class UserWrapper
{

    private User user;

    public UserWrapper(User user)
    {
        this.user = user;
    }

    public Address getAddress()
    {
        return user.getAddress();
    }
}

编辑: 感谢 I don't know who(他删除了答案),我发现 this link 他在原文 post 中提到的很有帮助。

Java 中没有可让您执行此操作的内置机制。通常,如果你移动实例,你会:

  1. 使用不可变对象
  2. 传递对象的副本

由于您want/can不选择这两种方式中的任何一种,因此您需要使用其他方式。根据您的要求和您的 class 结构的复杂程度,有很多不同的方法可以实现这一点,但一般的方法是发布一个不可变的包装器而不是原始包装器。
以下是一些示例:

public class XYZ {
    public int x, y, z;
}

public class XYZWrapper {
    private XYZ xyz;

    public XYZWrapper(XYZ xyz) {
        this.xyz = xyz;
    }

    public int getX() { return x; }
    public int getY() { return y; }
    public int getZ() { return z; }
}

public class Address {
    public String name;
    public XYZ xyz;
}

public class AddressWrapper {
    private String name; // Note that this could be public since any String is immutable
    private XYZWrapper xyzWrapper;

    public AddressWrapper(String name, XYZ xyz) {
        this.name = name;
        this.xyzWrapper = new XYZWrapper(xyz);
    }

    public String getName() { 
        return name;
    }

    public XYZWrapper getXYZWrapper() { 
        return xyzWrapper;
    }
}

现在,如果您使用接口而不是 XYZAddress class,您可以有 2 个实现(例如 XYZMutableXYZImmutable) 这将允许您抽象出您正在 return 的 class 类型,并且还使您能够从 XYZMutable 的实例创建 XYZImmutable 的实例(假设接口只定义了 & 所有 getter 方法)。

关于此方法的更多注意事项(尤其是如果您使用接口以首选方式执行此操作):即使您有一个复杂的 class 层次结构,您也可以通过创建一个生成器 class 接收接口实例、可变实现实例和 returns 不可变实现实例作为 return 值。

传统方式:

  • 深拷贝 - 防止突变影响正在阅读的客户
  • 不可变对象 - 不是为客户端复制,而是复制以更新并且客户端获得旧指针引用。
  • 客户迭代器 - 您提供自己的迭代器/导航界面,它对嵌入数据结构的 "version" 字段敏感。在访问每个元素之前,它会检查自创建迭代器以来版本是否未更改(java 集合执行此操作)。
  • 强同步 - 当 reader 正在读取时,reader 锁定数据结构以防止更新。通常是一个糟糕的解决方案,但偶尔有用(为了完整性而包括在内)。
  • 惰性复制 - 您构造一个主要引用原始对象的对象,但会触发(作为侦听器)原始对象,这样当对原始对象进行突变时,您可以在本地复制突变前的值。 这就像一个懒惰的深度复制策略。

还有其他的,但这应该可以帮助您入门。

也许您正在考虑“copy on write”成语。这使您可以避免复制,除非您必须这样做。通常不推荐使用它,因为它不是线程安全的,除非您使用同步,这会不必要地减慢单线程应用程序。

它通过保持其内部数据的引用计数来工作;类似于这段未经测试的代码:

public class User
{
    private int addressReferenceCount;
    private Address address;

    public User(Address address) {
        addressReferenceCount = 0;
        this.address = address;
    }

    public Address getAddress() {
        addressReferenceCount++;
        return address;
    }

    public void setAddress(Address address)
    {
        if (addressReferenceCount == 0) {
            this.address = address;
        }
        else {
            this.address = new Address(address);
            addressReferenceCount = 0;
        }
    }
}

这确保了像这样的用户代码在必要时会得到不同的地址:

User u = new User(new Address("1 Acacia Avenue"));
Address oldAddress = u.getAddress();
Address stillOldAddress = u.getAddress();
u.setAddress(new Address("2 Acacia Avenue"));
Address newAddress = u.getAddress();
assert (oldAddress == stillOldAddress); // both refer to same object
assert (oldAddress != newAddress);