Java提高篇——對象克虏芴濉(復(fù)制)

假如說你想復(fù)制一個簡單變量包蓝。很簡單:

int apples = 5;  
int pears = apples;  

不僅僅是int類型,其它七種原始數(shù)據(jù)類型(boolean,char,byte,short,float,double.long)同樣適用于該類情況堵腹。

但是如果你復(fù)制的是一個對象,情況就有些復(fù)雜了星澳。

假設(shè)說我是一個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("學(xué)生1:" + stu1.getNumber());  
        System.out.println("學(xué)生2:" + stu2.getNumber());  
    }  
}

結(jié)果:

學(xué)生1:12345

學(xué)生2:12345

這里我們自定義了一個學(xué)生類,該類只有一個number字段禁偎。

我們新建了一個學(xué)生實例腿堤,然后將該值賦值給stu2實例。(Student stu2 = stu1;)

再看看打印結(jié)果如暖,作為一個新手笆檀,拍了拍胸腹,對象復(fù)制不過如此盒至,

難道真的是這樣嗎酗洒?

我們試著改變stu2實例的number字段士修,再打印結(jié)果看看:

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

結(jié)果:

學(xué)生1:54321

學(xué)生2:54321

這就怪了,為什么改變學(xué)生2的學(xué)號樱衷,學(xué)生1的學(xué)號也發(fā)生了變化呢棋嘲?

原因出在(stu2 = stu1) 這一句。該語句的作用是將stu1的引用賦值給stu2矩桂,

這樣沸移,stu1和stu2指向內(nèi)存堆中同一個對象。如圖:


690102-20160727114833309-1421006998.png

那么侄榴,怎樣才能達到復(fù)制一個對象呢雹锣?

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

在Java中所有的類都是缺省的繼承自Java語言包中的Object類的涣达,查看它的源碼在辆,你可以把你的JDK目錄下的src.zip復(fù)制到其他地方然后解壓,里面就是所有的源碼度苔。發(fā)現(xiàn)里面有一個訪問限定符為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語言實現(xiàn)的代碼寇窑,供Java程序調(diào)用的鸦概,因為Java程序是運行在JVM虛擬機上面的,要想訪問到比較底層的與操作系統(tǒng)相關(guān)的就沒辦法了甩骏,只能由靠近操作系統(tǒng)的語言來實現(xiàn)窗市。

第一次聲明保證克隆對象將有單獨的內(nèi)存地址分配。
第二次聲明表明饮笛,原始和克隆的對象應(yīng)該具有相同的類類型咨察,但它不是強制性的。
第三聲明表明福青,原始和克隆的對象應(yīng)該是平等的equals()方法使用摄狱,但它不是強制性的。
因為每個類直接或間接的父類都是Object无午,因此它們都含有clone()方法媒役,但是因為該方法是protected,所以都不能在類外進行訪問宪迟。

要想對一個對象進行復(fù)制酣衷,就需要對clone方法覆蓋。

為什么要克虏妊椤鸥诽?

大家先思考一個問題商玫,為什么需要克隆對象?直接new一個對象不行嗎牡借?

答案是:克隆的對象可能包含一些已經(jīng)修改過的屬性拳昌,而new出來的對象的屬性都還是初始化時候的值,所以當(dāng)需要一個新的對象來保存當(dāng)前對象的“狀態(tài)”就靠clone方法了钠龙。那么我把這個對象的臨時屬性一個一個的賦值給我新new的對象不也行嘛炬藤?可以是可以,但是一來麻煩不說碴里,二來沈矿,大家通過上面的源碼都發(fā)現(xiàn)了clone是一個native方法,就是快啊咬腋,在底層實現(xiàn)的羹膳。

提個醒,我們常見的Object a=new Object();Object b;b=a;這種形式的代碼復(fù)制的是引用根竿,即對象在內(nèi)存中的地址陵像,a和b對象仍然指向了同一個對象。

而通過clone方法賦值的對象跟原來的對象時同時獨立存在的寇壳。

如何實現(xiàn)克隆

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

在Java語言中壳炎,數(shù)據(jù)類型分為值類型(基本數(shù)據(jù)類型)和引用類型泞歉,值類型包括int、double匿辩、byte腰耙、boolean、char等簡單數(shù)據(jù)類型铲球,引用類型包括類沟优、接口、數(shù)組等復(fù)雜類型睬辐。淺克隆和深克隆的主要區(qū)別在于是否支持引用類型的成員變量的復(fù)制,下面將對兩者進行詳細介紹宾肺。

