如何制作对象的深层副本?

Posted

技术标签:

【中文标题】如何制作对象的深层副本?【英文标题】:How do you make a deep copy of an object? 【发布时间】:2022-01-18 13:56:33 【问题描述】:

实现深度对象复制功能有点困难。您采取了哪些步骤来确保原始对象和克隆对象不共享引用?

【问题讨论】:

Kryo 内置了对copying/cloning 的支持。这是从对象直接复制到对象,而不是对象->字节->对象。 这里有一个相关的问题,后来被问到:Deep clone utility recomendation 使用克隆库为我节省了一天! github.com/kostaskougios/cloning 【参考方案1】:

一种安全的方法是序列化对象,然后反序列化。这可确保一切都是全新的参考。

Here's an article关于如何有效地做到这一点。

注意事项:类可能会覆盖序列化,从而创建新实例,例如对于单身人士。如果您的类不可序列化,这当然也不起作用。

【讨论】:

请注意,文章中提供的 FastByteArrayOutputStream 实现可能更高效。当缓冲区填满时,它使用 ArrayList 样式的扩展,但最好使用 LinkedList 样式的扩展方法。与其创建一个新的 2x 缓冲区并 memcpy-ing 当前缓冲区,不如维护一个缓冲区链接列表,当当前缓冲区填满时添加一个新缓冲区。如果您收到写入超出默认缓冲区大小的数据的请求,请创建一个与请求完全相同的缓冲区节点;节点的大小不必相同。 只使用kryo:github.com/EsotericSoftware/kryo#copyingcloning benchmark slideshare.net/AlexTumanoff/serialization-and-performance 一篇好文章,通过序列化解释深拷贝:javaworld.com/article/2077578/learn-java/… @BrianHarris 链表并不比动态数组更有效。将元素插入动态数组是摊销常数复杂度,而插入链表是线性复杂度 序列化和反序列化比复制构造方法慢多少?【参考方案2】:

一些人提到使用或覆盖Object.clone()。不要这样做。 Object.clone() 存在一些重大问题,在大多数情况下不鼓励使用它。请参阅 Joshua Bloch 的“Effective Java”中的第 11 项以获得完整答案。我相信您可以在原始类型数组上安全地使用Object.clone(),但除此之外,您还需要谨慎地正确使用和覆盖克隆。

依赖序列化(XML 或其他)的方案很笨拙。

这里没有简单的答案。如果要深度复制对象,则必须遍历对象图并通过对象的复制构造函数或静态工厂方法显式复制每个子对象,进而深度复制子对象。不需要复制不可变对象(例如 Strings)。顺便说一句,出于这个原因,您应该支持不变性。

【讨论】:

为什么不鼓励使用Object.clone()?请在答案中至少添加一个简短的解释,我不想买这本书。【参考方案3】:

您可以使用序列化进行深层复制,而无需创建文件。

您想要深拷贝的对象需要implement 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();

【讨论】:

如果类是最终的,你将如何扩展它? @KumarManish 类 MyContainer 实现 Serializable MyFinalClass 实例; ... 我觉得这个回复很好。克隆是一团糟 @MatteoT。不可序列化的类属性将如何序列化,在这种情况下不可序列化instance【参考方案4】:

您可以在 Apache Commons Lang 中使用 org.apache.commons.lang3.SerializationUtils.clone(T) 进行基于序列化的深度克隆,但要小心 - 性能很差。

一般来说,最佳实践是为需要克隆的对象图中的每个对象类编写自己的克隆方法。

【讨论】:

org.apache.commons.lang.SerializationUtils也可以使用【参考方案5】:

实现深度复制的一种方法是向每个关联的类添加复制构造函数。复制构造函数将 '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 实例,除非您明确解决此问题。您可以使用反射在参数的运行时类型中查找复制构造函数。但如果需要以一般方式涵盖继承,我建议不要走这条路并寻找其他解决方案。

【讨论】:

只是对您正在复制的是子类但被父类引用的情况感兴趣。是否可以覆盖复制构造函数? 为什么你的父类引用它的子类?能举个例子吗? 公共类 Car 扩展 Vehicle ,然后将汽车称为车辆。 originaList = new ArrayList; copyList = new ArrayList; originalList.add(new Car()); for(Vehicle 车辆: vehicleList) copyList.add(new Vehicle(vehicle)); @AdriaanKoster:如果原始列表包含Toyota,您的代码会将Car 放入目标列表中。正确的克隆通常要求类提供一个虚拟工厂方法,其合同规定它将返回其自己类的新对象;复制构造器本身应该是protected,以确保它只用于构造精确类型与被复制对象匹配的对象)。 所以如果我正确理解你的建议,工厂方法会调用私有复制构造函数吗?子类的复制构造函数如何确保超类字段被初始化?能举个例子吗?【参考方案6】:

你可以use a library,它有一个简单的API,并通过反射执行相对快速的克隆(应该比序列化方法更快)。

Cloner cloner = new Cloner();

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

【讨论】:

【参考方案7】:

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

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

【讨论】:

这仅适用于实现 Serializable 的对象,但也适用于其中实现 Serializable 的所有字段。【参考方案8】:

适用于 Spring Framework 用户。使用类org.springframework.util.SerializationUtils

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

【讨论】:

此解决方案有效,不需要使用外部库【参考方案9】:

对于复杂的对象,当性能不重要时,我使用 json 库,例如 gson 将对象序列化为 json 文本,然后反序列化文本以获取新对象。

基于反射的 gson 在大多数情况下都可以工作,除了 transient 字段不会被复制,并且带有循环引用的对象会导致 ***Error

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);

【讨论】:

为了您自己和我们的利益,请遵守 Java 命名约定。【参考方案10】:

