深複製、淺複製,一文了解原型模式
一、前言
看了上一期的單例模式之後,是不是可以應對一些簡單的面試題目了,我相信你可以的。第一稿的原型模式我感覺是比較好完成了,書籍包括視頻上面,都花了很長的篇幅,寫上一篇感覺不算費力。這一篇還真是不好寫,我先從我覺得較簡單的說起吧,這一篇說原型模式,然後說說淺複製和深複製的區別,值類型和引用類型…開講啦
二、原型模式的運用場景和解釋
看了《大話設計模式》和一些博客,他們都喜歡用簡歷這個例子來說明原型模式。在投簡歷的時候我們是寫好一份簡歷,然後複製瘋狂投簡歷。當然我們也對簡歷進行一些簡單崗位薪資方面的修改,這個時候不需要修改基本信息,這時就用到了設計模式了。每次New一個對象,都需要執行一次構造函數,如果構造函數的執行時間很長,那麼多次執行這個初始化的操作不就很浪費時間呀。所以在大部分信息不變的情況下,克隆是最好的辦法。這即隱藏了對象創建的細節,又對性能是大大的提高了。
原型模式不用重新的初始化對象,而是動態地獲得對象運行時的狀態。
三、先看看這幾個簡單的程序
每次新建一份簡歷我都new一下
//簡歷類
package prototype;
/**
* 原型模式:用原型實例指定創建對象的總類,並且通過拷貝這些原型創建新的對象
* */
public class Resume {
private String name;
private String sex;
private String age;
private String timeArea;
private String company;
public Resume(String name){
this.name = name;
}
//設置個人信息
public void SetPersonalInfo(String sex,String age){
this.sex = sex;
this.age = age;
}
//設置工作經歷
public void SetWorkExperience(String timeArea,String company){
this.timeArea = timeArea;
this.company = company;
}
}
//主函數
package prototype;
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Resume a = new Resume("大鳥");
a.SetPersonalInfo("男","30");
a.SetWorkExperience("2019-2020","xx公司");
Resume b = new Resume("大鳥");
b.SetPersonalInfo("男","31");
b.SetWorkExperience("2019-2020","xx公司");
Resume c = new Resume("大鳥");
c.SetPersonalInfo("男","32");
c.SetWorkExperience("2019-2020","xx公司");
System.out.println(a);
System.out.println(b);
System.out.println(c);
}
}
//OutPut
prototype.Resume@74a14482
prototype.Resume@1540e19d
prototype.Resume@677327b6
我們新建了3份簡歷,簡歷有一些不同,選擇new了3次,等到了3份簡歷。
直接引用不復制了(錯誤)
//簡歷類
package prototype;
/**
* 原型模式:用原型實例指定創建對象的總類,並且通過拷貝這些原型創建新的對象
* */
public class Resume {
private String name;
private String sex;
private String age;
private String timeArea;
private String company;
public Resume(String name){
this.name = name;
}
//設置個人信息
public void SetPersonalInfo(String sex,String age){
this.sex = sex;
this.age = age;
}
//設置工作經歷
public void SetWorkExperience(String timeArea,String company){
this.timeArea = timeArea;
this.company = company;
}
}
//主函數
package prototype;
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Resume a = new Resume("大鳥");
a.SetPersonalInfo("男","30");
a.SetWorkExperience("2019-2020","xx公司");
Resume b = a;
Resume c = a;
System.out.println(a);
System.out.println(b);
System.out.println(c);
}
}
//OutPut
prototype.Resume@74a14482
prototype.Resume@74a14482
prototype.Resume@74a14482
這是a、b、c都是同一個對象,這其實違背了複製的想法,是錯誤的。簡單理解這是b和c的內容是:簡歷在a處。
不想複製的時候new對象了
//簡歷類
package prototype;
/**
* 原型模式:用原型實例指定創建對象的總類,並且通過拷貝這些原型創建新的對象
* */
public class Resume2 implements Cloneable{
private String name;
private String sex;
private String age;
private String timeArea;
private String company;
public Resume2(String name){
this.name = name;
}
//設置個人信息
public void SetPersonalInfo(String sex,String age){
this.sex = sex;
this.age = age;
}
//設置工作經歷
public void SetWorkExperience(String timeArea,String company){
this.timeArea = timeArea;
this.company = company;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
//主函數
package prototype;
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Resume2 a = new Resume2("大鳥");
a.SetPersonalInfo("男","30");
a.SetWorkExperience("2019-2020","xx公司");
Resume2 b = (Resume2) a.clone();
Resume2 c = (Resume2) a.clone();
System.out.println(a);
System.out.println(b);
System.out.println(c);
}
}
//OutPut
prototype.Resume2@74a14482
prototype.Resume2@1540e19d
prototype.Resume2@677327b6
可以看到這種clone的方式和new相比起來還是很簡便的,只需要一句就行。如果說你需要初始化100個對象,會花上很多的時間,那麼這一句是很好的選擇。
對副本進行簡單的修改
//簡歷類
package prototype;
/**
* 原型模式:用原型實例指定創建對象的總類,並且通過拷貝這些原型創建新的對象
* */
public class Resume2 implements Cloneable{
private String name;
private String sex;
private String age;
private String timeArea;
private String company;
public Resume2(String name){
this.name = name;
}
//設置個人信息
public void SetPersonalInfo(String sex,String age){
this.sex = sex;
this.age = age;
}
//設置工作經歷
public void SetWorkExperience(String timeArea,String company){
this.timeArea = timeArea;
this.company = company;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "Resume2{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", age='" + age + '\'' +
", timeArea='" + timeArea + '\'' +
", company='" + company + '\'' +
'}';
}
}
//主函數
package prototype;
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Resume2 a = new Resume2("大鳥");
a.SetPersonalInfo("男","30");
a.SetWorkExperience("2019-2020","xx公司");
Resume2 b = (Resume2) a.clone();
b.SetWorkExperience("2018-2019","yy公司");
Resume2 c = (Resume2) a.clone();
c.SetWorkExperience("2018-2019","zz公司");
System.out.println("a= " + a.toString());
System.out.println("b= " + b.toString());
System.out.println("c= " + c.toString());
}
}
//OutPut
a= Resume2{name='大鳥', sex='男', age='30', timeArea='2019-2020', company='xx公司'}
b= Resume2{name='大鳥', sex='男', age='30', timeArea='2018-2019', company='yy公司'}
c= Resume2{name='大鳥', sex='男', age='30', timeArea='2018-2019', company='zz公司'}
克隆之後,我們可以對簡歷進行下一步的修改,複製也想做自己。
引用類型就不能做自己麼
//簡歷類
package prototype;
/**
* 原型模式:用原型實例指定創建對象的總類,並且通過拷貝這些原型創建新的對象
* */
public class Resume3 implements Cloneable{
private String name;
private String sex;
private String age;
private WorkExperience workExperience;
public Resume3(String name){
this.name = name;
this.workExperience = new WorkExperience();
}
//設置個人信息
public void SetPersonalInfo(String sex,String age){
this.sex = sex;
this.age = age;
}
//設置工作經歷
public void SetWorkExperience(String timeArea,String company){
this.workExperience.setCompany(company);
this.workExperience.setTimeArea(timeArea);
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "Resume3{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", age='" + age + '\'' +
", workExperience=" + workExperience.getCompany() +
", " + workExperience.getTimeArea() +
'}';
}
}
//工作經歷類
package prototype;
public class WorkExperience {
private String timeArea;
private String company;
public String getTimeArea() {
return timeArea;
}
public void setTimeArea(String timeArea) {
this.timeArea = timeArea;
}
public String getCompany() {
return company;
}
public void setCompany(String company) {
this.company = company;
}
}
//主函數
package prototype;
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Resume3 a = new Resume3("大鳥");
a.SetWorkExperience("2019-2020","xx公司");
a.SetPersonalInfo("男","30");
Resume3 b = (Resume3) a.clone();
b.SetWorkExperience("2019-2020","yy公司");
b.SetPersonalInfo("男","31");
Resume3 c = (Resume3) a.clone();
c.SetWorkExperience("2019-2020","zz公司");
c.SetPersonalInfo("男","32");
System.out.println("a= " + a.toString());
System.out.println("b= " + b.toString());
System.out.println("c= " + c.toString());
}
}
//OutPut
a= Resume3{name='大鳥', sex='男', age='30', workExperience=zz公司, 2019-2020}
b= Resume3{name='大鳥', sex='男', age='31', workExperience=zz公司, 2019-2020}
c= Resume3{name='大鳥', sex='男', age='32', workExperience=zz公司, 2019-2020}
我們本來是想對3個不同的對象分別個性化他們的workExperience,但是好像程序並不盡人意。這就需要引出一個知識點:淺複製和深複製。
淺複製:被複制的所有變量都含有原來的對象相同的值,而所有的對其他對象的引用都仍然指向原來的對象。
深複製:把引用對象的變量指向複製的新對象,而不是原有的被引用的對象。
還有另一個知識點那就是:值類型和引用類型。當我們clone的時候,創建當前對象的淺表副本,方法是創建一個新的對象,然後將當前對象的非靜態字段複製到該新對象。
如果字段是值類型,則對該字段執行逐位複製。如果字段是引用類型,則複製引用但不會複製引用的對象
引用類型的變化
//簡歷類
package prototype;
/**
* 原型模式:用原型實例指定創建對象的總類,並且通過拷貝這些原型創建新的對象
* */
public class Resume4 implements Cloneable{
private String name;
private String sex;
private String age;
private WorkExperienceImprove workExperience;
public Resume4(String name){
this.name = name;
this.workExperience = new WorkExperienceImprove();
}
public Resume4(WorkExperienceImprove workExperience) throws CloneNotSupportedException {
this.workExperience = (WorkExperienceImprove) workExperience.clone();
}
//設置個人信息
public void SetPersonalInfo(String sex,String age){
this.sex = sex;
this.age = age;
}
//設置工作經歷
public void SetWorkExperience(String timeArea,String company){
this.workExperience.setCompany(company);
this.workExperience.setTimeArea(timeArea);
}
@Override
protected Object clone() throws CloneNotSupportedException {
Resume4 resume4 = new Resume4(this.workExperience);
resume4.name = this.name;
resume4.age = this.age;
resume4.sex = this.sex;
return resume4;
}
@Override
public String toString() {
return "Resume3{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", age='" + age + '\'' +
", workExperience=" + workExperience.getCompany() +
", " + workExperience.getTimeArea() +
'}';
}
}
//工作經歷類
package prototype;
public class WorkExperienceImprove implements Cloneable {
private String timeArea;
private String company;
public String getTimeArea() {
return timeArea;
}
public void setTimeArea(String timeArea) {
this.timeArea = timeArea;
}
public String getCompany() {
return company;
}
public void setCompany(String company) {
this.company = company;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
//主函數
package prototype;
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Resume4 a = new Resume4("大鳥");
a.SetWorkExperience("2019-2020","xx公司");
a.SetPersonalInfo("男","30");
Resume4 b = (Resume4) a.clone();
b.SetWorkExperience("2019-2020","yy公司");
b.SetPersonalInfo("男","31");
Resume4 c = (Resume4) a.clone();
c.SetWorkExperience("2019-2020","zz公司");
c.SetPersonalInfo("男","32");
System.out.println("a= " + a.toString());
System.out.println("b= " + b.toString());
System.out.println("c= " + c.toString());
}
}
//OutPut
a= Resume3{name='大鳥', sex='男', age='30', workExperience=xx公司, 2019-2020}
b= Resume3{name='大鳥', sex='男', age='31', workExperience=yy公司, 2019-2020}
c= Resume3{name='大鳥', sex='男', age='32', workExperience=zz公司, 2019-2020}
這個例子,第一次看,還有點難懂,主要就是我們需要把workExperience也實現Cloneable的clone接口
public Resume4(WorkExperienceImprove workExperience) throws CloneNotSupportedException {
this.workExperience = (WorkExperienceImprove) workExperience.clone();
}
@Override
protected Object clone() throws CloneNotSupportedException {
Resume4 resume4 = new Resume4(this.workExperience);
resume4.name = this.name;
resume4.age = this.age;
resume4.sex = this.sex;
return resume4;
}
然後通過重寫clone方法,傳入workExperience的對象,通過構造方法克隆workExperience。
四、淺複製和深複製的區別
在本文的原型模式的最後的兩個栗子當中,大家應該可以體會到淺複製和深複製的區別。
淺複製:被複制的所有變量都含有原來的對象相同的值,而所有的對其他對象的引用都仍然指向原來的對象。
深複製:它會把引用對象的變量指向複製的新對象,而不是原有的被引用的對象。
下面引用一下別人的博客,仔細推敲文字還是非常不錯的,如果有用,記得給別人點贊。
- 淺拷貝可以使用列表自帶的copy()函數(如list.copy()),或者使用copy模塊的copy()函數。深拷貝只能使用copy模塊的deepcopy(),所以使用前要導入:from copy import deepcopy。
- 如果拷貝的對象裏的元素只有值,沒有引用,那淺拷貝和深拷貝沒有差別,都會將原有對象複製一份,產生一個新對象,對新對象裏的值進行修改不會影響原有對象,新對象和原對象完全分離開。
- 如果拷貝的對象裏的元素包含引用(像一個列表裏儲存着另一個列表,存的就是另一個列表的引用),那淺拷貝和深拷貝是不同的,淺拷貝雖然將原有對象複製一份,但是依然保存的是引用,所以對新對象裏的引用裏的值進行修改,依然會改變原對象裏的列表的值,新對象和原對象完全分離開並沒有完全分離開。而深拷貝則不同,它會將原對象裏的引用也新創建一個,即新建一個列表,然後放的是新列表的引用,這樣就可以將新對象和原對象完全分離開。
原博客:https://blog.csdn.net/qq_34493908/article/details/81560546
五、值類型和引用類型的區別
在本文的栗子中,當我們clone的時候,創建當前對象的淺表副本,方法是創建一個新的對象,然後將當前對象的非靜態字段複製到該新對象。如果字段是值類型,則對該字段執行逐位複製。如果字段是引用類型,則複製引用但不會複製引用的對象
網上找了一些資料,關於值類型和引用類型,他們舉例子都是使用的C#,我也用C#吧。
C#把數據類型分爲兩大類:值類型(數據存放在棧中)和引用類型(數據存放在堆中,但是地址存放在棧中)。值類型包含:簡單類型、枚舉和結構體。引用類型包含:數組,字符串,接口,類,委託。完了!越解釋越亂了,要不我們直接看上面寫的第二個例子吧,Resume b = a;Resume c = a;這就是引用類型,把b和c指向a的地址,其他這時a,b,c都是一樣的。
六、參考資料
書籍:《大話設計模式》 第9章 簡歷複印–原型模式
博客:https://blog.csdn.net/qq_34493908/article/details/81560546
七、關於本系列的解釋
本系列想製作23種設計模式+7種設計原則一系列課程,其目的就是一個簡單的記錄學習的過程。不知道能幫助到多少人,也不知道技術是否會有一定的深度。
製作不易,您的點贊是我最大的動力。
希望我們都能成爲想成爲的人