JAVA設計模式什麼鬼(原型)——作者:凸凹裡歐
原型(Prototype)是什麼意思?工業生產中通常是指在量產之前研發出的概念實現,如果可行性滿足即可參照原型進行量產。有人說了,那不就是印章?其實這並不怎麼貼切,印章並不是最終例項,我更願意稱其為“類”!
大家一定見過這種印章吧,就是皮帶輪可以轉動,可隨意調整成自己需要的文字,其實跟我們的四大發明活字印刷同出一轍,我們填完表格簽好字,行政人員拿這個往上一蓋,一個日期便出現在落款出。
其實當行政人員調整好了文字,照紙上蓋下去那一剎那,其實就類似於例項化的過程了,new Stamp();每個蓋出的印都可以不一樣,例如我們更換了日期,那麼每天都有不同日期的例項了,那有人意識到了,同一天的那些例項們,其實是完全一模一樣的例項拷貝,那這就比較麻煩,每個文件都要用章子(類)去蓋(例項化)一下。
好了,讓我們忘掉蓋章例項化模式吧。通常我們都是怎樣做協議書的呢?搞一個Word文件吧,寫好後複製給別人修改就好了。
注意了,行政人員要新建一個word文件了,這個過程其實是在例項化,我們暫且叫它“零號”檔案,那當寫好了文件後,把這個檔案複製給其他公司員工去填寫,那麼這個零號檔案我們就稱之為“原型”。
想必我們已經搞明白了,原型模式,實際上是從原型例項複製克隆出新例項,而絕不是從類去例項化,這個過程的區別一定要搞清楚!OK,那開始我們的實戰部分。
假設我們要做一個打飛機遊戲,遊戲設定位縱版移動,單打。
既然是單打,那我們的主角飛機當然只有一架,於是我們寫一個單例模式(
1 public class EnemyPlane { 2 private int x;//敵機橫座標 3 private int y = 0;//敵機縱座標 4 5 public EnemyPlane(int x) {//構造器 6 this.x = x; 7 } 8 9 public int getX() { 10 return x; 11 } 12 13 public int getY() { 14 return y; 15 } 16 17 public void fly(){//讓敵機飛 18 y++;//每呼叫一次,敵機飛行時縱座標+1 19 } 20 }
程式碼第5行,初始化只接收x座標,因為敵機一開始是從頂部出來所以縱座標y必然是0。此類只提供getter而沒有setter,也就是說只能在初始化時確定敵機的橫座標x,後續是不需要更改座標了,只要連續呼叫第17行的fly方法即可讓飛機跟雨點一樣往下砸。
好了,我們開始繪製敵機動畫了,先例項化出50架吧。
1 public class Client {
2 public static void main(String[] args) {
3 List<EnemyPlane> enemyPlanes = new ArrayList<EnemyPlane>();
4
5 for (int i = 0; i < 50; i++) {
6 //此處隨機位置產生敵機
7 EnemyPlane ep = new EnemyPlane(new Random().nextInt(200));
8 enemyPlanes.add(ep);
9 }
10
11 }
12 }
注意程式碼第7行,覺不覺得每個迭代都例項化new出一個物件存在效能問題呢?答案是肯定的,這個例項化的過程是得不償失的,構造方法會被呼叫50次,cpu被極大浪費了,記憶體被極大浪費了,尤其對於遊戲來說效能瓶頸絕對是大忌,這會造成使用者體驗問題,誰也不希望玩遊戲會卡幀吧。
那到底什麼時候去new?遊戲場景初始化就new敵機(如以上程式碼)?這關會出現500個敵機那我們一次都new出來吧?浪費記憶體!那我們實時的去new,每到一個地方才new出來一個!浪費CPU!如果敵機執行緒過多造成CPU資源耗盡,每出一個敵機遊戲會卡一下,試想一下這種極端情況下,遊戲物件例項很多的話就是在作死。
解決方案到底是什麼呢?好,原型模式Prototype!上程式碼!我們把上面的敵機類改造一下,讓它支援原型拷貝。
1 public class EnemyPlane implements Cloneable{//此處實現克隆介面
2 private int x;//敵機橫座標
3 private int y = 0;//敵機縱座標
4
5 public EnemyPlane(int x) {//構造器
6 this.x = x;
7 }
8
9 public int getX() {
10 return x;
11 }
12
13 public int getY() {
14 return y;
15 }
16
17 public void fly(){//讓敵機飛
18 y++;//每呼叫一次,敵機飛行時縱座標+1
19 }
20
21 //此處開放setX,為了讓克隆後的例項重新修改x座標
22 public void setX(int x) {
23 this.x = x;
24 }
25
26 //為了保證飛機飛行的連貫性
27 //這裡我們關閉setY方法,不支援隨意更改Y縱座標
28// public void setY(int y) {
29// this.y = y;
30// }
31
32 //重寫克隆方法
33 @Override
34 public EnemyPlane clone() throws CloneNotSupportedException {
35 return (EnemyPlane)super.clone();
36 }
37 }
注意看從第21行開始的修改,setX()方法為了保證克隆飛機的個性化,因為它們出現的位置是不同的。第34行的克隆方法重寫我們呼叫了父類Object的克隆方法,這裡JVM會進行記憶體操作直接拷貝原始資料流,簡單粗暴,不會有其他更多的複雜操作(類載入,例項化,初始化等等),速度遠遠快於例項化操作。OK,我們看怎麼克隆這些敵機,做一個造飛機的工廠吧。
1 public class EnemyPlaneFactory {
2 //此處用痴漢模式造一個敵機原型
3 private static EnemyPlane protoType = new EnemyPlane(200);
4
5 //獲取敵機克隆例項
6 public static EnemyPlane getInstance(int x){
7 EnemyPlane clone = protoType.clone();//複製原型機
8 clone.setX(x);//重新設定克隆機的x座標
9 return clone;
10 }
11 }
此處我們省去抓異常,隨後的事情就非常簡單了,我們只需要很簡單地呼叫EnemyPlaneFactory.getInstance(int x)並宣告x座標位置,一架敵機很快地就做好了,並且我們保證是在敵機出現的時候再去克隆,確保不要一開局就全部克隆出來,如此一來,既保證了實時性節省了記憶體空間,又保證了敵機例項化的速度,遊戲絕不會卡幀!至於此處程式碼中的懶漢原型還可以怎樣優化那就要根據具體場景了,交給大家自由發揮吧,這裡只說明主要問題。
最後,還要強調一點就是淺拷貝和深拷貝的問題。假如我們的敵機類裡有一顆子彈bullet可以射擊我們的主角,如下。
1 public class EnemyPlane implements Cloneable{
2 private Bullet bullet = new Bullet();
3 private int x;//敵機橫座標
4 private int y = 0;//敵機縱座標
5
6 //之後程式碼省略……
7 }
我們都知道Java中的變數分為原始型別和引用型別,所謂淺拷貝只是拷貝原始型別的指,比如座標x, y的指會被拷貝到克隆物件中,對於物件bullet也會被拷貝,但是請注意拷貝的只是地址而已,那麼多個地址其實真正指向的物件還是同一個bullet。
由於我們呼叫父類Object的clone方法進行的是淺拷貝,所以此處的bullet並沒有被克隆成功,比如我們每架敵機必須攜帶的子彈是不同的例項,那麼我們就必須進行深拷貝,於是我們的程式碼就得做這樣的改動。
1 public class EnemyPlane implements Cloneable{
2 private Bullet bullet = new Bullet();
3
4 public void setBullet(Bullet bullet) {
5 this.bullet = bullet;
6 }
7
8 @Override
9 protected EnemyPlane clone() throws CloneNotSupportedException {
10 EnemyPlane clonePlane = (EnemyPlane) super.clone();//先克隆出敵機,其中子彈還未進行克隆。
11 clonePlane.setBullet(this.bullet.clone());//對子彈進行深拷貝
12 return clonePlane;
13 }
14
15 //之後程式碼省略……
16 }
相信大家看註釋就能懂了,這裡就不做過多解釋,當然對於Bullet類也同樣實現了克隆介面,程式碼不用再寫了吧?相信大家都學會了舉一反三。至此,我們的每個敵機攜帶的彈藥也同樣被克隆完畢了,再也不必擔心遊戲的流暢性了。