如何制作对象的深拷贝?

361

实现一个深度对象复制函数有一些困难。 你需要采取哪些步骤来确保原始对象和克隆对象没有共享引用?


4
Kryo内置了对复制/克隆的支持。这是直接从一个对象复制到另一个对象,而不是通过对象->字节->对象的方式。 - NateS
1
这里有一个相关的后续问题:深拷贝实用程序推荐 - Brad Cupit
使用克隆库为我解决了大问题!https://github.com/kostaskougios/cloning - Gaurav
22个回答

188

一种安全的方法是将对象序列化,然后反序列化。这可以确保所有内容都是全新的引用。

这里有一篇文章介绍如何高效地实现此操作。

注意:某些类可能会覆盖序列化,使得不会创建新实例,例如对于单例。另外,如果您的类不可序列化,则当然无法使用此方法。


7
注意,本文提供的FastByteArrayOutputStream实现可能不是最高效的。当缓冲区填满时,它使用类似ArrayList的扩展方式,但更好的方法是使用类似LinkedList的扩展方式。不要创建一个新的两倍大小的缓冲区,并将当前缓冲区进行memcpy复制,而是维护一个缓冲区的链接列表,在当前缓冲区填满时添加一个新缓冲区。如果您收到写入超过默认缓冲区大小的数据请求,请创建一个恰好与请求大小相同的缓冲区节点;这些节点不需要是相同大小的。 - Brian Harris
1
只需使用kryo:https://github.com/EsotericSoftware/kryo#copyingcloning 基准测试 http://www.slideshare.net/AlexTumanoff/serialization-and-performance - zengr
一篇好的文章,通过序列化来解释深拷贝:http://www.javaworld.com/article/2077578/learn-java/java-tip-76--an-alternative-to-the-deep-copy-technique.html - Ad Infinitum
@BrianHarris 链表并不比动态数组更高效。将元素插入到动态数组中的平摊复杂度是常数级别的,而将元素插入到链表中的复杂度是线性的。 - Norill Tempest
序列化和反序列化相比于复制构造函数方法慢多少? - Woland

81
一些人提到了使用或覆盖Object.clone()。不要这样做。 Object.clone()存在一些主要问题,在大多数情况下不建议使用。请参见Joshua Bloch的《Effective Java》中的第11项,以获得完整答案。我相信您可以安全地在原始类型数组上使用Object.clone(),但除此之外,您需要审慎地正确使用和覆盖clone。
依赖序列化(XML或其他方式)的方案是笨拙的。
这里没有简单的答案。如果要深度复制对象,则必须遍历对象图并通过对象的复制构造函数或静态工厂方法显式复制每个子对象。不需要复制不可变对象(例如String)。顺便说一句,出于这个原因,您应该倾向于使用不可变性。

5
为什么不建议使用 Object.clone()? 请在回答中至少添加一个简短的解释,不需要购买书籍。 - jazzpi
这个视频提到了使用克隆方法的一些缺点。 - Fotis Kolytoumpas

63

您可以通过序列化而不创建文件来进行深度复制。

您希望进行深层复制的对象将需要实现可序列化接口。如果该类不是final或无法修改,则可以扩展该类并实现serializable接口。

将您的类转换为一系列字节:

ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(object);
oos.flush();
oos.close();
bos.close();
byte[] byteData = bos.toByteArray();

从字节流中恢复您的类:

ByteArrayInputStream bais = new ByteArrayInputStream(byteData);
Object object = new ObjectInputStream(bais).readObject();

6
如果一个类是final的,你怎么扩展它? - Kumar Manish
2
@KumarManish 类 MyContainer 实现 Serializable 接口 { MyFinalClass 实例; ... } - Matteo T.
1
我认为这是一个很好的回复。克隆是一团糟。 - blackbird014
1
@MatteoT. 非可序列化类属性将如何被序列化,非可序列化的instance呢? - Farid
ObjectOutputStream.writeObject() 对于大对象来说非常慢,不幸的是。 - Happy
2022年,这似乎不再起作用了。会出现“java.io.NotSerializableException:”的错误。 - user1034912

48

1
它也可以在org.apache.commons.lang.SerializationUtils中找到。 - Pino

30

实现深拷贝的一种方法是为每个相关类添加复制构造函数。复制构造函数以“this”实例作为其唯一参数,并从中复制所有值。这需要一些工作,但非常直观和安全。

