歡迎光臨
每天分享高質量文章

Java 面試:物件克隆

(給ImportNew加星標,提高Java技能)

 

轉自:部落格園,作者:萌小Q

連結:www.cnblogs.com/Qian123/p/5710533.html#_labelTop

 

假如說你想複製一個簡單變數。很簡單:

 

int apples = 5;
int pears = apples;

 

不僅僅是int型別,其它七種原始資料型別(boolean,char,byte,short,float,double.long)同樣適用於該類情況。

 

但是如果你複製的是一個物件,情況就有些複雜了。

 

假設說我是一個beginner,我會這樣寫:

 

class Student {
    private int number;
  
    public int getNumber() {
        return number;
    }
  
    public void setNumber(int number) {
        this.number = number;
    }
      
}
public class Test {
      
    public static void main(String args[]) {
        Student stu1 = new Student();
        stu1.setNumber(12345);
        Student stu2 = stu1;
          
        System.out.println("學生1:" + stu1.getNumber());
        System.out.println("學生2:" + stu2.getNumber());
    }
}

 

結果:

 

 

學生1:12345  

學生2:12345

 

這裡我們自定義了一個學生類,該類只有一個number欄位。

 

我們新建了一個學生實體,然後將該值賦值給stu2實體。(Student stu2 = stu1;)

 

再看看列印結果,作為一個新手,拍了拍胸腹,物件複製不過如此,

 

難道真的是這樣嗎?

 

我們試著改變stu2實體的number欄位,再列印結果看看:

 

stu2.setNumber(54321);
  
System.out.println("學生1:" + stu1.getNumber());
System.out.println("學生2:" + stu2.getNumber());

 

結果:

 

 

學生1:54321  

學生2:54321

 

這就怪了,為什麼改變學生2的學號,學生1的學號也發生了變化呢?

 

原因出在(stu2 = stu1) 這一句。該陳述句的作用是將stu1的取用賦值給stu2,

 

這樣,stu1和stu2指向記憶體堆中同一個物件。如圖:

 

 

那麼,怎樣才能達到複製一個物件呢?

 

是否記得萬類之王Object。它有11個方法,有兩個protected的方法,其中一個為clone方法。

 

在Java中所有的類都是預設的繼承自Java語言包中的Object類的,檢視它的原始碼,你可以把你的JDK目錄下的src.zip複製到其他地方然後解壓,裡面就是所有的原始碼。發現裡面有一個訪問限定符為protected的方法clone():

 

/*
Creates and returns a copy of this object. The precise meaning of "copy" may depend on the class of the object.
The general intent is that, for any object x, the expression:
1) x.clone() != x will be true
2) x.clone().getClass() == x.getClass() will be true, but these are not absolute requirements.
3) x.clone().equals(x) will be true, this is not an absolute requirement.
*/
protected native Object clone() throws CloneNotSupportedException;

 

仔細一看,它還是一個native方法,大家都知道native方法是非Java語言實現的程式碼,供Java程式呼叫的,因為Java程式是執行在JVM虛擬機器上面的,要想訪問到比較底層的與作業系統相關的就沒辦法了,只能由靠近作業系統的語言來實現。

 

  1. 第一次宣告保證克隆物件將有單獨的記憶體地址分配。
  2. 第二次宣告表明,原始和克隆的物件應該具有相同的型別別,但它不是強制性的。
  3. 第三宣告表明,原始和克隆的物件應該是平等的equals()方法使用,但它不是強制性的。

 

因為每個類直接或間接的父類都是Object,因此它們都含有clone()方法,但是因為該方法是protected,所以都不能在類外進行訪問。

 

要想對一個物件進行複製,就需要對clone方法改寫。

 

為什麼要克隆?

 

大家先思考一個問題,為什麼需要克隆物件?直接new一個物件不行嗎?

 

答案是:克隆的物件可能包含一些已經修改過的屬性,而new出來的物件的屬性都還是初始化時候的值,所以當需要一個新的物件來儲存當前物件的“狀態”就靠clone方法了。那麼我把這個物件的臨時屬性一個一個的賦值給我新new的物件不也行嘛?可以是可以,但是一來麻煩不說,二來,大家透過上面的原始碼都發現了clone是一個native方法,就是快啊,在底層實現的。

 

提個醒,我們常見的Object a=new Object();Object b;b=a;這種形式的程式碼複製的是取用,即物件在記憶體中的地址,a和b物件仍然指向了同一個物件。

 

而透過clone方法賦值的物件跟原來的物件是同時獨立存在的。

 

如何實現克隆

 

先介紹一下兩種不同的克隆方法,淺克隆(ShallowClone)深克隆(DeepClone)

 

在Java語言中,資料型別分為值型別(基本資料型別)和取用型別,值型別包括int、double、byte、boolean、char等簡單資料型別,取用型別包括類、介面、陣列等複雜型別。淺克隆和深克隆的主要區別在於是否支援取用型別的成員變數的複製,下麵將對兩者進行詳細介紹。

 

