天天看點

【JAVA零基礎入門系列】Day14 Java對象的克隆

  【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()方法進行對象拷貝,将對象中的複雜類型同樣進行了拷貝,這樣兩個對象就再無瓜葛,井水不犯河水了。

  至此,對象的克隆就真正的結束了,歡迎大家繼續關注!如有不懂的問題可以留言。也歡迎各位大佬來批評指正。喜歡我的教程的話記得動動小手點下推薦,也歡迎關注我的部落格。

真正重要的東西,用眼睛是看不見的。