🌲本文收錄于專欄《原始碼中的設計模式》——理論與實戰的完美結合
作者其它優質專欄推薦:
📚《技術專家修煉》——搞技術,進大廠,聊人生三合一專欄
📚《leetcode 300題》——每天一道演算法題,進大廠必備
📚《糊涂演算法》——從今天起,邁過資料結構和演算法這道坎
📚《從實戰學python》——Python的爬蟲,自動化,AI等實戰應用
點擊跳轉到文末領取粉絲福利
哈嘍,大家好,我是一條~
之前的《白話設計模式》因為作業被擱置,如今再次啟航,并搭配框架原始碼決議一起食用,將理論與實戰完美結合,
對設計模式不是很熟悉的同學可以先看一下《23種設計模式的一句話通俗解讀》全面的了解一下設計模式,形成一個整體的框架,再逐個擊破,
今天我們一塊看一下原型模式,屬于簡單且常用的一種,
定義
官方定義
用原型實體指定創建物件的種類,并且通過拷貝這個原型來創建新的物件,
通俗解讀
在需要創建重復的物件,為了保證性能,本體給外部提供一個克隆體進行使用,
類似我國的印刷術,省去new的程序,通過copy的方式創建物件,
結構圖

代碼實作
目錄結構
建議跟著一條學設計模式的小伙伴都建一個
maven工程,并安裝lombok依賴和插件,并建立如下包目錄,便于歸納整理,
pom如下
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
開發場景
假設一條開發了一個替代Mybatis的框架,叫YitiaoBatis,每次操作資料庫,從資料庫里面查出很多記錄,但是改變的部分是很少的,如果每次查資料庫,查到以后把所有資料都封裝一個物件,就會導致要new很多重復的物件,造成資源的浪費,
一條想到一個解決辦法,就是把查過的資料保存起來,下來查相同的資料,直接把保存好的物件回傳,也就是快取的思想,
我們用代碼模擬一下:
1.創建Yitiao物體類
/**
* author:一條
*/
@Data
@AllArgsConstructor
public class Yitiao {
private String name;
private Integer id;
private String wechat;
public Yitiao(){
System.out.println("Yitiao物件創建");
}
}
2.創建YitiaoBatis類
/**
* author:一條
*/
public class YitiaoBatis {
//快取Map
private Map<String,Yitiao> yitiaoCache = new HashMap<>();
//從快取拿物件
public Yitiao getYitiao(String name){
//判斷快取中是否存在
if (yitiaoCache.containsKey(name)){
Yitiao yitiao = yitiaoCache.get(name);
System.out.println("從快取查到資料:"+yitiao);
return yitiao;
}else {
//模擬從資料庫查資料
Yitiao yitiao = new Yitiao();
yitiao.setName(name);
yitiao.setId(1);
yitiao.setWechat("公眾號:一條coding");
System.out.println("從資料庫查到資料:"+yitiao);
//放入快取
yitiaoCache.put(name,yitiao);
return yitiao;
}
}
}
3.撰寫測驗類
/**
* author:一條
*/
public class MainTest {
public static void main(String[] args) {
YitiaoBatis yitiaoBatis = new YitiaoBatis();
Yitiao yitiao1 = yitiaoBatis.getYitiao("yitiao");
System.out.println("第一次查詢:"+yitiao1);
Yitiao yitiao2 = yitiaoBatis.getYitiao("yitiao");
System.out.println("第二次查詢:"+yitiao2);
}
}
輸出結果

從結果可以看出:
- 物件創建了一次,有點單例的感覺
- 第一次從資料庫查,第二次從快取查
好像是實作了YitiaoBatis框架的需求,思考🤔一下有什么問題呢?
4.修改物件id
在測驗類繼續撰寫
//執行后續業務,修改id
yitiao2.setId(100);
Yitiao yitiao3 = yitiaoBatis.getYitiao("yitiao");
System.out.println("第三次查詢:"+yitiao3);
輸出結果