一般步驟是(淺克滤荻):

  1. 被復(fù)制的類需要實現(xiàn)Clonenable接口(不實現(xiàn)的話在調(diào)用clone方法會拋出CloneNotSupportedException異常), 該接口為標(biāo)記接口(不含任何方法)

  2. 覆蓋clone()方法锨用,訪問修飾符設(shè)為public丰刊。方法中調(diào)用super.clone()方法得到需要的復(fù)制對象。(native為本地方法)

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

    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("學(xué)生1:" + stu1.getNumber());  
        System.out.println("學(xué)生2:" + stu2.getNumber());  
          
        stu2.setNumber(54321);  
      
        System.out.println("學(xué)生1:" + stu1.getNumber());  
        System.out.println("學(xué)生2:" + stu2.getNumber());  
    }  
}

結(jié)果:

學(xué)生1:12345

學(xué)生2:12345

學(xué)生1:12345

學(xué)生2:54321

如果你還不相信這兩個對象不是同一個對象增拥,那么你可以看看這一句:

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

上面的復(fù)制被稱為淺克隆啄巧。

還有一種稍微復(fù)雜的深度復(fù)制:

我們在學(xué)生類里再加一個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("學(xué)生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());  
        System.out.println("學(xué)生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());  
    }  
}

結(jié)果:

學(xué)生1:123,地址:杭州市

學(xué)生2:123,地址:杭州市

乍一看沒什么問題,真的是這樣嗎秩仆?

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

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

結(jié)果:

學(xué)生1:123,地址:杭州市  
學(xué)生2:123,地址:杭州市  
學(xué)生1:123,地址:西湖區(qū)  
學(xué)生2:123,地址:西湖區(qū) 

這就奇怪了,怎么兩個學(xué)生的地址都改變了澄耍?

原因是淺復(fù)制只是復(fù)制了addr變量的引用噪珊,并沒有真正的開辟另一塊空間,將值復(fù)制后再將引用返回給新對象齐莲。

所以痢站,為了達到真正的復(fù)制對象,而不是純粹引用復(fù)制选酗。我們需要將Address類可復(fù)制化阵难,并且修改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();   //淺復(fù)制  
        }catch(CloneNotSupportedException e) {  
            e.printStackTrace();  
        }  
        stu.addr = (Address)addr.clone();   //深度復(fù)制  
        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("學(xué)生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());  
        System.out.println("學(xué)生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());  
          
        addr.setAdd("西湖區(qū)");  
          
        System.out.println("學(xué)生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());  
        System.out.println("學(xué)生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());  
    }  
}

結(jié)果:

學(xué)生1:123,地址:杭州市  
學(xué)生2:123,地址:杭州市  
學(xué)生1:123,地址:西湖區(qū)  
學(xué)生2:123,地址:杭州市  

這樣結(jié)果就符合我們的想法了芒填。

最后我們可以看看API里其中一個實現(xiàn)了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;  
}

該類其實也屬于深度復(fù)制呜叫。

參考文檔:Java如何復(fù)制對象

淺克隆和深克隆

1、淺克隆

在淺克隆中氢烘,如果原型對象的成員變量是值類型怀偷,將復(fù)制一份給克隆對象;如果原型對象的成員變量是引用類型播玖,則將引用對象的地址復(fù)制一份給克隆對象椎工,也就是說原型對象和克隆對象的成員變量指向相同的內(nèi)存地址。

簡單來說蜀踏,在淺克隆中维蒙,當(dāng)對象被復(fù)制時只復(fù)制它本身和其中包含的值類型的成員變量,而引用類型的成員對象并沒有復(fù)制果覆。

image

在Java語言中颅痊,通過覆蓋Object類的clone()方法可以實現(xiàn)淺克隆

2局待、深克隆

在深克隆中斑响,無論原型對象的成員變量是值類型還是引用類型,都將復(fù)制一份給克隆對象钳榨,深克隆將原型對象的所有引用對象也復(fù)制一份給克隆對象舰罚。

簡單來說,在深克隆中薛耻,除了對象本身被復(fù)制外营罢,對象所包含的所有成員變量也將復(fù)制。

image

在Java語言中饼齿,如果需要實現(xiàn)深克隆饲漾,可以通過覆蓋Object類的clone()方法實現(xiàn)蝙搔,也可以通過序列化(Serialization)等方式來實現(xiàn)。

如果引用類型里面還包含很多引用類型考传,或者內(nèi)層引用類型的類里面又包含引用類型吃型,使用clone方法就會很麻煩。這時我們可以用序列化的方式來實現(xiàn)對象的深克隆伙菊。

序列化就是將對象寫到流的過程败玉,寫到流中的對象是原有對象的一個拷貝,而原對象仍然存在于內(nèi)存中镜硕。通過序列化實現(xiàn)的拷貝不僅可以復(fù)制對象本身运翼,而且可以復(fù)制其引用的成員對象,因此通過序列化將對象寫到一個流中兴枯,再從流里將其讀出來血淌,可以實現(xiàn)深克隆。需要注意的是能夠?qū)崿F(xiàn)序列化的對象其類必須實現(xiàn)Serializable接口财剖,否則無法實現(xiàn)序列化操作悠夯。

