Java 深淺拷貝和原型模式

前言:

本來是在學習原型模式的,後來發現它就是一個拷貝,然後就去研究了下深淺拷貝以及它們的實現,這裏不說大道理,儘量通俗易懂地把它們都講清楚。

1.引入

問:java 對象拷貝的意義何在?爲啥要拷貝?
答:因爲懶,不想實例化一個,所以拷貝生成一個新的對象

現實實例:
本科學習的時候有很多課程是講 ppt 進行考覈的,有些人忙(其實嘛…)沒做 ppt,所以就會拷貝一份室友(室長)的;A室友拷貝了一份 ppt 就放在室長電腦裏面同一個目錄下,改了些個人信息,並把 ppt 鏈接的素材裁剪了一番;B室友把 ppt 和素材都拷貝到自己電腦上了,然後對鏈接的素材修剪了一番。

  1. 上面例子的拷貝就是此次博客說的拷貝
  2. 淺拷貝:A室友修改素材的操作會影響到室長嗎?當然會,室長講 ppt 打開素材的時候內心一定會想,豬…隊友;B室友的動作就是淺拷貝,修改了 ppt 本身的屬性是可以的,但修改其鏈接素材會同步改變室長 ppt 的鏈接素材。
  3. 深拷貝:B室友的拷貝 ppt 就是深拷貝,和室長的 ppt 可以獨立修改,互不影響。
2.淺拷貝的實現

本質上使用的是 java 自帶的 clone() 方法
這裏舉一個例子,Book類(課本),Subject類(課程)包含Book

Book類

public class Book {

    private String bookName;
    private int page;

    public Book(String bookName, int page) {
        this.bookName = bookName;
        this.page = page;
    }

    public String getBookName() {
        return bookName;
    }

    public int getPage() {
        return page;
    }

    public void setBookName(String bookName) {
        this.bookName = bookName;
    }

    public void setPage(int page) {
        this.page = page;
    }

}

Subject類,需要實現 Cloneable 接口的 clone()方法才能被拷貝

public class Subject implements Cloneable {
    private Book book;
    private String subjectName;

    public Book getBook() {
        return book;
    }

    public void setBook(Book book) {
        this.book = book;
    }

    public String getSubjectName() {
        return subjectName;
    }

    public void setSubjectName(String subjectName) {
        this.subjectName = subjectName;
    }

    @Override
    public Object clone() {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }

}

測試

public class Main {
    public static void main(String[] args) {
        Subject music = new Subject();
        music.setBook(new Book("五線譜", 50));
        music.setSubjectName("音樂課");

        Subject sports = (Subject) music.clone();
        sports.setSubjectName("體育課");
        sports.getBook().setBookName("體操講義");

        //驗證淺拷貝
        System.out.print("課程名:" + sports.getSubjectName() + ",");
        System.out.println("課本名:" + sports.getBook().getBookName());
        System.out.print("課程名:" + music.getSubjectName() + ",");
        System.out.println("課本名:" + music.getBook().getBookName());
    }

}

打印結果:

課程名:體育課,課本名:體操講義
課程名:音樂課,課本名:體操講義

解析:
這邊最初實例化一個“音樂課”的課程,並給了一本“五線譜”的書,在此基礎上,拷貝產生了一個新對象,並修改課程名稱爲“體育類課”,修改課本爲“體操講義”;把他們打印出來就會發現,課程名(subjectName)改變後相互不影響,課本(book)做了同步修改,淺拷貝就是這種情況,當然這種在生產上也有很大用處。

3.所有類都實現自身拷貝的深拷貝方式

2 中的例子是 Subject 實現拷貝本身,如果拷貝的時候把 Book 也拷貝過去是不是可以實現深拷貝呢?當然 Book 也需要實現自身的拷貝函數

public class Book implements Cloneable {

    private String bookName;
    private int page;

    public Book(String bookName, int page) {
        this.bookName = bookName;
        this.page = page;
    }

    public String getBookName() {
        return bookName;
    }

    public int getPage() {
        return page;
    }

    public void setBookName(String bookName) {
        this.bookName = bookName;
    }

    public void setPage(int page) {
        this.page = page;
    }