一般步驟是(淺克隆):

 

  1. 被覆制的類需要實現Clonenable介面(不實現的話在呼叫clone方法會丟擲CloneNotSupportedException異常), 該介面為標記介面(不含任何方法)
  2. 改寫clone()方法,訪問修飾符設為public。方法中呼叫super.clone()方法得到需要的複製物件。(native為本地方法)

 

下麵對上面那個方法進行改造:

 

class Student implements Cloneable{
    private int number;
  
    public int getNumber() {
        return number;
    }
  
    public void setNumber(int number) {
        this.number = number;
    }
      
    @Override
    public Object clone() {
        Student stu = null;
        try{
            stu = (Student)super.clone();
        }catch(CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return stu;
    }
}
public class Test {
    public static void main(String args[]) {
        Student stu1 = new Student();
        stu1.setNumber(12345);
        Student stu2 = (Student)stu1.clone();
          
        System.out.println("學生1:" + stu1.getNumber());
        System.out.println("學生2:" + stu2.getNumber());
          
        stu2.setNumber(54321);
      
        System.out.println("學生1:" + stu1.getNumber());
        System.out.println("學生2:" + stu2.getNumber());
    }
}

 

結果:

 

學生1:12345  

學生2:12345  

學生1:12345  

學生2:54321

 

如果你還不相信這兩個物件不是同一個物件,那麼你可以看看這一句:

 

System.out.println(stu1 == stu2); // false

 

上面的複製被稱為淺克隆。

 

還有一種稍微複雜的深度複製:

 

我們在學生類裡再加一個Address類。

 

class Address  {
    private String add;
  
    public String getAdd() {
        return add;
    }
  
    public void setAdd(String add) {
        this.add = add;
    }
      
}
  
class Student implements Cloneable{
    private int number;
  
    private Address addr;
      
    public Address getAddr() {
        return addr;
    }
  
    public void setAddr(Address addr) {
        this.addr = addr;
    }
  
    public int getNumber() {
        return number;
    }
  
    public void setNumber(int number) {
        this.number = number;
    }
      
    @Override
    public Object clone() {
        Student stu = null;
        try{
            stu = (Student)super.clone();
        }catch(CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return stu;
    }
}
public class Test {
      