<caption style="margin: 0px; padding: 0px;">擴展</caption>
| Java語言提供的Cloneable接口和Serializable接口的代碼非常簡單,它們都是空接口躺坟,這種空接口也稱為標(biāo)識接口沦补,標(biāo)識接口中沒有任何方法的定義,其作用是告訴JRE這些接口的實現(xiàn)類是否具有某個功能咪橙,如是否支持克隆夕膀、是否支持序列化等。 |

解決多層克隆問題

如果引用類型里面還包含很多引用類型美侦,或者內(nèi)層引用類型的類里面又包含引用類型产舞,使用clone方法就會很麻煩。這時我們可以用序列化的方式來實現(xiàn)對象的深克隆菠剩。

public class Outer implements Serializable{
  private static final long serialVersionUID = 369285298572941L;  //最好是顯式聲明ID
  public Inner inner;
 //Discription:[深度復(fù)制方法,需要對象及對象所有的對象屬性都實現(xiàn)序列化] 
  public Outer myclone() {
      Outer outer = null;
      try { // 將該對象序列化成流,因為寫在流里的是對象的一個拷貝易猫,而原對象仍然存在于JVM里面。所以利用這個特性可以實現(xiàn)對象的深拷貝
          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也必須實現(xiàn)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;
  }
}

這樣也能使兩個對象在內(nèi)存空間內(nèi)完全獨立存在准颓,互不影響對方的值。

總結(jié)

實現(xiàn)對象克隆有兩種方式:

1. 實現(xiàn)Cloneable接口并重寫Object類中的clone()方法棺妓;

2. 實現(xiàn)Serializable接口瞬场,通過對象的序列化和反序列化實現(xiàn)克隆,可以實現(xiàn)真正的深度克隆涧郊。

注意:基于序列化和反序列化實現(xiàn)的克隆不僅僅是深度克隆,更重要的是通過泛型限定眼五,可以檢查出要克隆的對象是否支持序列化妆艘,這項檢查是編譯器完成的彤灶,不是在運行時拋出異常,這種是方案明顯優(yōu)于使用Object類的clone方法克隆對象批旺。讓問題在編譯的時候暴露出來總是優(yōu)于把問題留到運行時幌陕。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市汽煮,隨后出現(xiàn)的幾起案子搏熄,更是在濱河造成了極大的恐慌,老刑警劉巖暇赤,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件心例,死亡現(xiàn)場離奇詭異,居然都是意外死亡鞋囊,警方通過查閱死者的電腦和手機止后,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來溜腐,“玉大人译株,你說我怎么就攤上這事⊥σ妫” “怎么了歉糜?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長望众。 經(jīng)常有香客問我匪补,道長,這世上最難降的妖魔是什么黍檩? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任叉袍,我火速辦了婚禮,結(jié)果婚禮上刽酱,老公的妹妹穿的比我還像新娘喳逛。我一直安慰自己,他們只是感情好棵里,可當(dāng)我...
    茶點故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布润文。 她就那樣靜靜地躺著,像睡著了一般殿怜。 火紅的嫁衣襯著肌膚如雪典蝌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天头谜,我揣著相機與錄音骏掀,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛截驮,可吹牛的內(nèi)容都是我干的笑陈。 我是一名探鬼主播,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼葵袭,長吁一口氣:“原來是場噩夢啊……” “哼涵妥!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起坡锡,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蓬网,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后鹉勒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體帆锋,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年贸弥,在試婚紗的時候發(fā)現(xiàn)自己被綠了窟坐。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡绵疲,死狀恐怖哲鸳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情盔憨,我是刑警寧澤徙菠,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站郁岩,受9級特大地震影響婿奔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜问慎,卻給世界環(huán)境...
    茶點故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一萍摊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧如叼,春花似錦冰木、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至社证,卻和暖如春逼龟,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背追葡。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工腺律, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留奕短,地道東北人。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓匀钧,卻偏偏與公主長得像篡诽,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子榴捡,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,573評論 2 359

推薦閱讀更多精彩內(nèi)容