如何在Java中复制一个对象?

921

考虑下面的代码:

DummyBean dum = new DummyBean();
dum.setDummy("foo");
System.out.println(dum.getDummy()); // prints 'foo'

DummyBean dumtwo = dum;
System.out.println(dumtwo.getDummy()); // prints 'foo'

dum.setDummy("bar");
System.out.println(dumtwo.getDummy()); // prints 'bar' but it should print 'foo'
所以,我想将 dum 复制到 dumtwo 并更改 dum,而不影响 dumtwo。但是上面的代码并没有实现这个功能。当我在 dum 中进行更改时,dumtwo 中也会发生相同的更改。

我猜,当我说 dumtwo = dum 时,Java 只复制了引用。那么,有没有办法创建 dum 的一个新副本并将其赋值给 dumtwo

23个回答

702

创建一个拷贝构造函数:

class DummyBean {
  private String dummy;

  public DummyBean(DummyBean another) {
    this.dummy = another.dummy; // you can access  
  }
}

每个对象都有一个克隆方法可用于复制对象,但不要使用它。这种方式太容易创建类并进行不恰当的克隆方法。如果你打算这样做,请至少阅读一下 Joshua Bloch 在 Effective Java 中所说的内容。


46
那他需要把代码改为 DummyBean two = new DummyBean(one) 对吗? - Chris K
13
这种方法是否能够有效地达到深拷贝的效果? - Matthew Piziak
140
对我来说,这不会是一个深拷贝,因为任何嵌套对象仍然引用原始实例,除非每个引用(非值类型)对象提供与上述相同的构造函数模板。 - SliverNinja - MSFT
18
是的,它们将引用相同的字符串,但由于它是不可变的,所以没问题。对于基本类型也是如此。对于非基本类型,您只需递归调用复制构造函数。例如,如果DummyBean引用了FooBar,则FooBar应该有构造函数FooBar(FooBar another),并且dummy应该调用此函数。foobar = new FooBar(another.foobar)。 - egaga
7
不,它不会是“johndoe”。就像Timmmm所说,字符串本身是不可变的。通过setDummy(..)将引用设置为指向“johndoe”,但不是one中的引用。 - keuleJ
显示剩余11条评论

454

基础:Java中的对象复制。

我们假设有一个叫obj1的对象,它包含两个对象:containedObj1containedObj2
enter image description here

浅拷贝:
浅拷贝创建一个新的相同类的实例,将所有字段复制到新的实例中并返回它。Object类提供了一个clone方法,并支持浅拷贝。
enter image description here

深拷贝:
当对象被复制并且其引用的对象也被复制时,就发生了深拷贝。下图展示了在obj1上执行深拷贝后的结果。obj1不仅被复制了,而且其中包含的对象也被复制了。我们可以使用Java对象序列化来进行深拷贝。不幸的是,这种方法也存在一些问题(详细示例)。
enter image description here

可能出现的问题:
clone 实现起来很棘手。
最好使用 防御性复制, 复制构造函数(如 @egaga 回答所述)或 静态工厂方法

如果你有一个对象,你知道它有一个公共的clone()方法,但你在编译时不知道对象的类型,那么你就有问题了。Java有一个叫做Cloneable的接口。实际上,如果我们想让一个对象可克隆,我们应该实现这个接口。Object.cloneprotected的,所以我们必须用一个公共方法来覆盖它,以便它可以被访问。
当我们尝试对一个复杂对象进行深度复制时,另一个问题就出现了。假设所有成员对象变量的clone()方法也进行了深度复制,这是一个太冒险的假设。你必须控制所有类中的代码。
例如,org.apache.commons.lang.SerializationUtils将使用序列化(Source)进行深度克隆的方法。如果我们需要克隆Bean,则org.apache.commons.beanutils中有几个实用程序方法(Source)。
  • cloneBean将基于可用的属性getter和setter克隆Bean,即使Bean类本身没有实现Cloneable。
  • copyProperties将为所有属性名称相同的情况将属性值从原始Bean复制到目标Bean。

