假如說你想復(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)存堆中同一個對象。如圖:
那么侄榴,怎樣才能達到復(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ù)制,下面將對兩者進行詳細介紹宾肺。
一般步驟是(淺克滤荻):
被復(fù)制的類需要實現(xiàn)Clonenable接口(不實現(xiàn)的話在調(diào)用clone方法會拋出CloneNotSupportedException異常), 該接口為標(biāo)記接口(不含任何方法)
覆蓋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ù)制果覆。
在Java語言中颅痊,通過覆蓋Object類的clone()方法可以實現(xiàn)淺克隆。
2局待、深克隆
在深克隆中斑响,無論原型對象的成員變量是值類型還是引用類型,都將復(fù)制一份給克隆對象钳榨,深克隆將原型對象的所有引用對象也復(fù)制一份給克隆對象舰罚。
簡單來說,在深克隆中薛耻,除了對象本身被復(fù)制外营罢,對象所包含的所有成員變量也將復(fù)制。
在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)于把問題留到運行時幌陕。