    public static void main(String args[]) {
          
        Address addr = new Address();
        addr.setAdd("杭州市");
        Student stu1 = new Student();
        stu1.setNumber(123);
        stu1.setAddr(addr);
          
        Student stu2 = (Student)stu1.clone();
          
        System.out.println("學生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());
        System.out.println("學生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());
    }
}

 

結果:

 

學生1:123,地址:杭州市  

學生2:123,地址:杭州市

 

乍一看沒什麼問題,真的是這樣嗎?

 

我們在main方法中試著改變addr實體的地址。

 

addr.setAdd("西湖區");
  
System.out.println("學生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());
System.out.println("學生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());

 

結果:

 

 

學生1:123,地址:杭州市  
學生2:123,地址:杭州市  
學生1:123,地址:西湖區  
學生2:123,地址:西湖區

 

這就奇怪了,怎麼兩個學生的地址都改變了?

 

原因是淺複製只是複製了addr變數的取用,並沒有真正的開闢另一塊空間,將值複製後再將取用傳回給新物件。

 

所以,為了達到真正的複製物件,而不是純粹取用複製。我們需要將Address類可複製化,並且修改clone方法,完整程式碼如下:

 

package abc;
  
class Address implements Cloneable {
    private String add;
  
    public String getAdd() {
        return add;
    }
  
    public void setAdd(String add) {
        this.add = add;
    }
      
    @Override
    public Object clone() {
        Address addr = null;
        try{
            addr = (Address)super.clone();
        }catch(CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return addr;
    }
}
  
class Student implements Cloneable{
    private int number;
  
    private Address addr;
      
    public Address getAddr() {
        return addr;
    }
  
    public void setAddr(Address addr) {
        this.addr = addr;
    }
  
    public int getNumber() {
        return number;
    }
  
    public void setNumber(int number) {
        this.number = number;
    }
      
    @Override
    public Object clone() {
        Student stu = null;
        try{
            stu = (Student)super.clone();   //淺複製
        }catch(CloneNotSupportedException e) {
            e.printStackTrace();
        }
        stu.addr = (Address)addr.clone();   //深度複製
        return stu;
    }
}
public class Test {
      
    public static void main(String args[]) {
          
        Address addr = new Address();
        addr.setAdd("杭州市");
        Student stu1 = new Student();
        stu1.setNumber(123);
        stu1.setAddr(addr);
          
        Student stu2 = (Student)stu1.clone();
          
        System.out.println("學生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());
        System.out.println("學生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());
          
        addr.setAdd("西湖區");
          
        System.out.println("學生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());
        System.out.println("學生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());
    }
}

 

結果: 

 

學生1:123,地址:杭州市  
學生2:123,地址:杭州市  
學生1:123,地址:西湖區  
學生2:123,地址:杭州市

 

這樣結果就符合我們的想法了。

 

最後我們可以看看API裡其中一個實現了clone方法的類:

 

java.util.Date:

 

/**
 * Return a copy of this object.
 */  
public Object clone() {
    Date d = null;
    try {
        d = (Date)super.clone();
        if (cdate != null) {  
            d.cdate = (BaseCalendar.Date) cdate.clone();
        }
    } catch (CloneNotSupportedException e) {} // Won't happen
    return d;
}

 

該類其實也屬於深度複製。

 

淺克隆和深克隆

 

1、淺克隆

 

在淺克隆中,如果原型物件的成員變數是值型別,將複製一份給克隆物件;如果原型物件的成員變數是取用型別,則將取用物件的地址複製一份給克隆物件,也就是說原型物件和克隆物件的成員變數指向相同的記憶體地址。

 

簡單來說,在淺克隆中,當物件被覆制時只複製它本身和其中包含的值型別的成員變數,而取用型別的成員物件並沒有複製。

 

 

在Java語言中,透過改寫Object類的clone()方法可以實現淺克隆

 

2、深克隆

 

在深克隆中,無論原型物件的成員變數是值型別還是取用型別,都將複製一份給克隆物件,深克隆將原型物件的所有取用物件也複製一份給克隆物件。

 

簡單來說,在深克隆中,除了物件本身被覆制外,物件所包含的所有成員變數也將複製。

 

 

在Java語言中,如果需要實現深克隆,可以透過改寫Object類的clone()方法實現,也可以透過序列化(Serialization)等方式來實現。

 

(如果取用型別裡面還包含很多取用型別,或者內層取用型別的類裡面又包含取用型別,使用clone方法就會很麻煩。這時我們可以用序列化的方式來實現物件的深克隆。)

 

序列化就是將物件寫到流的過程,寫到流中的物件是原有物件的一個複製,而原物件仍然存在於記憶體中。透過序列化實現的複製不僅可以複製物件本身,而且可以複製其取用的成員物件,因此透過序列化將物件寫到一個流中,再從流裡將其讀出來,可以實現深克隆。需要註意的是能夠實現序列化的物件其類必須實現Serializable介面,否則無法實現序列化操作。

 

擴充套件

 

Java語言提供的Cloneable介面和Serializable介面的程式碼非常簡單,它們都是空介面,這種空介面也稱為標識介面,標識介面中沒有任何方法的定義,其作用是告訴JRE這些介面的實現類是否具有某個功能,如是否支援克隆、是否支援序列化等。

 

解決多層克隆問題

 

如果取用型別裡面還包含很多取用型別,或者內層取用型別的類裡面又包含取用型別,使用clone方法就會很麻煩。這時我們可以用序列化的方式來實現物件的深克隆。

 

public class Outer implements Serializable{
  private static final long serialVersionUID = 369285298572941L;  //最好是顯式宣告ID
  public Inner inner;
 //Discription:[深度複製方法,需要物件及物件所有的物件屬性都實現序列化] 
  public Outer myclone() {
      Outer outer = null;
      try { // 將該物件序列化成流,因為寫在流裡的是物件的一個複製,而原物件仍然存在於JVM裡面。所以利用這個特性可以實現物件的深複製
          ByteArrayOutputStream baos = new ByteArrayOutputStream();
          ObjectOutputStream oos = new ObjectOutputStream(baos);
          oos.writeObject(this);
      // 將流序列化成物件
          ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
          ObjectInputStream ois = new ObjectInputStream(bais);
          outer = (Outer) ois.readObject();
      } catch (IOException e) {
          e.printStackTrace();
      } catch (ClassNotFoundException e) {
          e.printStackTrace();
      }
      return outer;
  }
}

 

Inner也必須實現Serializable,否則無法序列化:

 

public class Inner implements Serializable{
  private static final long serialVersionUID = 872390113109L; //最好是顯式宣告ID
  public String name = "";

  public Inner(String name) {
      this.name = name;
  }

  @Override
  public String toString() {
      return "Inner的name值為:" + name;
  }
}

 

這樣也能使兩個物件在記憶體空間內完全獨立存在,互不影響對方的值。

 

總結

 

實現物件克隆有兩種方式:

 

  1. 實現Cloneable介面並重寫Object類中的clone()方法;
  2. 實現Serializable介面,透過物件的序列化和反序列化實現克隆,可以實現真正的深度克隆。

 

註意:基於序列化和反序列化實現的克隆不僅僅是深度克隆,更重要的是透過泛型限定,可以檢查出要克隆的物件是否支援序列化,這項檢查是編譯器完成的,不是在執行時丟擲異常,這種是方案明顯優於使用Object類的clone方法克隆物件。讓問題在編譯的時候暴露出來總是優於把問題留到執行時。

已同步到看一看
贊(0)

分享創造快樂