【问题标题】:Shallow cloning and deep cloning浅克隆和深克隆
【发布时间】:2016-08-31 07:12:54
【问题描述】:

我正在重新审视 Java 的一些基本方面,并对浅层克隆和深层克隆有一些疑问。我了解他们的细节并了解内部情况。但我偶然发现了这个简单的练习 -

部门类-

public class Department {
    String deptName;

    public String getdName() {
        return deptName;
    }

    public void setdName(String dName) {
        this.deptName = dName;
    }

    public Department() {
        super();
    }

    public Department(String dName) {
        super();
        this.deptName = dName;
    }
}

员工类-

public class Employee implements Cloneable{
    int empNo;
    String empName;
    Department dept;

    public int getEmpNo() {
        return empNo;
    }
    public void setEmpNo(int empNo) {
        this.empNo = empNo;
    }
    public String getEmpName() {
        return empName;
    }
    public void setEmpName(String empName) {
        this.empName = empName;
    }
    public Department getDept() {
        return dept;
    }
    public void setDept(Department dept) {
        this.dept = dept;
    }
    public Employee(int empNo, String empName, Department dept) {
        super();
        this.empNo = empNo;
        this.empName = empName;
        this.dept = dept;
    }
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

主类-

public class ShalowCopyTest {

    public static void main(String args[]) {
        Department dept1 = new Department("Development");
        Department dept2 = new Department("Testing");
        Employee emp1 = new Employee(10, "Peter", dept1);
        Employee emp2 = null;
        try {
            emp2 = (Employee) emp1.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        System.out.println(emp1.getDept().getdName());
        System.out.println(emp2.getDept().getdName());

        System.out.println("Now changing emp1.dept");
        //emp1.setDept(dept2); //This is deep cloning - why?
        emp1.getDept().setdName("Testing"); //This is shallow cloning
        System.out.println(emp1.getDept().getdName());
        System.out.println(emp2.getDept().getdName());
    }
}

可以看到如果我使用emp1.setDept(dept2),改变emp1的dept对emp2没有影响。所以这是深度克隆。但是仅通过 emp1.getDept().setdName("Testing") 更改 dName 也会更改 emp2 的 dName。所以这是浅层克隆。为什么这两条线会有所作为?为什么他们不一样? 谢谢。

【问题讨论】:

  • 您在这里比较苹果和橘子。两条线都应该导致某些东西“粘住”。在第一种情况下,您更新了部门,在第二种情况下,您更新了名称。
  • 嗨@TimBiegeleisen,谢谢!问题是如果我直接更改原始部门的名称,那么原始参考会被更新。在这种情况下,它不会创建另一个副本并在那里进行更改,它只是更新原始副本,这会影响 emp1 和 emp2。但是,如果我将部门作为一个整体进行更改,那么它将制作部门的另一个副本并将其分配给克隆。那么为什么在这种情况下它会创建另一个部门参考?代码的哪一部分告诉这样做?如果你看到 setDept() 方法,我使用的是 this.dept=dept -> 表示浅克隆!
  • 我没有使用 new() 来创建一个新的部门,然后再修改它!我也没有在任何地方使用任何复制构造函数。所以一切都是肤浅的。但是 this.dept=dept 正在创建一个新的参考,这是我的问题。这是预期的行为吗?谢谢。

标签: java cloning


【解决方案1】:

我注意到您使用的示例与此链接上的示例相同:A Guide to Object Cloning in Java

您可能已经知道 super.clone() 的默认实现执行的是浅拷贝而不是深拷贝。

在这种情况下,两个对象实例:emp1 和 emp2 都将包含指向内存中相同位置的引用。 引用 Department 实例,这是它在 emp1 对象被克隆后在内存中引用的方式: 两者都引用内存中的同一个地方,其中包含一个不可变的字符串:“Development”。

通过设置:

emp1.setDept(dept2);

它将 emp1 的引用更改为新对象 dept2:

在这种情况下,您会将其视为 Deep Copy,但实际上并非如此。只是emp1改变了它对内存中新位置的引用。

在另一种情况下,当你设置:

emp1.getDept().setdName("Testing");

您将把 Departman 的名称更改为“Testing” - 这对两个对象实例都是可见的。

如果您还有其他问题,请告诉我。

【讨论】:

