java克隆(深浅拷贝,复制)详解

时间:2021-10-03 21:39:58

1.浅复制与深复制概念

(1)浅复制(浅克隆)被复制对象的所有变量都含有与原来对象相同的值,而所有的对其他对象的引用仍然只指向原来的对象,换言之,浅复制仅仅复制锁考虑的对象,而不复制它所引用的对象。(2)深复制(深克隆)被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量,那些引用其他对象的变量将指向被复制过的新对象,而不再试原有的那些被引用的对象,换言之,深复制把要复制的对象所引用的对象都复制了一遍。

实现ICloneable接口的方式取决于我们的类型的数据成员。如果类型仅包含值类型(int,byte等类型)和string类型的数据成员,我们只要在Clone方法中初始化一个新的对象,将其的数据成员设置为当前对象的各个成员的值即可。事实上,object类的MemberwiseClone方法会自动完成该过程。

如果自定义类型包含引用类型的数据成员,必须考虑Clone方法是实现浅拷贝(shallow copy)还是深拷贝(deep copy)。浅拷贝是指副本对象中的引用类型的数据成员与源对象的数据成员指向相同的对象。而如果是深拷贝,则必须创建整个对象的结构,副本对象中的引用类型的数据成员与源对象的数据成员指向不同的对象。

浅拷贝是容易实现的,就是使用前面提到的MemberwiseClone方法。开发人员往往希望使用的类型能够实现深拷贝,但会发现这样的类型并不多。这种情况在System.Collections命名空间中尤其常见,这里面的类在其Clone方法中实现的都是浅拷贝

 

2. java的clone()方法


(1)clone方法将对象复制了一份并返回给调用之,一般而言,clone()方法满足:
     对任何的对象C=X,都有x.clone()!=x//克隆对象与源对象不是同一个对象
     对任何的对象X,都有x.clone().getClass()==x.getClass()//克隆对象与源对象类型一样
     如果对象X的equals()方法定义恰当,那么x.clone().equals(x)应该成立。

(2)java对象中的克隆
    为了获取对象的一份拷贝,我们可以利用Object类得clone()方法。
    在派生类中覆盖基类的clone(),并声明为public。
    在派生类的clone()方法中,调用super.clone().
    在派生类中实现Cloneable接口
代码如下 

package clone;

public class Student implements Cloneable{
 String name;
 int age;
 Student(String name,int age){
  this.name=name;
  this.age=age;
 }
 public Object clone(){
  Object o =null;
  try{
   o=(Student)super.clone();//Object中的clone()识别出你要复制的哪一个对象
  }
  catch(CloneNotSupportedException e){
   System.out.println(e.toString());
  }
  return o;
 }
 
 public static void main(String[] args){
  Student s1 = new Student("zhang",18);
  Student s2 = (Student)s1.clone();
  s2.name="li";
  s2.age=20;
  System.out.println("name="+s1.name+","+"age="+s1.age);//修改学生2后不影响学生1的值
   }
}

说明:
1问什么我们在派生类中覆盖Object的clone方式时,一定要调用super.clone()呢?在运行时刻,Object中的clone()识别出你要复制的是哪一个对象,然后为此对象分配空间,并进行对象的复制,将原始对象的内容一一复制到新对象的存储空间中。
继承自java.lang.Object类得clone()方式是浅复制,以下代码可以证明

class Professor
{
    String name;
    int age;
    Professor(String name,int age)
    {
        this.name=name;
        this.age=age;
    }
}
class Student implements Cloneable
{
    String name;//常量对象。
    int age;
    Professor p;//学生 1和学生2的引用值都是一样的。
    Student(String name,int age,Professor p)
    {
        this.name=name;
        this.age=age;
        this.p=p;
    }
    public Object clone()
    {
        Student o=null;
        try
        {
            o=(Student)super.clone();
        }
        catch(CloneNotSupportedException e)
        {
            System.out.println(e.toString());
        }
        o.p=(Professor)p.clone();//只是此处编译错误clone是Object类的一个protected方法
        return o;
    }
}
public static void main(String[] args)
    {
      Professor p=new Professor("wangwu",50);
      Student s1=new Student("zhangsan",18,p);
      Student s2=(Student)s1.clone();
      s2.p.name="lisi";
     s2.p.age=30;
 System.out.println("name="+s1.p.name+","+"age="+s1.p.age);//学生 1的教授成为 lisi,age为30。
}

 此处浅拷贝的测试代码如果要运行正确需要把professor类中的o.p=(Professor)p.clone();去掉

package clone;

public class CloneTestPro  
{  
    public static void main(String[] args)  
    {  
     Professor p = new Professor("王教授",50);
        Student s1 = new Student("张学生",18,p); 
        System.out.println("s1:" + s1.name + " " + s1.age + " "   + s1.p.name+ " "   + s1.p.age);  
          
            //拷贝一个学生   
        Student s2 = (Student)s1.clone();  
        s2.age=21;  
        s2.name="WUDI";  
        System.out.println("s2:" + s2.name + " " + s2.age + " "+ s2.p.name+ " " + s2.p.age);  
        System.out.println("after s2 changing some property:");  
        //通过学生s2改变了他所在学校的名称和成立时间  
        s2.name="Tianjin University";  
        s2.age=12;  
        System.out.println("s1:" + s1.name + " " + s1.age + " "  + s1.p.name+ " "   + s1.p.age);  
        System.out.println("s2:" + s2.name + " " + s2.age + " " + s2.p.name+ " "  + s2.p.age);  
        //从运行结果可以看出,通过学生s2将其引用school所指向的对象的name和bornYear属性改变,
        //相应的s1的也被改变,这说明学生s1和学生s2里面的school引用指向的始终是同一个School对象。这就是浅拷贝的含义。
    }  
}  

 

