深入瞭解Java物件的克隆
今天要介紹一個概念,物件的克隆。本篇有一定難度,請先做好心理準備。看不懂的話可以多看兩遍,還是不懂的話,可以在下方留言,我會看情況進行修改和補充。
克隆,自然就是將物件重新複製一份,那為什麼要用克隆呢?什麼時候需要使用呢?先來看一個小栗子:
簡單起見,我們這裡用的是Goods類的簡單版本。
public class Goods { private String title; private double price; public Goods(String aTitle,double aPrice){ title = aTitle; price = aPrice; } public void setPrice(double price) { this.price = price; } public void setTitle(String title) { this.title = title; } //用於列印輸出商品資訊 public void print(){ System.out.println("Title:"+title+" Price:"+price); } }
然後我們來使用這個類。
public class GoodsTest { public static void main(String[] args){ Goods goodsA = new Goods("GoodsA",20); Goods goodsB = goodsA; System.out.println("Before Change:"); goodsA.print(); goodsB.print(); goodsB.setTitle("GoodsB"); goodsB.setPrice(50); System.out.println("After Change:"); goodsA.print(); goodsB.print(); } }
我們建立了一個Goods物件賦值給變數goodsA,然後又建立了一個Goods變數,並把goodsA賦值給它,先呼叫Goods的print方法輸出這兩個變數中的資訊,然後呼叫Goods類中的setTitle和setPrice方法來修改goodsB中的物件內容,再輸出兩個變數中的資訊,下面是輸出:
Before Change:
Title:GoodsA Price:20.0
Title:GoodsA Price:20.0
After Change:
Title:GoodsB Price:50.0
Title:GoodsB Price:50.0
這裡我們發現了靈異事,我們明明修改的是goodsB的內容,可是goodsA的內容也同樣發生了改變,這究竟是為什麼呢?別心急,且聽我慢慢道來。
在Java語言中,資料型別分為值型別(基本資料型別)和引用型別,值型別包括int、double、byte、boolean、char等簡單資料型別,引用型別包括類、介面、陣列等複雜型別。使用等號賦值都是進行值傳遞的,如將一個整數型變數賦值給另一個整數型變數,那麼後者將儲存前者的值,也就是變數中的整數值,對於基本型別如int,double,char等是沒有問題的,但是對於物件,則又是另一回事了,這裡的goodsA和goodsB都是Goods類物件的變數,但是它們並沒有儲存Goods類物件的內容,而是儲存了它的地址,也就相當於C++中的指標,如果對於指標不瞭解,那我就再舉個栗子好了。我們之前舉過一個栗子,把計算機比作是倉庫管理員,記憶體比作是倉庫,你要使用什麼型別的變數,就需要先登記,然後管理員才會把東西給你,但如果是給你分配一座房子呢?這時候不是把房子搬起來放到登記簿粒,而是登記下房子的地址,這裡的地址就是我們的類物件變數裡記錄的內容,所以,當我們把一個類物件變數賦值給另一個類物件變數,如goodsB = goodsA時,實際上只是把A指向的物件地址賦值給了B,這樣B也同樣指向這個地址,所以這時候,goodsA和goodsB操作的是同一個物件。
所以,如果只是簡單的賦值的話,之後對於goodsA和goodsB的操作都將影響同一個物件,這顯然不是我們的本意。也許你還會問,直接再new一個物件不就好了,確實如此,但有時候,如果我們需要儲存一個goodsA的副本,那就不僅僅要new一個物件,還需要進行一系列賦值操作才能將我們的新物件設定成跟goodsA物件一樣,而且Goods類越複雜,這個操作將會越繁瑣,另外使用clone方法還進行本地優化,效率上也會快很多,總而言之,就是簡單粗暴。
那如何使用克隆呢?這裡我們就要介紹我們牛逼哄哄的Object類了,所有的類都是Object類的子類,雖然我們並沒有顯式宣告繼承關係,但所有類都難逃它的魔掌,它有兩個protected方法,其中一個就是clone方法。
下面我來展示一波正確的騷操作:
//要使用克隆方法需要實現Cloneable介面 public class Goods implements Cloneable{ private String title; private double price; public Goods(String aTitle,double aPrice){ title = aTitle; price = aPrice; } public void setPrice(double price) { this.price = price; } public void setTitle(String title) { this.title = title; } public void print(){ System.out.println("Title:"+title+" Price:"+price); } //這裡過載了介面的clone方法 @Override protected Object clone(){ Goods g = null; //這裡是異常處理的語句塊,可以先不用瞭解,只要知道是這樣使用就好,之後的文章中會有詳細的介紹 try{ g = (Goods)super.clone(); }catch (CloneNotSupportedException e){ System.out.println(e.toString()); } return g; } }
其實修改的地方只有兩個,一個是定義類的時候實現了Cloneable介面,關於介面的知識在之後會有詳細說明,這裡只要簡單理解為是一種規範就行了,然後我們過載了clone方法,並在裡面呼叫了父類也就是(Object)的clone方法。可以看到我們並沒有new一個新的物件,而是使用父類的clone方法進行克隆,關於try catch的知識這裡不做過多介紹,之後會有文章做詳細說明,這裡只需要理解為try語句塊裡是一個可能發生錯誤的程式碼,catch會捕獲這種錯誤並進行處理。
接下來我們再使用這個類的克隆方法:
public class GoodsTest { public static void main(String[] args){ Goods goodsA = new Goods("GoodsA",20); Goods goodsB = (Goods)goodsA.clone(); System.out.println("Before Change:"); goodsA.print(); goodsB.print(); goodsB.setTitle("GoodsB"); goodsB.setPrice(50); System.out.println("After Change:"); goodsA.print(); goodsB.print(); } }
我們僅僅是把賦值改成了呼叫goodsA的clone方法並進行型別轉換。輸出如下:
Before Change:
Title:GoodsA Price:20.0
Title:GoodsA Price:20.0
After Change:
Title:GoodsA Price:20.0
Title:GoodsB Price:50.0
看,這樣不就達到我們目的了嗎?是不是很簡單?
但是別高興的太早,關於克隆,還有一點內容需要介紹。
克隆分為淺克隆和深克隆。我們上面使用的只是淺克隆,那兩者有什麼區別呢?這裡再舉一個栗子,使用的是簡化版的Cart類:
public class Cart implements Cloneable{ //例項域 Goods goodsList = new Goods("",0);//簡單起見,這裡只放了一個商品 double budget = 0.0;//預算 //建構函式 public Cart(double aBudget){ budget = aBudget; } //獲取預算 public double getBudget() { return budget; } //修改預算 public void setBudget(double aBudget) { budget = aBudget; } //這裡只是簡單的將商品進行了賦值 public void addGoods(Goods goods){ goodsList = (Goods) goods.clone(); } //這是為了演示加上的程式碼,僅僅將商品標題修改成新標題 public void changeGoodsTitle(String title){ goodsList.setTitle(title); } //列印商品資訊 public void print(){ System.out.print("Cart內的預算資訊:"+budget+" 商品資訊:"); goodsList.print(); } //過載clone方法 @Override protected Object clone(){ Cart c = null; try{ c = (Cart)super.clone(); }catch (CloneNotSupportedException e ){ e.printStackTrace(); } return c; } }
這裡將goodsList由陣列改成了單個物件變數,僅僅用於演示方便,還增加了一個changeGoodsTitle方法,用於將商品的標題修改成另一個標題,接下來修改一下GoodsTest類:
public class GoodsTest { public static void main(String[] args){ Goods goodsA = new Goods("GoodsA",20);//新建一個商品物件 Cart cartA = new Cart(5000);//新建一個購物車物件 cartA.addGoods(goodsA);//新增商品 Cart cartB = (Cart) cartA.clone();//使用淺克隆 //輸出修改前資訊 System.out.println("Before Change:"); cartA.print(); cartB.print(); //修改購物車A中的商品標題 cartA.changeGoodsTitle("NewTitle"); //重新輸出修改後的資訊 System.out.println("After Change:"); cartA.print(); cartB.print(); } }
輸出資訊:
Before Change:
Cart內的預算資訊:5000.0 商品資訊:Title:GoodsA Price:20.0
Cart內的預算資訊:5000.0 商品資訊:Title:GoodsA Price:20.0
After Change:
Cart內的預算資訊:5000.0 商品資訊:Title:NewTitle Price:20.0
Cart內的預算資訊:5000.0 商品資訊:Title:NewTitle Price:20.0
我們發現,雖然我們呼叫的是cartA中的方法修改購物車A中的商品資訊,但購物車B中的資訊同樣被修改了,這是因為使用淺克隆模式的時候,成員變數如果是物件等複雜型別時,僅僅使用的是值拷貝,就跟我們之前介紹的那樣,所以cartB雖然是cartA的一個拷貝,但是它們的成員變數goodsList卻共用一個物件,這樣就藕斷絲連了,顯然不是我們想要的效果,這時候就需要使用深拷貝了,只需要將Cart類的clone方法修改一下即可:
@Override protected Object clone(){ Cart c = null; try{ c = (Cart)super.clone(); c.goodsList = (Goods) goodsList.clone();//僅僅添加了這段程式碼,將商品物件也進行了克隆 }catch (CloneNotSupportedException e ){ e.printStackTrace(); } return c; }
現在再來執行一下:
Before Change:
Cart內的預算資訊:5000.0 商品資訊:Title:GoodsA Price:20.0
Cart內的預算資訊:5000.0 商品資訊:Title:GoodsA Price:20.0
After Change:
Cart內的預算資訊:5000.0 商品資訊:Title:NewTitle Price:20.0
Cart內的預算資訊:5000.0 商品資訊:Title:GoodsA Price:20.0
這樣就得到了我們想要的結果了。
這樣,物件的拷貝就講完了。
嗎?
哈哈哈哈,不要崩潰,並沒有,還有一種更復雜的情況,那就是當你的成員變數裡也包含引用型別的時候,比如Cart類中有一個CartB類的成員變數,CartB類中同樣存在引用型別的成員變數,這時候,就存在多層克隆的問題了。這裡再介紹一個騷操作,只需要瞭解即可,那就是序列化物件。操作如下:
import java.io.*; public class Cart implements Serializable{ //例項域 Goods goodsList = new Goods("",0);//簡單起見,這裡只放了一個商品 double budget = 0.0;//預算 //建構函式 public Cart(double aBudget){ budget = aBudget; } //獲取預算 public double getBudget() { return budget; } //修改預算 public void setBudget(double aBudget) { budget = aBudget; } //這裡只是簡單的將商品進行了賦值 public void addGoods(Goods goods){ goodsList = (Goods) goods.clone(); } //這是為了演示加上的程式碼,僅僅將商品標題修改成新標題 public void changeGoodsTitle(String title){ goodsList.setTitle(title); } //列印商品資訊 public void print(){ System.out.print("Cart內的預算資訊:"+budget+" 商品資訊:"); goodsList.print(); } //這裡是主要是騷操作 public Object deepClone() throws IOException,OptionalDataException,ClassNotFoundException { // 將物件寫到流裡 ByteArrayOutputStream 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()); } }
關於這種方法我就不多做介紹了,大家只需要知道有這樣一種方法就行了,以後如果遇到了需要使用這種情況,就知道該怎樣處理了。
這裡總結一下,物件的克隆就是把一個物件的當前狀態重新拷貝一份到另一個新物件中,兩個物件變數指向不同的物件,淺克隆僅僅呼叫super.clone()方法,對成員變數也只是簡單的值拷貝,所以當成員變數中有陣列,物件等複雜型別的時候,就會存在藕斷絲連的混亂關係,深拷貝不僅僅呼叫super.clone()方法進行物件拷貝,將物件中的複雜型別同樣進行了拷貝,這樣兩個物件就再無瓜葛,井水不犯河水了。
至此,物件的克隆就真正的結束了,歡迎大家繼續關注!如有不懂的問題可以留言。也歡迎各位大佬來批評指正。喜歡我的教程的話記得動動小手點下推薦,也歡迎關注我的部落格。
以上就是深入瞭解Java物件的克隆的詳細內容,更多關於Java 克隆的資料請關注我們其它相關文章!