【问题标题】:Combine advantages of clone and reference?结合克隆和参考的优点?
【发布时间】:2014-03-18 13:43:02
【问题描述】:

在 Java 中,也可能在其他语言中,例如在 getter 中,您必须决定是要返回对某物的引用还是克隆(副本)。

return myInstance; 

仅返回引用非常快,不需要额外的内存,但实例的修改会“写回”原始实例。

return myInstance.clone();

返回一个克隆需要时间,并且该变量的内存加倍,但要保证它的安全。

可以在某物上创建不可变视图:

return MyUtil.immutableView(myInstance);

但有时我想要修改它,只是不想把它写回去。

现在我的想法是,是否有可能(或者已经完成,或者是否有一种编程语言可以做到)创建一个最初是对某事物的引用的对象只要没有修改。一旦第一次修改开始,引用就会自行更新为克隆。

类似这样的:

Class<T> CloneReference
{
 T ref;
 boolean cloned=false;
 public CloneReference(T ref) {this.ref=ref;}

 T getForReadOnly()
 {
  return ref;
 }

 T getForReadWrite()
 {
  if(!cloned) ref=ref.clone();
  return ref;
 }

}

不幸的是,这个解决方案复杂、笨拙且容易破解(调用 getForReadOnly() 然后使用实例更改操作)。是否有可能做得更好,还是 Java 无法做到这一点?

【问题讨论】:

  • 但是如果你有一个对象的引用,你不能在需要调用clone()的时候克隆它吗?
  • clone() 只克隆一层深度。它克隆对象中的引用,但不克隆这些引用指向的对象。这些对象仍将与原始对象相同,您在其中修改的任何内容也将在原始对象中显示出来。
  • @prmottajr:是的,但我希望库/类来处理这个问题,而不是信任调用者。

标签: java clone


【解决方案1】:

您正在寻找的内容听起来很像 Copy-On-Write。我记得PHP 是一种实现了这一点的语言。

我认为在 Java 中实现 COW 基本上应该也是可能的。我想返回一些用原始实例初始化的proxy。在第一次写入访问时,代理将继续使用副本。这是SSCCE

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.junit.Test;
import static org.junit.Assert.*;

public class CowSSCCE {

    public interface Bean {

        public String getName();

        public void setName(String name);

        public Object clone();

    }

    public class BeanImpl implements Bean {

        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Object clone() {
            BeanImpl copy = new BeanImpl();
            copy.name = new String(name);
            return copy;
        }

    }

    public class COWInvocationHandler implements InvocationHandler {

        private Bean instance;

        private boolean copy = false;

        public COWInvocationHandler(Bean instance) {
            this.instance = instance;
        }

        public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {

            // copy only on the first setter call.
            if (!copy && method.getName().startsWith("set")) {
                instance = (Bean) instance.clone();
                copy = true;

            }

            return method.invoke(instance, args);
        }

    }

    @Test
    public void testCOW() {
        Bean original = new BeanImpl();
        original.setName("original");

        Bean reference = (Bean) Proxy.newProxyInstance(
                Bean.class.getClassLoader(), new Class[] { Bean.class },
                new COWInvocationHandler(original));

        // no write access, reference is pointing to the original instance
        assertEquals(original.getName(), reference.getName());
        assertEquals(original.toString(), reference.toString());

        // write access, reference is a copied instance
        reference.setName("reference");
        assertEquals("reference", reference.getName());
        assertNotEquals(original.getName(), reference.getName());
        assertNotEquals(original.toString(), reference.toString());
    }

}

正如有人提到可读性,这不应该是一个问题:写一个建议,例如注释@ReturnCopyOnwriteReference 用代理透明地替换返回的对象。返回此类代理的 API 方法只需要该注释:

@ReturnCopyOnwriteReference
public Bean getExpensiveBean() {
    return originalBean;
}

如果您只是在寻找 COW 集合,请使用 Java 的 CopyOnWriteArrayList

【讨论】:

  • 谢谢!看来我的想法并不新鲜,如果我知道它被称为“写时复制”,它可以帮助我进一步研究!也许我的想法有点不同,因为我只想在第一次写入时复制,而不是在任何后续写入时复制,但我想基本点是相同的。
  • 我的示例在第一次写入时只复制一次。
  • 啊,是的,我的错。没有足够清楚地检查源代码。
【解决方案2】:

看看 Scala 编程语言。它运行在JVM中,大多数情况下变量是不可变的。

在 Java 中有一个 java.util.Collections#unmodifiableCollection() 方法,它将任何集合包装成不可修改的集合。这可以防止它进行编辑。但是我没有看到或想到任何可以提供您想要的行为的用例。

【讨论】:

    【解决方案3】:

    听起来你想要 C++ 的 const 正确性。不幸的是,Java 中没有什么是天生的,但是有几种策略可以实现类似的结果。

    所有这些的全部意义在于确保程序的正确性,并帮助减少副作用。

    复制构造函数

    总是返回一个副本,这样类中的对象就不会被修改。实现一个拷贝构造函数可能是最简单的,但你可能想要一个深拷贝,这意味着任何非原始成员都需要提供一种方法来获取它们自己的深拷贝(就像另一个拷贝构造函数一样)。

    不可修改的视图

    Java 的Collections 类使用Collections.unmodifiableList 等来执行此操作。此方法接受List 并使用它自己的(私有)List 实现代理它,该实现将调用转发给访问器方法,但突变方法抛出一个@ 987654325@。这有点危险,因为您只能通过文档来支持这一点。

    类层次结构

    您始终可以拥有一个不可修改的顶级 interface Foo 和一个 interface ModifiableFoo,在必要时您只返回前者。可能是最好的选择,因为您可以使用编译器而不是运行时异常来强制执行可变性,如上所述。

    【讨论】:

      【解决方案4】:

      我曾经在我的博客中写过这个主题:

      http://eyalsch.wordpress.com/2010/02/11/refdamage/

      一般来说,对于“主”对象和从中“转义”的引用(作为参数或返回值),我尝试遵循以下原则:

      1) 如果主对象暴露了一些引用,我们必须确保不能以使类处于不一致状态的方式对引用进行操作。这可以通过多种方式完成(防御性副本、不变性、侦听器等)。

      2) 如果对引用状态的修改是合法的并且自动反映在主对象状态中,则必须正确记录。

      3) 如果调用者希望在不影响主对象的情况下更新引用状态,则正确克隆是调用者的责任。

      【讨论】:

        猜你喜欢
        • 2014-07-03
        • 1970-01-01
        • 2013-01-24
        • 1970-01-01
        • 1970-01-01
        • 2012-09-23
        • 2010-10-21
        • 2019-12-09
        • 1970-01-01
        相关资源
        最近更新 更多