    @Override
    public Object clone() {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }

}

public class Subject implements Cloneable {
    private Book book;
    private String subjectName;

    public Book getBook() {
        return book;
    }

    public void setBook(Book book) {
        this.book = book;
    }

    public String getSubjectName() {
        return subjectName;
    }

    public void setSubjectName(String subjectName) {
        this.subjectName = subjectName;
    }

    @Override
    public Object clone() {
        try {
            Subject subject = (Subject) super.clone();
            subject.setBook((Book) book.clone());
            return subject;
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }

}

測試:

public class Main {
    public static void main(String[] args) {
        Subject music = new Subject();
        music.setBook(new Book("五線譜", 50));
        music.setSubjectName("音樂課");

        Subject sports = (Subject) music.clone();
        sports.setSubjectName("體育課");
        sports.getBook().setBookName("體操講義");

        //驗證淺拷貝
        System.out.print("課程名:" + sports.getSubjectName() + ",");
        System.out.println("課本名:" + sports.getBook().getBookName());
        System.out.print("課程名:" + music.getSubjectName() + ",");
        System.out.println("課本名:" + music.getBook().getBookName());

    }

}

結果:

課程名:體育課,課本名:體操講義
課程名:音樂課,課本名:五線譜

保證了互不影響

4.序列化實現深拷貝

上述3的方式把每個類都實現自身拷貝的方法有點麻煩,有簡單的嗎?有,序列化實現拷貝,只需要類都實現接口聲明就行了,不需要寫實現方法
序列化類和其方法與被拷貝的對象沒有任何關係,如下
tips:不建議把序列化方法寫成類成員函數,通用性太差

import java.io.*;

public class SerializedClone {

    @SuppressWarnings("unchecked")
    public static <X extends Serializable> X clone(X obj) {
        X cloneObj = null;
        try {
            //寫入字節流
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ObjectOutputStream obs = new ObjectOutputStream(out);
            obs.writeObject(obj);
            obs.close();

            //分配內存,寫入原始對象,生成新對象
            ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(ios);
            //返回生成的新對象
            cloneObj = (X) ois.readObject();
            ois.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return cloneObj;
    }
}

還是那兩個類 Book 和 Subject,只需要聲明接口 Serializable 就行,不需要方法實現(Cloneable 需要實現 clone() 方法)

public class Book implements Serializable {
    private String bookName;
    private int page;

    public Book(String bookName, int page) {
        this.bookName = bookName;
        this.page = page;
    }

    public String getBookName() {
        return bookName;
    }

    public int getPage() {
        return page;
    }

    public void setBookName(String bookName) {
        this.bookName = bookName;
    }

    public void setPage(int page) {
        this.page = page;
    }
}



public class Subject implements Serializable {

    private Book book;
    private String subjectName;

    public Book getBook() {
        return book;
    }

    public void setBook(Book book) {
        this.book = book;
    }

    public String getSubjectName() {
        return subjectName;
    }

    public void setSubjectName(String subjectName) {
        this.subjectName = subjectName;
    }

}

測試:

public class Main {
    public static void main(String[] args) {

        Subject chemistry = new Subject();
        chemistry.setBook(new Book("火藥製造", 50));
        chemistry.setSubjectName("化學課");

        Subject physics = SerializedClone.clone(chemistry);
        physics.setSubjectName("物理課");
        physics.getBook().setBookName("穿牆講義");

        //驗證深拷貝
        System.out.print("課程名:" + chemistry.getSubjectName() + ",");
        System.out.println("課本名:" + chemistry.getBook().getBookName());
        System.out.print("課程名:" + physics.getSubjectName() + ",");
        System.out.println("課本名:" + physics.getBook().getBookName());
    }
}

結果:

課程名:化學課,課本名:火藥製造
課程名:物理課,課本名:穿牆講義

可以發現,序列化實現深拷貝更簡單

5.其它方式

還有一種反射的方式實現拷貝,實現起來稍微麻煩,有興趣可以研究

**另:**上述例子在處理 exception 的時候返回 null,這種方式不好,最好是拋出異常讓調用進行處理,例子只是爲了書寫方便才這麼用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章