淺複製、深複製與序列化【初級Java必需理解的概念】

救苦救难韩天尊發表於2024-06-17

淺複製

首先建立兩個類,方便理解淺複製

@Data
class Student implements Cloneable{
    //年齡和名字是基本屬性
    private int age;
    private String name;
    //書包是引用屬性
    private Bag bag;

    public Student(int age, String name, Bag bag) {
        this.age = age;
        this.name = name;
        this.bag = bag;
    }

    @Override
    public String toString() {
        return "age=" + age + ", name='" + name + ", bag=" + bag;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
@Data
class Bag {
    private String color;
    private int price;

    public Bag(String color, int price) {
        this.color = color;
        this.price = price;
    }

    @Override
    public String toString() {
        return "color='" + color + ", price=" + price;
    }
}

Cloneable 介面只是一個標記介面(沒屬性和方法):

public interface Cloneable {
}

標記介面的作用其實很簡單,用來表示某個功能在執行的時候是合法的。

如果不實現Cloneable介面直接重寫並呼叫clone()方法,會丟擲 CloneNotSupportedException 異常。

測試類

class TestClone {
    public static void main(String[] args) throws CloneNotSupportedException {
        Student student1 = new Student(18, "張三", new Bag("紅",100));
        Student student2 = (Student) student1.clone();

        System.out.println("淺複製後:");
        System.out.println("student1:" + student1);
        System.out.println("student2:" + student2);

        //修改非引用型別屬性name
        student2.setName("李四");

        //修改引用型別屬性bag
        Bag bag = student2.getBag();
        bag.setColor("藍");
        bag.setPrice(200);

        System.out.println("修改了 student2 的 name 和 bag 後:");
        System.out.println("student1:" + student1);
        System.out.println("student2:" + student2);

    }
}

//列印結果
淺複製後:
student1:age=18, name='張三, bag=color='紅, price=100
student2:age=18, name='張三, bag=color='紅, price=100
修改了 student2 的 name 和 bag 後:
student1:age=18, name='張三, bag=color='藍, price=200
student2:age=18, name='李四, bag=color='藍, price=200

可以看得出,淺複製後:

修改了student2的非引用型別屬性name,student1的name並不會跟著改變

但修改了student2的引用型別屬性bag,student1的bag跟著改變了

說明淺複製克隆的物件中,引用型別的欄位指向的是同一個,當改變任何一個物件,另外一個物件也會隨之改變。

深複製

深複製和淺複製不同的,深複製中的引用型別欄位也會克隆一份,當改變任何一個物件,另外一個物件不會隨之改變。

例子

@Data
class Bag implements Cloneable {
    private String color;
    private int price;

    public Bag(String color, int price) {
        this.color = color;
        this.price = price;
    }

    @Override
    public String toString() {
        return "color='" + color + ", price=" + price;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

注意,此時的 Bag 類和淺複製時不同,重寫了 clone() 方法,並實現了 Cloneable 介面。為的就是深複製的時候也能夠克隆該欄位。

@Data
class Student implements Cloneable{
    //年齡和名字是基本屬性
    private int age;
    private String name;
    //書包是引用屬性
    private Bag bag;

    public Student(int age, String name, Bag bag) {
        this.age = age;
        this.name = name;
        this.bag = bag;
    }

    @Override
    public String toString() {
        return "age=" + age + ", name='" + name + ", bag=" + bag;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Student s = (Student) super.clone();
        s.setBag((Bag) s.getBag().clone());
        return s;
    }

}

注意,此時 Student 類也與之前的不同,clone() 方法當中,不再只呼叫 Object 的 clone() 方法對 Student 進行克隆了,還對 Bag 也進行了克隆。

來看測試類

class TestClone {
    public static void main(String[] args) throws CloneNotSupportedException {
        Student student1 = new Student(18, "張三", new Bag("紅",100));
        Student student2 = (Student) student1.clone();

        System.out.println("深複製後:");
        System.out.println("student1:" + student1);
        System.out.println("student2:" + student2);

        //修改非引用型別屬性name
        student2.setName("李四");

        //修改引用型別屬性bag
        Bag bag = student2.getBag();
        bag.setColor("藍");
        bag.setPrice(200);

        System.out.println("修改了 student2 的 name 和 bag 後:");
        System.out.println("student1:" + student1);
        System.out.println("student2:" + student2);
    }
}

//這個測試類和之前的淺複製的測試類一樣,但執行結果是不同的。
深複製後:
student1:age=18, name='張三, bag=color='紅, price=100
student2:age=18, name='張三, bag=color='紅, price=100
修改了 student2 的 name 和 bag 後:
student1:age=18, name='張三, bag=color='紅, price=100
student2:age=18, name='李四, bag=color='藍, price=200

不只是 student1 和 student2 是不同的物件,它們中的 bag 也是不同的物件。所以,改變了 student2 中的 bag 並不會影響到 student1。

不過,透過 clone() 方法實現的深複製比較笨重,因為要將所有的引用型別都重寫 clone() 方法。

更好的方法是利用序列化

序列化

序列化是將物件寫入流中,而反序列化是將物件從流中讀取出來。寫入流中的物件就是對原始物件的複製。需要注意的是,每個要序列化的類都要實現 Serializable 介面,該介面和 Cloneable 介面類似,都是標記型介面。

來看例子

@Data
class Bag implements Serializable {
    private String color;
    private int price;

    public Bag(String color, int price) {
        this.color = color;
        this.price = price;
    }

    @Override
    public String toString() {
        return "color='" + color + ", price=" + price;
    }
}

Bag 需要實現 Serializable 介面

@Data
class Student implements Serializable {
    //年齡和名字是基本屬性
    private int age;
    private String name;
    //書包是引用屬性
    private Bag bag;

    public Student(int age, String name, Bag bag) {
        this.age = age;
        this.name = name;
        this.bag = bag;
    }

    @Override
    public String toString() {
        return "age=" + age + ", name='" + name + ", bag=" + bag;
    }

    //使用序列化複製
    public Object serializeClone() throws IOException, ClassNotFoundException {
        // 序列化
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);

        oos.writeObject(this);

        // 反序列化
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);

        return ois.readObject();
    }

}

Student 類也需要實現 Serializable 介面,並且在該類中,增加了一個 serializeClone() 的方法,利用 OutputStream 進行序列化,InputStream 進行反序列化,這樣就實現了深複製。

來看示例

class TestClone {
    public static void main(String[] args) throws CloneNotSupportedException, IOException, ClassNotFoundException {
        Student student1 = new Student(18, "張三", new Bag("紅",100));
        Student student2 = (Student) student1.serializeClone();

        System.out.println("淺複製後:");
        System.out.println("student1:" + student1);
        System.out.println("student2:" + student2);

        //修改非引用型別屬性name
        student2.setName("李四");

        //修改引用型別屬性bag
        Bag bag = student2.getBag();
        bag.setColor("藍");
        bag.setPrice(200);

        System.out.println("修改了 student2 的 name 和 bag 後:");
        System.out.println("student1:" + student1);
        System.out.println("student2:" + student2);

    }
}


//與之前測試類不同的是,呼叫了 serializeClone() 方法。
淺複製後:
student1:age=18, name='張三, bag=color='紅, price=100
student2:age=18, name='張三, bag=color='紅, price=100
修改了 student2 的 name 和 bag 後:
student1:age=18, name='張三, bag=color='紅, price=100
student2:age=18, name='李四, bag=color='藍, price=200

測試結果和之前用 clone() 方法實現的深複製一樣。

clone() 方法同時是一個本地(native)方法,它的具體實現會交給 HotSpot 虛擬機器,那就意味著虛擬機器在執行該方法的時候,會將其替換為更高效的 C/C++ 程式碼,進而呼叫作業系統去完成物件的克隆工作。

需要注意,由於是序列化涉及到輸入流和輸出流的讀寫,在效能上要比 HotSpot 虛擬機器實現的 clone() 方法差很多。

相關文章