如何安全地发布可变对象?
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 中没有可让您执行此操作的内置机制。通常,如果你移动实例,你会:
- 使用不可变对象
- 传递对象的副本
由于您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;
}
}
现在,如果您使用接口而不是 XYZ
和 Address
class,您可以有 2 个实现(例如 XYZMutable
和 XYZImmutable
) 这将允许您抽象出您正在 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);
我不想做深拷贝的方式
比如说,我有一个可变类型的字段,例如 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 中没有可让您执行此操作的内置机制。通常,如果你移动实例,你会:
- 使用不可变对象
- 传递对象的副本
由于您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;
}
}
现在,如果您使用接口而不是 XYZ
和 Address
class,您可以有 2 个实现(例如 XYZMutable
和 XYZImmutable
) 这将允许您抽象出您正在 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);