Java編程實現對象複製(複製)代碼詳解,java詳解
複製,想必大家都有耳聞,世界上第一隻複製羊多莉就是利用細胞核移植技術將哺乳動物的成年體細胞培育出新個體,甚為神奇。其實在Java中也存在複製的概念,即實現對象的複製。
本文將嘗試介紹一些關於Java中的複製和一些深入的問題,希望可以協助大家更好地瞭解複製。
假如說你想複製一個簡單變數。很簡單:
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 true2) 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虛擬機器上面的,要想訪問到比較底層的與作業系統相關的就沒辦法了,只能由靠近作業系統的語言來實現。
第一次聲明保證複製對象將有單獨的記憶體位址分配。
第二次聲明表明,原始和複製的對象應該具有相同的類類型,但它不是強制性的。
第三聲明表明,原始和複製的對象應該是平等的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語言中,資料類型分為實值型別(基礎資料型別 (Elementary Data Type))和參考型別,實值型別包括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方法複製對象。讓問題在編譯的時候暴露出來總是優於把問題留到運行時。
以上就是本文關於Java編程實現對象複製(複製)代碼詳解的全部內容,希望對大家有所協助。感興趣的來朋友可以繼續參閱本站其他相關專題,如有不足之處,歡迎留言指出。