注意:您不需要使用访问器方法来读取字段。您可以直接访问所有字段,因为源实例始终与具有复制构造函数的实例相同类型。显而易见,但可能会被忽视。

示例:

public class Order {

    private long number;

    public Order() {
    }

    /**
     * Copy constructor
     */
    public Order(Order source) {
        number = source.number;
    }
}


public class Customer {

    private String name;
    private List<Order> orders = new ArrayList<Order>();

    public Customer() {
    }

    /**
     * Copy constructor
     */
    public Customer(Customer source) {
        name = source.name;
        for (Order sourceOrder : source.orders) {
            orders.add(new Order(sourceOrder));
        }
    }

    public String getName() {
        return name;
    }

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

编辑:请注意,复制构造函数不考虑继承。例如:如果您将OnlineOrder(Order的子类)传递给复制构造函数,则在副本中将创建常规Order实例,除非您明确解决此问题。您可以使用反射在参数的运行时类型中查找复制构造函数。但我建议如果需要通用地涵盖继承,请不要走这条路并寻找另一种解决方案。


1
你对以下情况感兴趣:你要复制的是一个子类,但是它被父类引用。是否可能覆盖复制构造函数? - Pork 'n' Bunny
为什么你的父类引用了它的子类?你能举个例子吗? - Adriaan Koster
1
公共类汽车继承自交通工具。然后将汽车称为交通工具。originalList = new ArrayList<Vehicle>; copyList = new ArrayList<Vehicle>;originalList.add(new Car());for(Vehicle vehicle: vehicleList){ copyList.add(new Vehicle(vehicle)); } - Pork 'n' Bunny
如果原始列表包含Toyota,您的代码将在目标列表中放置一个Car。适当的克隆通常要求该类提供一个虚拟工厂方法,其合同规定它将返回自己类的新对象;复制构造函数本身应该是protected,以确保它只用于构造其精确类型与被复制对象相匹配的对象。 - supercat
如果我正确理解了您的建议,工厂方法会调用私有复制构造函数?子类的复制构造函数如何确保超类字段已初始化?您能举个例子吗? - Adriaan Koster

24

你可以使用一个库,它具有简单的 API,并使用反射执行相对快速的克隆(应该比序列化方法更快)。

Cloner cloner = new Cloner();

MyClass clone = cloner.deepClone(o);
// clone is a deep-clone of o

19

Apache Commons提供了一种快速深度克隆对象的方法。

My_Object object2= org.apache.commons.lang.SerializationUtils.clone(object1);

4
这仅适用于实现了Serializable接口的对象及其所有字段均实现了Serializable接口的情况。 - wonhee

19

对于使用Spring Framework的用户,可以使用org.springframework.util.SerializationUtils类:

@SuppressWarnings("unchecked")
public static <T extends Serializable> T clone(T object) {
     return (T) SerializationUtils.deserialize(SerializationUtils.serialize(object));
}

这个解决方案可行,且不需要使用外部库。 - Radhesh Khanna
与另一个答案相同:这仅适用于实现Serializable接口的对象,以及其中所有实现了Serializable接口的字段。 - Deqing

13

对于复杂对象且性能不是很重要的情况下,我会使用一个json库,比如gson将对象序列化为json文本,然后再反序列化该文本以获取新对象。

基于反射的gson在大多数情况下都可以正常工作,但transient字段将不会被复制,并且具有循环引用的对象将导致StackOverflowError

public static <T> T copy(T anObject, Class<T> classInfo) {
    Gson gson = new GsonBuilder().create();
    String text = gson.toJson(anObject);
    T newObject = gson.fromJson(text, classInfo);
    return newObject;
}
public static void main(String[] args) {
    String originalObject = "hello";
    String copiedObject = copy(originalObject, String.class);
}

3
请遵守 Java 命名规范,为了你自己和我们的利益。 - Patrick Bergner
它实际上是有效的,而且它不强制开发人员实现Serializable接口。 - ManishS

10
XStream在这种情况下非常有用。以下是一个简单的代码来进行克隆。
private static final XStream XSTREAM = new XStream();
...

Object newObject = XSTREAM.fromXML(XSTREAM.toXML(obj));

1
不需要将对象转换为 XML,这会增加额外的开销。 - egelev
@egeleve 你意识到你正在回复一个来自'08年的评论吗?我不再使用Java,现在可能有更好的工具。然而,在那个时候,将数据序列化为不同的格式,然后再反序列化似乎是一个不错的hack方法 - 显然效率低下。 - sankara

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接