2.那应该如何实现深层次的克隆,即修改s2的教授不会影响s1的教授?代码改进如下。

 
改进使学生 1的Professor不改变(深层次的克隆)
class Professor implements Cloneable
{
    String name;
    int age;
    Professor(String name,int age)
    {
        this.name=name;
        this.age=age;
    }
    public Object clone()
    {
        Object o=null;
        try
        {
            o=super.clone();
        }
        catch(CloneNotSupportedException e)
        {
            System.out.println(e.toString());
        }
        return o;
    }
}
class Student implements Cloneable
{
    String name;
    int age;
    Professor p;
    Student(String name,int age,Professor p)
    {
        this.name=name;
        this.age=age;
        this.p=p;
    }
    public Object clone()
    {
        Student o=null;
        try
        {
            o=(Student)super.clone();
        }
        catch(CloneNotSupportedException e)
        {
            System.out.println(e.toString());
        }
        o.p=(Professor)p.clone();
        return o;
    }
}
public static void main(String[] args)
    {
      Professor p=new Professor("wangwu",50);
      Student s1=new Student("zhangsan",18,p);
      Student s2=(Student)s1.clone();
      s2.p.name="lisi";
     s2.p.age=30;
System.out.println("name="+s1.p.name+","+"age="+s1.p.age);//学生 1的教授不改变。
}

(1)浅拷贝
默认的拷贝都是浅拷贝,你可以通过下面的方法实现:

Java代码

class Employee implements Cloneable  

{  

   public Employee clone() throws CloneNotSupportedException  

    {  

     return super.clone();  

    }  

class Employee implements Cloneable

{

   public Employee clone() throws CloneNotSupportedException

    {

     return super.clone();

    }

}


浅拷贝需要记住的是原对象的各属性最好都是基本类型或者不可变的对象。

(2)深拷贝
深拷贝是相对浅拷贝的,解决不能管理子对象的问题的。
实现方法如下:

Java代码

class Employee implements Cloneable  

{  

   public Object clone() throws CloneNotSupportedException  

    {  

     Employee cloned = (Employee)super.clone();  

    //深拷贝子对象  

     cloned.dep = (Department)dep.clone();  

     return cloned.  

    }  

 

  值得注意的是 :如果需要使用clone方法,必需实现java.lang.Cloneable接口,否则会抛出java.lang.CloneNotSupportedException。

  另外clone方法所做的的操作是直接复制字段的内容,换句话说,这个操作并不管该字段对应的对象实例内容。

  像这样字段对字段的拷贝(field to field copy)就成为"浅拷贝",clone方法所做的正是"浅拷贝"。

 

3.利用串行化来做深复制
把对象写到流里的过程是串行化(Serilization)过程,但是在Java程序师圈子里又非常形象地称为“冷冻”或者“腌咸菜(picking)”过程;而把对象从流中读出来的并行化(Deserialization)过程则叫做“解冻”或者“回鲜(depicking)”过程。应当指出的是,写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面,因此“腌成咸菜”的只是对象的一个拷贝,Java咸菜还可以回鲜。
在Java语言里深复制一个对象,常常可以先使对象实现Serializable接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里(腌成咸菜),再从流里读出来(把咸菜回鲜),便可以重建对象。
如下为深复制源代码。
public Object deepClone()
{
 //将对象写到流里
 ByteArrayOutoutStream bo=new ByteArrayOutputStream();
 ObjectOutputStream oo=new ObjectOutputStream(bo);
 oo.writeObject(this);
 //从流里读出来
 ByteArrayInputStream bi=new ByteArrayInputStream(bo.toByteArray());
 ObjectInputStream oi=new ObjectInputStream(bi);
 return(oi.readObject());
}
 
这样做的前提是对象以及对象内部所有引用到的对象都是可串行化的,否则,就需要仔细考察那些不可串行化的对象可否设成transient,从而将之排除在复制过程之外。上例代码改进如下。
 
class Professor implements Serializable
{
    String name;
    int age;
    Professor(String name,int age)
    {
        this.name=name;
        this.age=age;
    }
}
class Student implements Serializable
{
    String name;//常量对象。
    int age;
    Professor p;//学生 1和学生2的引用值都是一样的。
    Student(String name,int age,Professor p)
    {
        this.name=name;
        this.age=age;
        this.p=p;
    }
    public Object deepClone() throws IOException,
OptionalDataException,ClassNotFoundException
{
 //将对象写到流里
 ByteArrayOutoutStream bo=new ByteArrayOutputStream();
 ObjectOutputStream oo=new ObjectOutputStream(bo);
 oo.writeObject(this);
 //从流里读出来
 ByteArrayInputStream bi=new ByteArrayInputStream(bo.toByteArray());
 ObjectInputStream oi=new ObjectInputStream(bi);
 return(oi.readObject());
}
 
}
public static void main(String[] args)
    {
      Professor p=new Professor("wangwu",50);
      Student s1=new Student("zhangsan",18,p);
      Student s2=(Student)s1.deepClone();
      s2.p.name="lisi";
     s2.p.age=30;
System.out.println("name="+s1.p.name+","+"age="+s1.p.age); //学生 1的教授不改变。
}