前言:
本來是在學習原型模式的,後來發現它就是一個拷貝,然後就去研究了下深淺拷貝以及它們的實現,這裏不說大道理,儘量通俗易懂地把它們都講清楚。
1.引入
問:java 對象拷貝的意義何在?爲啥要拷貝?
答:因爲懶,不想實例化一個,所以拷貝生成一個新的對象
現實實例:
本科學習的時候有很多課程是講 ppt 進行考覈的,有些人忙(其實嘛…)沒做 ppt,所以就會拷貝一份室友(室長)的;A室友拷貝了一份 ppt 就放在室長電腦裏面同一個目錄下,改了些個人信息,並把 ppt 鏈接的素材裁剪了一番;B室友把 ppt 和素材都拷貝到自己電腦上了,然後對鏈接的素材修剪了一番。
- 上面例子的拷貝就是此次博客說的拷貝
- 淺拷貝:A室友修改素材的操作會影響到室長嗎?當然會,室長講 ppt 打開素材的時候內心一定會想,豬…隊友;B室友的動作就是淺拷貝,修改了 ppt 本身的屬性是可以的,但修改其鏈接素材會同步改變室長 ppt 的鏈接素材。
- 深拷貝: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,這種方式不好,最好是拋出異常讓調用進行處理,例子只是爲了書寫方便才這麼用。