重點看第三次查詢,id=100?
我們在記憶體修改的資料,導致從資料庫查出來的資料也跟著改變,出現臟資料,
怎么解決呢?原型模式正式開始,
5.實作Cloneable介面
本體給外部提供一個克隆體進行使用,在快取中拿到的物件不直接回傳,而是復制一份,這樣就保證了不會臟快取,
public class Yitiao implements Cloneable{
//……
@Override
protected Object clone() throws CloneNotSupportedException {
return (Yitiao) super.clone();
}
}
修改快取
//從快取拿物件
public Yitiao getYitiao(String name) throws CloneNotSupportedException {
//判斷快取中是否存在
if (yitiaoCache.containsKey(name)){
Yitiao yitiao = yitiaoCache.get(name);
System.out.println("從快取查到資料:"+yitiao);
//修改回傳
//return yitiao;
return yitiao.clone();
}else {
//模擬從資料庫查資料
Yitiao yitiao = new Yitiao();
yitiao.setName(name);
yitiao.setId(1);
yitiao.setWechat("公眾號:一條coding");
System.out.println("從資料庫查到資料:"+yitiao);
//放入快取
yitiaoCache.put(name,yitiao);
//修改回傳
//return yitiao;
return yitiao.clone();
}
6.再次測驗
不用改測驗類,直接看一下結果:

從輸出結果可以看出第三次查詢id依然是1,沒有臟快取現象,
基于原型模式的克隆思想,我可以快速拿到和「本體」一模一樣的「克隆體」,而且物件也只被new了一次,
不知道大家是否好奇物件是怎么被創建出來的,那我們就一起看一下「深拷貝」和「淺拷貝」是怎么回事,
深拷貝和淺拷貝
定義
深拷貝:不管拷貝物件里面是基本資料型別還是參考資料型別都是完全的復制一份到新的物件中,
淺拷貝:當拷貝物件只包含簡單的資料型別比如int、float 或者不可變的物件(字串)時,就直接將這些欄位復制到新的物件中,而參考的物件并沒有復制而是將參考物件的地址復制一份給克隆物件,
好比兩個兄弟,深拷貝是年輕的時候關系特別好,衣服買一樣的,房子住一塊,淺拷貝是長大了都成家立業,衣服可以繼續買一樣的,但房子必須要分開住了,
實作
在代碼上區分深拷貝和淺拷貝的方式就是看參考型別的變數在修改后,值是否發生變化,
淺拷貝
1.通過clone()方式的淺拷貝
新建Age類,作為Yitiao的參考屬性
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Age {
private int age;
}
2.測驗1
public static void main(String[] args) throws CloneNotSupportedException {
Yitiao yitiao1 = new Yitiao();
Age age = new Age(1);
yitiao1.setAge(age);
yitiao1.setId(1);
Yitiao clone = yitiao1.clone();
yitiao1.setId(2);
age.setAge(2); //不能new一個age
System.out.println("yitiao1:\n"+yitiao1+"\nclone:\n"+clone);
}
輸出結果

結論:基本型別id沒發生改變,參考型別Age由于地址指向的同一個物件,值跟隨變化,
3.通過構造方法實作淺拷貝
Yitiao.class增加構造方法
public Yitiao(Yitiao yitiao){
id=yitiao.id;
age=yitiao.age;
}
4.測驗2
Yitiao yitiao1 = new Yitiao();
Age age = new Age(1);
yitiao1.setAge(age);
yitiao1.setId(1);
Yitiao clone = new Yitiao(yitiao1); //差別在這
yitiao1.setId(2);
age.setAge(2);
System.out.println("yitiao1:\n"+yitiao1+"\nclone:\n"+clone);
輸出結果

與測驗1無異
深拷貝
1.通過物件序列化實作深拷貝
通過層次呼叫clone方法也可以實作深拷貝,但是代碼量太大,特別對于屬性數量比較多、層次比較深的類而言,每個類都要重寫clone方法太過繁瑣,一般不使用,亦不再舉例,
可以通過將物件序列化為位元組序列后,默認會將該物件的整個物件圖進行序列化,再通過反序列即可完美地實作深拷貝,
Yitiao和Age實作Serializable介面
2.測驗
//通過物件序列化實作深拷貝
Yitiao yitiao = new Yitiao();
Age age = new Age(1);
yitiao.setAge(age);
yitiao.setId(1);
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(yitiao);
oos.flush();
ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
Yitiao clone = (Yitiao) ois.readObject();
yitiao.setId(2);
age.setAge(2);
System.out.println("yitiao:\n"+yitiao+"\nclone:\n"+clone);
輸出結果

結論,參考物件也完全復制一個新的,值不變化,
不過要注意的是,如果某個屬性被transient修飾,那么該屬性就無法被拷貝了,
應用場景
我們說回原型模式,
原型模式在我們的代碼中是很常見的,但是又容易被我們所忽視的一種模式,比如我們常用的的
BeanUtils.copyProperties就是一種物件的淺拷貝,看看有哪些場景需要原型模式
- 資源優化
- 性能和安全要求
- 一個物件多個修改者的場景,
- 一個物件需要提供給其他物件訪問,而且各個呼叫者可能都需要修改其值時可以考慮使用原型模式拷貝多個物件供呼叫者使用,
原型模式已經與 Java 融為渾然一體,可以隨手拿來使用,
總結
原型模式應該算是除了單例最簡單的設計模式,但我還是寫了將近4個小時,畫圖,敲代碼,碼字,不知不覺寫了8000字,
一篇優質的原創文真的很耗費作者的心血,所以如果感覺寫的還不錯,麻煩給個三連,這對一條來說很重要,也是一條創作下去的動力!
最后
古語云:乘眾人之智,則無不任也;用眾人之力,則無不勝也,一個人或許可以走的很快,但一群人才能走的更遠,
為此,我制定了抱團生長計劃,每天分享1-3篇優質文章和1道leetcode演算法題

如果你剛剛大一,每天堅持學習,你將會至少比別人多看4000篇文章,多刷1200道題,那么畢業時你的工資就可能是別人的3-4倍,
如果你是職場人,每天提升自己,升職加薪,成為技術專家指日可待,
只要你愿意去奮斗,始終走在拼搏的路上,那你的人生,最壞的結果,也不過是大器晚成,
點此加入計劃
如果鏈接被屏蔽,或者有權限問題,可以私聊作者解決,
粉絲專屬福利
📚Java:1.5G學習資料——回復「資料」
📚演算法:視頻書籍——回復「演算法」
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/302283.html
標籤:其他