1
请问您能解释一下什么是包含另一个对象吗? - Freakyuser
1
@Chandra Sekhar,“浅复制创建一个相同类的新实例,并将所有字段复制到新实例中并返回它”这种说法是错误的,因为不需要提及所有字段,因为对象不会被复制,只有指向旧对象(原始对象)的相同对象引用被复制。 - JAVA
4
@sunny - Chandra的描述是正确的,你所描述的内容也是正确的;但我认为你对“copies all the fields”的含义理解有误。字段本身就是引用,而不是被引用的对象。 “复制所有字段”意味着“复制所有这些引用”。很高兴你能指出这个具体含义,这对于其他有和你一样错解“复制所有字段”语句的人来说非常有帮助。 :) - ToolmakerSteve
2
如果我们从某些较低级别的面向对象语言的角度来思考,使用指向对象的“指针”,这样的字段将包含内存中的地址(例如“0x70FF1234”),该地址是找到对象数据的位置。该地址是被复制(分配)的“字段值”。您是正确的,最终结果是两个对象都具有指向(指向)同一对象的字段。 - ToolmakerSteve
https://www.baeldung.com/java-deep-copy - Smart Coder

163

在包中 import org.apache.commons.lang.SerializationUtils; 中有一个方法:

SerializationUtils.clone(Object);

例子:

this.myObjectCloned = SerializationUtils.clone(this.object);

81
只要对象实现了Serializable接口。 - Androiderson
2
在这种情况下,如果最后一个是静态的,则克隆对象没有对原始对象的引用。 - Dante
12
一个第三方库只是为了克隆对象! - User
7
@Khan,“仅仅为了使用第三方库”是完全不同的讨论!:D - Charles Wood
我在Android 4、5和6上遇到了java.lang.NoClassDefFoundError错误:Fatal Exception: java.lang.NoClassDefFoundError org.apache.commons.lang3.-$$Lambda$Validate$0cAgQbsjQIo0VHKh79UWkAcDRWk - Mikhail
只有当此对象内的所有对象都可序列化时,此方法才有效。 - Rehmanali Momin

113

按照以下步骤操作:

public class Deletable implements Cloneable{

    private String str;
    public Deletable(){
    }
    public void setStr(String str){
        this.str = str;
    }
    public void display(){
        System.out.println("The String is "+str);
    }
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

并且无论在哪里您想要获取另一个对象,只需执行克隆即可。 例如:

Deletable del = new Deletable();
Deletable delTemp = (Deletable ) del.clone(); // this line will return you an independent
                                 // object, the changes made to this object will
                                 // not be reflected to other object

2
你测试过这个吗?我想在我的项目中使用它,所以正确性很重要。 - misty
3
@misty 我已经测试过了,在我的生产应用上完美运行。 - Andrii Kovalchuk
2
克隆后,当您修改原始对象时,它也会修改克隆对象。 - Sibish
12
这样做是错误的,因为这不是要求的深拷贝。 - Bluehorn
2
这个方法克隆指向可克隆对象的指针,但是两个对象内部的所有属性都是相同的。因此,在内存中创建了一个新对象,但每个对象内部的数据都是来自内存中的相同数据。 - Omar HossamEldin
显示剩余2条评论

47

为什么使用反射API没有答案?

private static Object cloneObject(Object obj){
        try{
            Object clone = obj.getClass().newInstance();
            for (Field field : obj.getClass().getDeclaredFields()) {
                field.setAccessible(true);
                field.set(clone, field.get(obj));
            }
            return clone;
        }catch(Exception e){
            return null;
        }
    }

这很简单。

编辑:通过递归包含子对象

private static Object cloneObject(Object obj){
        try{
            Object clone = obj.getClass().newInstance();
            for (Field field : obj.getClass().getDeclaredFields()) {
                field.setAccessible(true);
                if(field.get(obj) == null || Modifier.isFinal(field.getModifiers())){
                    continue;
                }
                if(field.getType().isPrimitive() || field.getType().equals(String.class)
                        || field.getType().getSuperclass().equals(Number.class)
                        || field.getType().equals(Boolean.class)){
                    field.set(clone, field.get(obj));
                }else{
                    Object childObj = field.get(obj);
                    if(childObj == obj){
                        field.set(clone, clone);
                    }else{
                        field.set(clone, cloneObject(field.get(obj)));
                    }
                }
            }
            return clone;
        }catch(Exception e){
            return null;
        }
    }

这看起来好多了,但是你只需要考虑最终字段,因为setAccessible(true)可能会失败,所以也许你需要单独处理当调用field.set(clone, field.get(obj))时抛出的IllegalAccessException异常。 - Max
1
我非常喜欢它,但你能重构一下使用泛型吗?private static <T> T cloneObject(T obj) { .... } - Adelin
2
我认为问题出在我们从属性引用到其父级时: Class A { B child; } Class B{ A parent; } - nhthai
我在使用getSuperClass时遇到了一些与null值相关的问题,可以通过Number.class.equals(field.getType().getSuperclass())来解决。对于数组,它无法正常工作,但可以通过以下方式进行修复:if(Object[].class.isAssignableFrom(field.getType())){ Object[] values = ((Object[])field.get(obj)); Object[] valuesClone = Arrays.copyOf(values, values.length); field.set(clone, valuesClone); } - EliuX
3
这容易出错,不确定它如何处理集合。 - ACV
显示剩余4条评论

34

我使用Google的JSON库对其进行序列化,然后创建序列化对象的新实例。它执行深度拷贝,但有一些限制:

