Java中物件的深複製和淺複製詳解

陶邦仁發表於2016-01-07

1.淺複製與深複製概念

⑴淺複製(淺克隆)

被複制物件的所有變數都含有與原來的物件相同的值,而所有的對其他物件的引用仍然指向原來的物件。換言之,淺複製僅僅複製所考慮的物件,而不復制它所引用的物件。

⑵深複製(深克隆)

被複制物件的所有變數都含有與原來的物件相同的值,除去那些引用其他物件的變數。那些引用其他物件的變數將指向被複制過的新物件,而不再是原有的那些被引用的物件。換言之,深複製把要複製的物件所引用的物件都複製了一遍。

2.Java的clone()方法

⑴clone方法將物件複製了一份並返回給呼叫者。一般而言,clone()方法滿足:

①對任何的物件x,都有x.clone() !=x//克隆物件與原物件不是同一個物件
②對任何的物件x,都有x.clone().getClass()= =x.getClass()//克隆物件與原物件的型別一樣
③如果物件x的equals()方法定義恰當,那麼x.clone().equals(x)應該成立。

⑵Java中物件的克隆

①為了獲取物件的一份拷貝,我們可以利用Object類的clone()方法。
②在派生類中覆蓋基類的clone()方法,並宣告為public。
③在派生類的clone()方法中,呼叫super.clone()。
④在派生類中實現Cloneable介面。

請看如下程式碼:

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("zhangsan",18); 
  Student s2=(Student)s1.clone(); 
  s2.name="lisi"; 
  s2.age=20; 
  //修改學生2後,不影響學生1的值。
  System.out.println("name="+s1.name+","+"age="+s1.age); 
  System.out.println("name="+s2.name+","+"age="+s2.age);
 }
}

說明:

①為什麼我們在派生類中覆蓋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; 
  } 
} 
public 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(); 
  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);
  System.out.println("name="+s2.p.name+","+"age="+s2.p.age);
  //輸出結果學生1和2的教授成為lisi,age為30。
  } 
}

那應該如何實現深層次的克隆,即修改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; 
  } 
} 
public 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; 
  //學生1的教授不 改變。
  System.out.println("name="+s1.p.name+","+"age="+s1.p.age); 
  System.out.println("name="+s2.p.name+","+"age="+s2.p.age); 
 } 
}

3.利用序列化來做深複製(主要是為了避免重寫比較複雜物件的深複製的clone()方法,也可以程式實現斷點續傳等等功能)

把物件寫到流裡的過程是序列化(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 Teacher implements Serializable{
  String name;
  int age;
  public void Teacher(String name,int age){
  this.name=name;
  this.age=age;
  }
}
public class Student implements Serializable{
 String name;//常量物件
 int age;
 Teacher t;//學生1和學生2的引用值都是一樣的。
 public void Student(String name,int age,Teacher t){
  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){ 
  Teacher t=new Teacher("tangliang",30);
  Student s1=new Student("zhangsan",18,t);
  Student s2=(Student)s1.deepClone();
  s2.t.name="tony";
  s2.t.age=40;
  //學生1的老師不改變
  System.out.println("name="+s1.t.name+","+"age="+s1.t.age);
 }
}

相關文章