在Java中通过复制构造函数复制对象而不影响原始对象

5

我想复制一个对象,然后对其进行修改,但不更改原始对象。

我找到了这个解决方案,似乎最好的方法是使用复制构造函数-据我理解,这将给我一个深层拷贝(与原始对象完全分离的对象)。

所以我尝试了这个方法。但是,我注意到当下面的代码执行时,它会影响之前从中复制的所有对象。当我调用surveyCopy.take()时,它将更改Survey中的值,并且还会更改selectedSurvey中的值。

public class MainDriver {
...
//Code that is supposed to create the copy
case "11":  selectedSurvey = retrieveBlankSurvey(currentSurveys);
            Survey surveyCopy = new Survey(selectedSurvey);
            surveyCopy.take(consoleIO);
            currentSurveys.add(surveyCopy);
            break;
}

这是我的复制构造函数的代码:

public class Survey implements Serializable
{
    ArrayList<Question> questionList;
    int numQuestions;
    String taker;
    String surveyName;
    boolean isTaken;

    //Copy constructor
    public Survey(Survey incoming)
    {
        this.taker = incoming.getTaker();
        this.numQuestions = incoming.getNumQuestions();
        this.questionList = incoming.getQuestionList();
        this.surveyName = incoming.getSurveyName();
        this.isTaken = incoming.isTaken();
    }
}

那么问题到底是什么?复制构造函数不能这样工作吗?我编写的方式有问题吗?
4个回答

14

这是问题所在,在您的拷贝构造函数中:

this.questionList = incoming.getQuestionList();

这只是将引用复制到列表中。两个对象仍然会引用同一个对象。

您可以使用:

this.questionList = new ArrayList<Question>(incoming.getQuestionList());

为了创建原始列表的副本 - 但是,如果Question本身是可变的,这仍然不够好。在这种情况下,您必须创建每个Question对象的副本才能实现完全隔离。

你的其他字段没问题,因为它们要么是基元类型,要么是String的引用(它是不可变的,允许您安全地共享引用)。


谢谢您的回复。听起来我应该使Question成为不可变对象,因为我不确定它是否是不可变的。我该如何做呢?编辑:也许这不是最好的主意,因为在Question内部还有更多的不可变对象。我有点困惑。 - iaacp
@iaacp:是的,如果您可以使 Question 变成不可变对象,那会有所帮助。如果它的所有字段已经是不可变类型,那应该会让这个过程更容易。 - Jon Skeet

8

This

this.questionList = incoming.getQuestionList();

最有可能是复制了对原始列表的引用(我说“可能”是因为getQuestionList()可能会给你一个防护性拷贝)。你可能需要创建该列表的新副本。还可能包括所包含的Question对象,以及它们引用的任何内容。
这就是深层拷贝的问题。为了可靠地完成这个操作,您必须复制所有可变对象。请注意,如果一个对象是不可变的(例如字符串),那么它们无法更改,因此可以引用原始字符串而不用担心它们会被更改。基本类型也是如此。这也是在代码库中鼓励不可变性的一个很好的理由。
如果无法创建不可变类,请编写类,使其生成防护性拷贝。即当客户端请求集合时,它应该创建一个副本并返回。否则,您所谓的善意客户端可能会无意或有意地更改您的内部状态。

肯定复制了引用。 - mishadoff
我理解你的意思。我试图表明该方法可以返回一个防御性副本(即它会创建一个副本然后返回它)。无论如何,引用副本都会在某个地方发生。 - Brian Agnew
@mishadoff - 不一定。我们还没有展示getter代码,有些getter确实会创建防御性副本(这对于深度克隆来说是可以正常工作的)。“最有可能复制引用”是我认为正确的解释。 - mikera
谢谢您的回复。将所有对象都变成不可变的有什么缺点吗?我该如何使它们不可变? - iaacp

5
创建深拷贝时的问题在于,除非你也使用了特定的深拷贝构造函数,否则所有非原始类型都是按引用复制的。
在您的特定情况下,您不会遇到布尔值、整数或字符串变量的问题,因为它们是按值传递的(实际上,字符串是按引用传递的,但它是不可变的,所以没有问题),但您正在传递一个ArrayList questionList。当您这样做时,
this.object = incoming.object

您只是复制一个引用。因此,两个变量指向内存中的同一对象,因此您并没有进行深度复制。您必须创建另一个实例具有相同的内部值,然后您将确信,例如this.object = new YourObject(incoming.object)
请注意,通常意味着您的类在组合树中越复杂,就越需要深入变量,直到将它们全部复制。

0
如果我们需要复制一个简单的POJO(非嵌套)。那么浅拷贝就足够了。
克隆器类
import java.lang.reflect.Field;
public class Cloner {
    public static <T> T cloneShallow(T srcEntity, T destEntity){
        try {
            return copy(srcEntity, destEntity);
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    private static <T> T copy(T srcEntity, T destEntity) throws IllegalAccessException, InstantiationException {
        if(srcEntity == null){
            return null;
        }

        Class<?> clazz = srcEntity.getClass();

        T newEntity;

        if(destEntity != null){
            newEntity = destEntity;
        }else{
            //create new instance
            newEntity = (T) srcEntity.getClass().newInstance();
        }

        while (clazz != null) {
            copyFields(srcEntity, newEntity, clazz);
            clazz = clazz.getSuperclass();
        }

        return newEntity;
    }

    private static  <T> T copyFields(T entity, T newEntity, Class<?> clazz) throws IllegalAccessException {
        for (Field field : clazz.getDeclaredFields()) {
            field.setAccessible(true);
            field.set(newEntity, field.get(entity));
        }
        return newEntity;
    }
}

让我们称之为...

eg.
Apple apple = new Apple();
apple.setColor("Green");

Apple newApple = Cloner.cloneShallow(apple, new Apple());
( or )
Apple newApple = Cloner.cloneShallow(apple, null);

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