  • 不能有任何递归引用

  • 不会复制不同类型的数组

  • 数组和列表应该是类型化的,否则它将无法找到要实例化的类

  • 您可能需要在声明自己的类中封装字符串

我还使用这个类来保存用户首选项、窗口等以在运行时重新加载。它非常易于使用且高效。

import com.google.gson.*;

public class SerialUtils {

//___________________________________________________________________________________

public static String serializeObject(Object o) {
    Gson gson = new Gson();
    String serializedObject = gson.toJson(o);
    return serializedObject;
}
//___________________________________________________________________________________

public static Object unserializeObject(String s, Object o){
    Gson gson = new Gson();
    Object object = gson.fromJson(s, o.getClass());
    return object;
}
       //___________________________________________________________________________________
public static Object cloneObject(Object o){
    String s = serializeObject(o);
    Object object = unserializeObject(s,o);
    return object;
}
}

1
这个很好用。但是要小心,如果你尝试克隆像List<Integer>这样的东西,它会出现错误,我的Integers变成了Doubles,100.0。我花了很长时间才明白为什么会这样。解决方法是逐个克隆整数并在循环中添加到列表中。 - paakjis

24

是的,你只是在引用对象。如果实现了 Cloneable 接口,则可以克隆对象。

查看这篇关于复制对象的维基文章。

请参阅:对象复制


18
在你的类中添加Cloneable和以下代码。
public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

使用此代码进行克隆:clonedObject = (YourClass) yourClassObject.clone();


18

深度克隆是您的答案,需要实现 Cloneable 接口并覆盖 clone() 方法。

public class DummyBean implements Cloneable {

   private String dummy;

   public void setDummy(String dummy) {
      this.dummy = dummy;
   }

   public String getDummy() {
      return dummy;
   }

   @Override
   public Object clone() throws CloneNotSupportedException {
      DummyBean cloned = (DummyBean)super.clone();
      cloned.setDummy(cloned.getDummy());
      // the above is applicable in case of primitive member types like String 
      // however, in case of non primitive types
      // cloned.setNonPrimitiveType(cloned.getNonPrimitiveType().clone());
      return cloned;
   }
}

您可以像这样调用它

DummyBean dumtwo = dum.clone();

4
“dummy”是一个字符串,它是不可变的,你不需要复制它。 - Steve Kuo

13

这也可以奏效。假设模型

class UserAccount{
   public int id;
   public String name;
}

首先,在您的app build.gradle文件中添加compile 'com.google.code.gson:gson:2.8.1',并同步。然后

Gson gson = new Gson();
updateUser = gson.fromJson(gson.toJson(mUser),UserAccount.class);

通过在访问修饰符后使用transient关键字,您可以排除使用字段。

注意:这是不良实践。同时不建议使用CloneableJavaSerialization,它们速度缓慢且容易出错。为了获得最佳性能,请编写复制构造函数。参考

例如:

class UserAccount{
        public int id;
        public String name;
        //empty constructor
        public UserAccount(){}
        //parameterize constructor
        public UserAccount(int id, String name) {
            this.id = id;
            this.name = name;
        }

        //copy constructor
        public UserAccount(UserAccount in){
            this(in.id,in.name);
        }
    }

90000次迭代测试结果:
代码行 UserAccount clone = gson.fromJson(gson.toJson(aO), UserAccount.class); 耗时 808毫秒

代码行 UserAccount clone = new UserAccount(aO); 耗时 不到1毫秒

结论: 如果你的老板很疯狂,而你更注重速度,请使用gson。如果你更注重质量,请使用第二个复制构造函数。

你还可以在Android Studio中使用复制构造函数代码生成器插件


如果这是不好的做法,你为什么建议它呢? - Parth Mehrotra
感谢 @ParthMehrotra,现在已经改进了。 - Qamar

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