  • 嗨@Luisa Bradusca,感谢您的留言。是的,我知道在这两种情况下都是浅拷贝,但只有在我执行 emp1.setDept(dept2) 时,引用指向不同的位置。即使使用复制构造函数也可以重现此问题。 Line1: System.out.println(orig.getDept()==copy1.getDept()); //True after cloning or copy constructor Case1: orig.setDept(dept2); Case2: orig.getDept().setdName("Testing"); Line2: System.out.println(orig.getDept()==copy1.getDept());你会看到在Case1中,Line2会给出false,但是对于Case2,Line2会给出true。
  • 所以这是我的主要问题,在 orig.setDept(dept2) 之前,orig 和 copy 的部门引用都指向同一个位置。但是在 orig.setDept(dept2) 之后,它们分离并开始指向不同的位置。但即使在 orig.getDept().setdName("Testing") 之后,它们仍然指向同一个位置!这两个 setp 有什么根本不同?谢谢。
  • 嗨@NirmalyaSinha,案例1:步骤1)进行克隆。步骤 2) 设置:orig.setDept(dept2)。在此之后,orig.getDept() != copy1.getDept(),因为正如您所说,它们分离了部门引用并指向不同的位置。案例 2:步骤 1) 进行克隆。第 2 步)设置:orig.getDept().setdName("Testing")。在此之后,orig.getDept() == copy1.getDept(),因为在克隆之后,它们都引用同一个对象,并且 orig.getDept().setdName("Testing") 更改了两个对象引用的部门的名称.
  • 案例 3:步骤 1) 进行克隆。步骤 2) 设置 orig.setDept(dept2)。步骤 3) orig.getDept().setdName("Testing")。在此之后,orig.getDept() != copy1.getDept(),因为在第 2 步)对象引用已分离,并且在第 3 步)名称属性(例如 dept2)已更改,而不是原始属性。我希望现在很清楚发生了什么。
  • 那么在案例 2 在 orig.getDept().setdName("Testing") 之后,为什么 orig 和 copy 的部门引用没有像案例 1 那样分离?这是我要说的重点。
【解决方案2】:

由于您使用的是super.clone(),而superObject,因此您会得到一个浅层克隆,其中每个字段都被复制。

所以如果你开始:

emp1 -> dept1 -> dname1

然后emp2 = emp2.clone(),你得到:

emp1 -> dept1 -> dname1 ^ emp2 ----/

即它们都指向同一个Department 对象。

如果你再做dept1.name = dname2,你只会影响dept1,所以你得到:

emp1 -> dept1 -> dname2 ^ emp2 ----/

... 和emp1.getDepartment().name = dname2 具有完全相同的效果——无论你如何到达对象。

如果你现在做emp2.department = dept2,它不会影响emp1,所以你最终得到:

emp1 -> dept1 -> ... // 不变 emp2 -> dept2 -> ... // 新分配的

要进行深度克隆,您需要编写自己的克隆例程,它会克隆每个级别。

这很容易出错。最好养成使用不可变对象的习惯,其中浅拷贝“正常工作”——因为您永远无法更改字段,您不会意外影响与您正在使用的对象共享同一对象的对象.

【讨论】:

    【解决方案3】:

    你的例子是一个浅拷贝,现在是一个深拷贝,看起来你只有一个深拷贝。让我试着解释一下原因:

    您正在克隆的对象 (Employee) 确实具有复杂的数据类型 (Department)。如果你克隆一个包含其他对象的对象,那么你只复制引用而不是实际对象。

    让我们看看你的代码:

    emp1.setDept(dept2)
    

    在这种情况下,您不会更改实际实例(这将对克隆和原始实例都有影响),而是将新对象分配给 deptemp1 实例。因此,您现在在emp1emp2 中有不同的dept 实例。这就是为什么看起来你有一个深层副本。

    假设您将在您的部门内有另一个实例变量,其中包含所有必要的方法:

    public class Department {
        String deptName;
        int someNumber;
    
        public void setNumber(int number){
        someNumber=number;
        }
    
        public int getNumber(){
        return someNumber
        }
    
    
        public Department(String dName, int number){
        super();
        deptName = dName;
        someNumber = number;
        }
        }
    

    在您的主要方法中,您可以执行以下操作:

    Department dept1 = new Department(“Development”, 50);
    Employee emp1 = new Employee(10, “Peter”, dept1);
    Employee emp2 = null;
    emp2 = (Employee) emp1.clone();
    System.out.println(emp1.getDept().getNumber; // Output: 50
    System.out.println(emp1.getDept().getNumber; // Output: 50
    //Now we change the instance variable of dep1 inside emp1
    emp1.getDept().setNumber(100);
    //Now print the numbers of both employees again
    System.out.println(emp1.getDept().getNumber; // Output: 100
    System.out.println(emp1.getDept().getNumber; // Output: 100
    

    如您所见,它是浅拷贝而不是深拷贝。 emp1.getDept().setdName(“Testing”) 只是更改原始而不是克隆的唯一原因是因为字符串是不可变的。每次更改 String 时都会返回一个新的 String 实例,因此克隆对象后不会对原始 String 实例产生任何影响。

    要使您的示例成为深层副本,您必须像这样调整您的克隆方法: (我也做了一些小的调整,因此数据类型匹配,您不再需要 try/catch)

    @Override
    public Employee clone(){
    Employee clone = null;
    
    try{
    clone = (Employee)super.clone();
    clone.Department = Department.clone(); // This is the important line!! You need to clone the Department Object aswell. 
    }catch (CloneNotSupportedException e){
    e.printStackTrace();
        }
    return clone;
    }
    

    当然,如果你的部门类也让它工作,你必须在里面实现可克隆接口和克隆方法。

    【讨论】:

      猜你喜欢
      • 2013-10-11
      • 2013-04-28
      • 1970-01-01
      • 2017-09-17
      • 2011-09-05
      • 2011-08-14
      • 2011-12-22
      • 2011-03-24
      相关资源
      最近更新 更多