XStream 在这种情况下非常有用。这是一个简单的克隆代码

private static final XStream XSTREAM = new XStream();
...

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

【讨论】:

不,你不需要 xml-ing 对象的开销。 @egeleve 您确实意识到您正在回复 08 年的评论,对吧?我不再使用 Java,现在可能有更好的工具。然而在那个时候,序列化为不同的格式,然后再序列化回来似乎是一个很好的技巧——它肯定是低效的。【参考方案11】:

一种非常简单的方法是使用 Jackson JSON 将复杂的 Java 对象序列化为 JSON 并将其读回。

来自https://github.com/FasterXML/jackson-databind/#5-minute-tutorial-streaming-parser-generator:

JsonFactory f = mapper.getFactory(); // may alternatively construct directly too

// First: write simple JSON output
File jsonFile = new File("test.json");
JsonGenerator g = f.createGenerator(jsonFile);
// write JSON:  "message" : "Hello world!" 
g.writeStartObject();
g.writeStringField("message", "Hello world!");
g.writeEndObject();
g.close();

// Second: read file back
JsonParser p = f.createParser(jsonFile);

JsonToken t = p.nextToken(); // Should be JsonToken.START_OBJECT
t = p.nextToken(); // JsonToken.FIELD_NAME
if ((t != JsonToken.FIELD_NAME) || !"message".equals(p.getCurrentName())) 
   // handle error

t = p.nextToken();
if (t != JsonToken.VALUE_STRING) 
   // similarly

String msg = p.getText();
System.out.printf("My message to you is: %s!\n", msg);
p.close();

【讨论】:

【参考方案12】:

使用 XStream(http://x-stream.github.io/)。您甚至可以通过注释或显式指定 XStream 类的属性名称来控制可以忽略的属性。此外,您不需要实现可克隆接口。

【讨论】:

【参考方案13】:

只有在每个班级同意的情况下才能进行深度复制。如果您可以控制类层次结构,则可以实现可克隆接口并实现 Clone 方法。否则做一个深拷贝是不可能安全的,因为对象也可能共享非数据资源(例如数据库连接)。但是,一般而言,深复制在 Java 环境中被认为是不好的做法,应通过适当的设计做法来避免。

【讨论】:

您能描述一下“适当的设计实践”吗?【参考方案14】:
import com.thoughtworks.xstream.XStream;

public class deepCopy 
    private static  XStream xstream = new XStream();

    //serialize with Xstream them deserialize ...
    public static Object deepCopy(Object obj)
        return xstream.fromXML(xstream.toXML(obj));
    

【讨论】:

【参考方案15】:

我使用 Dozer 克隆 java 对象,这很好,Kryo 库是另一个不错的选择。

【讨论】:

你能用一个代码示例详细说明如何使用推土机执行任务吗?【参考方案16】:

使用 Jackson 对对象进行序列化和反序列化。此实现不需要对象实现 Serializable 类。

  <T> T clone(T object, Class<T> clazzType) throws IOException 

    final ObjectMapper objMapper = new ObjectMapper();
    String jsonStr= objMapper.writeValueAsString(object);

    return objMapper.readValue(jsonStr, clazzType);

  

【讨论】:

【参考方案17】:

BeanUtils 在深度克隆 bean 方面做得非常好。

BeanUtils.cloneBean(obj);

【讨论】:

它进行浅层克隆。【参考方案18】:

1)

public static Object deepClone(Object object) 
   try 
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     ObjectOutputStream oos = new ObjectOutputStream(baos);
     oos.writeObject(object);
     ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
     ObjectInputStream ois = new ObjectInputStream(bais);
     return ois.readObject();
   
   catch (Exception e) 
     e.printStackTrace();
     return null;
   
 

2)

    // (1) create a MyPerson object named Al
    MyAddress address = new MyAddress("Vishrantwadi ", "Pune", "India");
    MyPerson al = new MyPerson("Al", "Arun", address);

    // (2) make a deep clone of Al
    MyPerson neighbor = (MyPerson)deepClone(al);

这里你的 MyPerson 和 MyAddress 类必须实现可序列化接口

【讨论】:

【参考方案19】:

这是一个通用的深度克隆方法,使用对象序列化和反序列化字节数组流(以避免写入文件)。

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

@SuppressWarnings("unchecked")
public static <T extends Serializable> T deepClone(T t) 
    try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);) 
        oos.writeObject(t);
        byte[] bytes = baos.toByteArray();
        try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) 
            return (T) ois.readObject();
        
     catch (IOException | ClassNotFoundException e) 
        throw new RuntimeException(e);
    

【讨论】:

【参考方案20】:

这是一个关于如何深度克隆任何对象的简单示例: 先实现可序列化

public class CSVTable implements Serializable
    Table<Integer, Integer, String> table; 
    public CSVTable() 
        this.table = HashBasedTable.create();
    
    
    public CSVTable deepClone() 
        try 
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(this);

            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            return (CSVTable) ois.readObject();
         catch (IOException e) 
            return null;
         catch (ClassNotFoundException e) 
            return null;
        
    


然后

CSVTable table = new CSVTable();
CSVTable tempTable = table.deepClone();

是你获得克隆的方式。

【讨论】:

以上是关于如何制作对象的深层副本?的主要内容,如果未能解决你的问题,请参考以下文章

如何制作一个快速的类对象数组的深层副本

如何制作Java ArrayList的深层副本[重复]

如何在 Java 中制作 ArrayList<Integer> 的深层副本? [复制]

如何制作保持不变的核心数据对象的副本

如何制作 Core Data 对象图的精确副本?

使用Spread制作对象类具有抽象方法的对象副本时如何解决错误?