五分鐘 掌握 原型模式
大家好,我是老田,今天我給大家分享設計模式中的原型模式。用貼切的生活故事,以及真實項目場景來講設計模式,最后用一句話來總結這個設計模式。
故事
還記得大四那年找工作,無意中我得從網上找到一份相對漂亮的程序員簡歷模板,然后全班同學開啟瘋狂的簡歷拷貝(U盤)。同時也鬧出了一個笑話,有幾位同學,拷貝過去的簡歷,內容完全沒改,名字都沒有改,截止投給面試官(校招面試官)。后來,結果大家也應該能猜出來,大家都去實習了,部分人還在找工作。后面公司面試官和同伴的其他同學反饋:收到一毛一樣的簡歷,好幾份,回來大家一聊就知道問題出哪里了,承認了自己拷貝過去完全沒改就拿出去投了,害,尷尬的一匹。
把簡歷拷貝分為為兩種:
- 一種是拷貝簡歷,然后把信息修改成自己的
- 另外一種是,拷貝簡歷,內容什么都不改。
原型模式定義
Specify the kinds of objects to create using a prototype instance ,and create new objects by coping this prototype
大致意思:用原型實例指定創建對象的種類,并且通過復制這些原型創建新的對象。
原型模式:Prototype Pattern,屬于創建型模式。
調用者不需要知道任何創建細節,也不用調用構造方法來創建對象。
使用場景
原型模式有如下使用場景:
- 類初始化消耗資源較多
- new產生的一個對象需要非常繁瑣的過程(數據準備、訪問權限等)
- 構造函數比較復雜
- 循環體內生成大量對象時
- 在Spring中,原型模式應用的非常廣泛,例如:scope='prototype'
我們可以將一些getter和setter之類封裝成一個工廠方法,然后對于使用的人來說,調用方法就可以了,不需要知道里面的getter和setter是怎么處理的。我們也可以使用JDK提供的實現Cloneable接口,實現快速復制。
創建對象的四種方式:
new、反射、克隆、序列化
實際案例
大家是否有遇到過這種常見,就是項目中規定,不能把與數據庫表映射的entity類返回給前端,所以通常返回給前端的有各種O,比如:XxxVO、XxxBO、XxxDTO...
這時候就會出現下面的場景,大家也想已經猜到了。
下面是與數據庫表映射的UserEntity實體類。
- public class UserEntity {
- private Long id;
- private String name;
- private Integer age;
- //....可能還有很多屬性
- //省略getter setter
- }
返回給前端或者調用方的UserVO實體類。
- public class UserVO {
- private Long id;
- private String name;
- private Integer age;
- //....可能還有很多屬性
- //省略getter setter
- }
此時,從數據庫里查出來的UserEntity需要轉換成UserVO,然后再返回給前端(或者調用方)。
- public class ObjectConvertUtil {
- public static UserVo convertUserEntityToUserVO(UserEntity userEntity) {
- if (userEntity == null) {
- return null;
- }
- UserVo userVo = new UserVo();
- userVo.setId(userEntity.getId());
- userVo.setName(userEntity.getName());
- userVo.setAge(userEntity.getAge());
- //如果還有更多屬性呢?
- return userVo;
- }
- }
從這個util類中,我們可以看出,如果一個類的屬性有幾十個,上百個的,這代碼量是不是有點恐怖?
于是,我們通常都會使用一些工具類來處理,比如常見有以下:
- BeanUtils.copy();
- JSON.parseObject()
- Guava工具類
- .....
這些工具類就用到了原型模式。
通過一個對象,創建一個新的對象。
也把原型模式稱之為對象的拷貝、克隆。
其實對象的克隆分淺克隆和深克隆,下面我們就來聊聊淺克隆和深克隆。
- 淺克隆:創建一個新對象,新對象的屬性和原來對象完全相同,對于非基本類型屬性,仍指向原來對象的屬性所指向的對象的內存地址。
- 深克?。簞摻ㄒ粋€新對象,屬性中引用的其他對象也會被克隆,不再指向原有對象地址。
我們先來聊聊淺克隆,都喜歡由淺入深。
淺克隆
比如,我現在相對用戶信息User進行克隆,但是User中有用戶地址信息UserAddress屬性。
以下是代碼的實現:
- //用戶地址信息
- public class UserAddress implements Serializable{
- private String province;
- private String cityCode;
- public UserAddress(String province, String cityCode) {
- this.province = province;
- this.cityCode = cityCode;
- }
- }
- //用戶信息
- public class User implements Cloneable {
- private int age;
- private String name;
- //用戶地址信息
- private UserAddress userAddress;
- //getter setter 省略
- @Override
- protected Object clone() throws CloneNotSupportedException {
- return super.clone();
- }
- }
- //測試
- public class UserTest {
- public static void main(String[] args) throws Exception {
- User user = new User();
- user.setAge(20);
- user.setName("田維常");
- UserAddress userAddress = new UserAddress("貴州", "梵凈山");
- user.setUserAddress(userAddress);
- User clone = (User) user.clone();
- System.out.println("克隆前后UserAddress比較:" + (user.getUserAddress() == clone.getUserAddress()));
- }
- }
輸出結果
- 克隆前后 UserAddress 比較:true
兩個對象屬性 UserAddress 指向的是同一個地址。
這就是所謂的淺克隆,只是克隆了對象,對于該對象的非基本類型屬性,仍指向原來對象的屬性所指向的對象的內存地址。
關系如下:
深克隆
關于深克隆,我們來用一個很經典的案例,西游記里的孫悟空。一個孫悟空能變成n多個孫悟空,手里都會拿著一個金箍棒。
按照前面的淺克隆,結果就是:孫悟空倒是變成很多孫悟空,但是金箍棒用的是同一根。
深克隆的結果是:孫悟空變成了很多個,金箍棒也變成很多個根。
下面我們用代碼來實現:
- //猴子,有身高體重和生日
- public class Monkey {
- public int height;
- public int weight;
- public Date birthday;
- }
孫悟空也是猴子,兵器 孫悟空有個金箍棒:
- import java.io.Serializable;
- //孫悟空的金箍棒
- public class JinGuBang implements Serializable{
- public float h=100;
- public float d=10;
- //金箍棒變大
- public void big(){
- this.h *=10;
- this.d *=10;
- }
- //金箍棒變小
- public void small(){
- this.h /=10;
- this.d /=10;
- }
- }
齊天大圣孫悟空:
- import java.io.*;
- import java.util.Date;
- //孫悟空有七十二變,拔猴毛生成一個金箍棒
- //使用JDK的克隆機制,
- //實現Cloneable并重寫clone方法
- public class QiTianDaSheng extends Monkey implements Cloneable, Serializable {
- public JinGuBang jinGuBang;
- public QiTianDaSheng() {
- this.birthday = new Date();
- this.jinGuBang = new JinGuBang();
- }
- @Override
- protected Object clone() throws CloneNotSupportedException {
- return this.deepClone();
- }
- //深克隆
- public QiTianDaSheng deepClone() {
- try {
- //內存中操作完成、對象讀寫,是通過字節碼直接操作
- //與序列化操作類似
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- ObjectOutputStream oos = new ObjectOutputStream(bos);
- oos.writeObject(this);
- ByteArrayInputStream bais = new ByteArrayInputStream(bos.toByteArray());
- ObjectInputStream bis = new ObjectInputStream(bais);
- //完成一個新的對象,底層是使用new創建的一個對象
- //詳情可以了解readObject方法
- QiTianDaSheng qiTianDaSheng = (QiTianDaSheng) bis.readObject();
- //每個猴子的生日不一樣,所以每次拷貝的時候,把生日改一下
- qiTianDaSheng.birthday = new Date();
- return qiTianDaSheng;
- } catch (Exception ex) {
- ex.printStackTrace();
- return null;
- }
- }
- //淺克隆,就是簡單的賦值
- public QiTianDaSheng shalllowClone(QiTianDaSheng target) {
- QiTianDaSheng qiTianDaSheng = new QiTianDaSheng();
- qiTianDaSheng.height = target.height;
- qiTianDaSheng.weight = target.weight;
- qiTianDaSheng.jinGuBang = target.jinGuBang;
- qiTianDaSheng.birthday = new Date();
- return qiTianDaSheng;
- }
- }
接著我們就來測試一下:
- public class DeepCloneTest {
- public static void main(String[] args) {
- QiTianDaSheng qiTianDaSheng = new QiTianDaSheng();
- try {
- QiTianDaSheng newObject = (QiTianDaSheng) qiTianDaSheng.clone();
- System.out.print("深克隆后 ");
- System.out.println("金箍棒是否一直:" + (qiTianDaSheng.jinGuBang == newObject.jinGuBang));
- } catch (Exception ex) {
- ex.printStackTrace();
- }
- QiTianDaSheng newObject=qiTianDaSheng.shalllowClone(qiTianDaSheng);
- System.out.print("淺克隆后 ");
- System.out.println("金箍棒是否一直:" + (qiTianDaSheng.jinGuBang == newObject.jinGuBang));
- }
- }
輸出結果為:
- 深克隆后 金箍棒是否一直:false
- 淺克隆后 金箍棒是否一直:true
結論
深克隆后每個孫悟空都有自己的金箍棒,而淺克隆后每個孫悟空用的金箍棒實質上還是同一根。
總結
切記:深和淺,指的是克隆對象里的屬性(引用類型)是否指向同一個內存地址。
為了更深刻的理解深克隆和淺克隆,我們回答文中的簡歷拷貝的故事。
- 深拷貝:拷貝一份簡歷,然后對簡歷中的信息進行修改成自己的
- 淺拷貝:拷貝一份簡歷,簡歷內容完全不變
優點:
- Java 原型模式基于內存二進制流復制,比直接 new 的性能會更好一些。
- 可以利用深克隆保存對象狀態,存一份舊的(克隆出來),在對其修改,可以充當一個撤銷功能。
缺點:
- 需要配置 clone 方法,改造時需要對已有類進行修改,違背 “開閉原則”。
- 如果對象間存在多重嵌套引用時,每一層都需要實現克隆。
我們從原型模式的定義,使用場景,真實案例、淺克隆、深克隆、優缺點等方面,對原型模式進行了一個全面的講解。
一句話總結:
一份簡歷,全班同學用
本文轉載自微信公眾號「Java后端技術全棧」,可以通過以下二維碼關注。轉載本文請聯系Java后端技術全棧公眾號。