編寫高質量代碼:改善Java程序的151個建議(第6章:枚舉和註解___建議88~92)

建議88:用枚舉實現工廠方法模式更簡潔

  工廠方法模式(Factory Method Pattern)是" 建立對象的接口,讓子類決定實例化哪個類,並使一個類的實例化延遲到其它子類"。工廠方法模式在咱們的開發中常常會用到。下面以汽車製造爲例,看看通常的工廠方法模式是如何實現的,代碼以下:java

 1 //抽象產品
 2 interface Car{
 3     
 4 }
 5 //具體產品類
 6 class FordCar implements Car{
 7     
 8 }
 9 //具體產品類
10 class BuickCar implements Car{
11     
12 }
13 //工廠類
14 class CarFactory{
15     //生產汽車
16     public static Car createCar(Class<? extends Car> c){
17         try {
18             return c.newInstance();
19         } catch (InstantiationException | IllegalAccessException e) {
20             e.printStackTrace();
21         }
22         return null;
23     }
24 }

  這是最原始的工廠方法模式,有兩個產品:福特汽車和別克汽車,而後經過工廠方法模式來生產。有了工廠方法模式,咱們就不用關心一輛車具體是怎麼生成的了,只要告訴工廠" 給我生產一輛福特汽車 "就能夠了,下面是產出一輛福特汽車時客戶端的代碼: 程序員

    public static void main(String[] args) {
        //生產車輛
        Car car = CarFactory.createCar(FordCar.class);
    }

  這就是咱們常用的工廠方法模式,但常用並不表明就是最優秀、最簡潔的。此處再介紹一種經過枚舉實現工廠方法模式的方案,誰優誰劣你自行評價。枚舉實現工廠方法模式有兩種方法:數組

(1)、枚舉非靜態方法實現工廠方法模式ide

  咱們知道每一個枚舉項都是該枚舉的實例對象,那是否是定義一個方法能夠生成每一個枚舉項對應產品來實現此模式呢?代碼以下:函數

 1 enum CarFactory {
 2     // 定義生產類能生產汽車的類型
 3     FordCar, BuickCar;
 4     // 生產汽車
 5     public Car create() {
 6         switch (this) {
 7         case FordCar:
 8             return new FordCar();
 9         case BuickCar:
10             return new BuickCar();
11         default:
12             throw new AssertionError("無效參數");
13         }
14     }
15 
16 }

  create是一個非靜態方法,也就是隻有經過FordCar、BuickCar枚舉項才能訪問。採用這種方式實現工廠方法模式時,客戶端要生產一輛汽車就很簡單了,代碼以下: 性能

public static void main(String[] args) {
        // 生產車輛
        Car car = CarFactory.BuickCar.create();
    }

(2)、經過抽象方法生成產品ui

  枚舉類型雖然不能繼承,可是能夠用abstract修飾其方法,此時就表示該枚舉是一個抽象枚舉,須要每一個枚舉項自行實現該方法,也就是說枚舉項的類型是該枚舉的一個子類,咱們倆看代碼:this

 1 enum CarFactory {
 2     // 定義生產類能生產汽車的類型
 3     FordCar{
 4         public Car create(){
 5             return new FordCar();
 6         }
 7     },
 8     BuickCar{
 9         public Car create(){
10             return new BuickCar();
11         }
12     };
13     //抽象生產方法
14     public abstract Car create();
15 }

  首先定義一個抽象製造方法create,而後每一個枚舉項自行實現,這種方式編譯後會產生CarFactory的匿名子類,由於每一個枚舉項都要實現create抽象方法。客戶端調用與上一個方案相同,再也不贅述。spa

  你們可能會問,爲何要使用枚舉類型的工廠方法模式呢?那是由於使用枚舉類型的工廠方法模式有如下三個優勢:設計

  • 避免錯誤調用的發生:通常工廠方法模式中的生產方法(也就是createCar方法),能夠接收三種類型的參數:類型參數(如咱們的例子)、String參數(生產方法中判斷String參數是須要生產什麼產品)、int參數(根據int值判斷須要生產什麼類型的的產品),這三種參數都是寬泛的數據類型,很容易發生錯誤(好比邊界問題、null值問題),並且出現這類錯誤編譯器還不會報警,例如:
    public static void main(String[] args) {
        // 生產車輛
        Car car = CarFactory.createCar(Car.class);
    }

  Car是一個接口,徹底合乎createCar的要求,因此它在編譯時不會報任何錯誤,但一運行就會報出InstantiationException異常,而使用枚舉類型的工廠方法模式就不存在該問題了,不須要傳遞任何參數,只須要選擇好生產什麼類型的產品便可。

  • 性能好,使用簡潔:枚舉類型的計算時以int類型的計算爲基礎的,這是最基本的操做,性能固然會快,至於使用便捷,注意看客戶端的調用,代碼的字面意思就是" 汽車工廠,我要一輛別克汽車,趕快生產"。
  • 下降類間耦合:無論生產方法接收的是Class、String仍是int的參數,都會成爲客戶端類的負擔,這些類並非客戶端須要的,而是由於工廠方法的限制必須輸入的,例如Class參數,對客戶端main方法來講,他須要傳遞一個FordCar.class參數才能生產一輛福特汽車,除了在create方法中傳遞參數外,業務類不須要改Car的實現類。這嚴重違背了迪米特原則(Law of Demeter  簡稱LoD),也就是最少知識原則:一個對象應該對其它對象有最少的瞭解。

  而枚舉類型的工廠方法就沒有這種問題了,它只須要依賴工廠類就能夠生產一輛符合接口的汽車,徹底能夠無視具體汽車類的存在。

建議89:枚舉項的數量限制在64個之內

  爲了更好地使用枚舉,Java提供了兩個枚舉集合:EnumSet和EnumMap,這兩個集合使用的方法都比較簡單,EnumSet表示其元素必須是某一枚舉的枚舉項,EnumMap表示Key值必須是某一枚舉的枚舉項,因爲枚舉類型的實例數量固定而且有限,相對來講EnumSet和EnumMap的效率會比其它Set和Map要高。

      雖然EnumSet很好用,可是它有一個隱藏的特色,咱們逐步分析。在項目中通常會把枚舉用做常量定義,可能會定義很是多的枚舉項,而後經過EnumSet訪問、遍歷,但它對不一樣的枚舉數量有不一樣的處理方式。爲了進行對比,咱們定義兩個枚舉,一個數量等於64,一個是65(大於64便可,爲何是64而不是128,512呢,一會解釋),代碼以下: 

 1 //普通枚舉項,數量等於64
 2 enum Const{
 3     A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,
 4     AA,BB,CC,DD,EE,FF,GG,HH,II,JJ,KK,LL,MM,NN,OO,PP,QQ,RR,SS,TT,UU,VV,WW,XX,YY,ZZ,
 5     AAA,BBB,CCC,DDD,EEE,FFF,GGG,HHH,III,JJJ,KKK,LLL
 6 }
 7 //大枚舉,數量超過64
 8 enum LargeConst{
 9     A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,
10     AA,BB,CC,DD,EE,FF,GG,HH,II,JJ,KK,LL,MM,NN,OO,PP,QQ,RR,SS,TT,UU,VV,WW,XX,YY,ZZ,
11     AAAA,BBBB,CCCC,DDDD,EEEE,FFFF,GGGG,HHHH,IIII,JJJJ,KKKK,LLLL,MMMM
12 }

  Const的枚舉項數量是64,LagrgeConst的枚舉項數量是65,接下來咱們但願把這兩個枚舉轉換爲EnumSet,而後判斷一下它們的class類型是否相同,代碼以下: 

 1 public class Client89 {
 2     public static void main(String[] args) {
 3         EnumSet<Const> cs = EnumSet.allOf(Const.class);
 4         EnumSet<LargeConst> lcs = EnumSet.allOf(LargeConst.class);
 5         //打印出枚舉數量
 6         System.out.println("Const的枚舉數量:"+cs.size());
 7         System.out.println("LargeConst的枚舉數量:"+lcs.size());
 8         //輸出兩個EnumSet的class
 9         System.out.println(cs.getClass());
10         System.out.println(lcs.getClass());
11     }
12 }

  程序很簡單,如今的問題是:cs和lcs的class類型是否相同?應該相同吧,都是EnumSet類的工廠方法allOf生成的EnumSet類,並且JDK API也沒有提示EnumSet有子類。咱們來看看輸出結果:

  

  很遺憾,二者不相等。就差一個元素,二者就不相等了?確實如此,這也是咱們重點關注枚舉項數量的緣由。先來看看Java是如何處理的,首先跟蹤allOf方法,其源碼以下:  

1  public static <E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType) {
2         //生成一個空EnumSet
3         EnumSet<E> result = noneOf(elementType);
4         //加入全部的枚舉項
5         result.addAll();
6         return result;
7     }

  allOf經過noneOf方法首先生成了一個EnumSet對象,而後把全部的枚舉都加進去,問題可能就出在EnumSet的生成上了,咱們來看看noneOf的源碼:  

 1   public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
 2         //得到全部的枚舉項
 3         Enum[] universe = getUniverse(elementType);
 4         if (universe == null)
 5             throw new ClassCastException(elementType + " not an enum");
 6         //枚舉數量小於等於64
 7         if (universe.length <= 64)
 8             return new RegularEnumSet<>(elementType, universe);
 9         else 
10             //枚舉數量大於64
11             return new JumboEnumSet<>(elementType, universe);
12     }

  看到這裏,恍然大悟,Java原來是如此處理的:當枚舉項數量小於等於64時,建立一個RegularEnumSet實例對象,大於64時則建立一個JumboEnumSet實例對象。

  爲何要如此處理呢?這還要看看這兩個類之間的差別,首先看RegularEnumSet類,源碼以下:

 1 class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {
 2     private static final long serialVersionUID = 3411599620347842686L;
 3     /**
 4      * Bit vector representation of this set.  The 2^k bit indicates the
 5      * presence of universe[k] in this set.
 6      */
 7     //記錄全部的枚舉號,注意是long型
 8     private long elements = 0L;
 9    //構造函數
10     RegularEnumSet(Class<E>elementType, Enum[] universe) {
11         super(elementType, universe);
12     }
13 
14    //加入全部元素
15     void addAll() {
16         if (universe.length != 0)
17             elements = -1L >>> -universe.length;
18     }
19     
20    //其它代碼略
21 }

  咱們知道枚舉項的排序值ordinal 是從0、一、2......依次遞增的,沒有重號,沒有跳號,RegularEnumSet就是利用這一點把每一個枚舉項的ordinal映射到一個long類型的每一個位置上的,注意看addAll方法的elements元素,它使用了無符號右移操做,而且操做數是負值,位移也是負值,這表示是負數(符號位是1)的"無符號左移":符號位爲0,並補充低位,簡單的說,Java把一個很少於64個枚舉項映射到了一個long類型變量上。這纔是EnumSet處理的重點,其餘的size方法、contains方法等都是根據elements方法等都是根據elements計算出來的。想一想看,一個long類型的數字包含了全部的枚舉項,其效率和性能能確定是很是優秀的。

  咱們知道long類型是64位的,因此RegularEnumSet類型也就只能負責枚舉項的數量不大於64的枚舉(這也是咱們以64來舉例,而不以128,512舉例的緣由),大於64則由JumboEnumSet處理,咱們看它是怎麼處理的: 

 1 class JumboEnumSet<E extends Enum<E>> extends EnumSet<E> {
 2     private static final long serialVersionUID = 334349849919042784L;
 3 
 4     /**
 5      * Bit vector representation of this set.  The ith bit of the jth
 6      * element of this array represents the  presence of universe[64*j +i]
 7      * in this set.
 8      */
 9    //映射全部的枚舉項
10     private long elements[];
11 
12     // Redundant - maintained for performance
13     private int size = 0;
14 
15     JumboEnumSet(Class<E>elementType, Enum[] universe) {
16         super(elementType, universe);
17         //默認長度是枚舉項數量除以64再加1
18         elements = new long[(universe.length + 63) >>> 6];
19     }
20 
21       void addAll() {
22         //elements中每一個元素表示64個枚舉項
23         for (int i = 0; i < elements.length; i++)
24             elements[i] = -1;
25         elements[elements.length - 1] >>>= -universe.length;
26         size = universe.length;
27     }
28 }

  JumboEnumSet類把枚舉項按照64個元素一組拆分紅了多組,每組都映射到一個long類型的數字上,而後該數組再放置到elements數組中,簡單來講JumboEnumSet類的原理與RegularEnumSet類似,只是JumboEnumSet使用了long數組容納更多的枚舉項。不過,這樣的程序看着會不會以爲鬱悶呢?其實這是由於咱們在開發中不多使用位移操做。你們能夠這樣理解:RegularEnumSet是把每一個枚舉項映射到一個long類型數字的每一個位上,JumboEnumSet是先按照64個一組進行拆分,而後每一個組再映射到一個long類型數字的每一個位上。

  從以上的分析可知,EnumSet提供的兩個實現都是基本的數字類型操做,其性能確定比其餘的Set類型要好的多,特別是Enum的數量少於64的時候,那簡直就是飛通常的速度。

  注意:枚舉項數量不要超過64,不然建議拆分。

建議90:當心註解繼承

  Java從1.5版本開始引入註解(Annotation),其目的是在不影響代碼語義的狀況下加強代碼的可讀性,而且不改變代碼的執行邏輯,對於註解始終有兩派爭論,正方認爲註解有益於數據與代碼的耦合,"在有代碼的周邊集合數據";反方認爲註解把代碼和數據混淆在一塊兒,增長了代碼的易變性,消弱了程序的健壯性和穩定性。這些爭論暫且擱置,咱們要說的是一個咱們不經常使用的元註解(Meta-Annotation):@Inheruted,它表示一個註解是否能夠自動繼承,咱們開看它如何使用。

  思考一個例子,好比描述鳥類,它有顏色、體型、習性等屬性,咱們以顏色爲例,定義一個註解來修飾一下,代碼以下:

 1 import java.lang.annotation.ElementType;
 2 import java.lang.annotation.Inherited;
 3 import java.lang.annotation.Retention;
 4 import java.lang.annotation.RetentionPolicy;
 5 import java.lang.annotation.Target;
 6 
 7 @Retention(RetentionPolicy.RUNTIME)
 8 @Target(ElementType.TYPE)
 9 @Inherited
10 public @interface Desc {
11     enum Color {
12         White, Grayish, Yellow
13     }
14 
15     // 默認顏色是白色的
16     Color c() default Color.White;
17 }

  該註解Desc前增長了三個註解:Retention表示的是該註解的保留級別,Target表示的是註解能夠標註在什麼地方,@Inherited表示該註解會被自動繼承。註解定義完畢,咱們把它標註在類上,代碼以下: 

 1 @Desc(c = Color.White)
 2 abstract class Bird {
 3     public abstract Color getColor();
 4 }
 5 
 6 // 麻雀
 7 class Sparrow extends Bird {
 8     private Color color;
 9 
10     // 默認是淺灰色
11     public Sparrow() {
12         color = Color.Grayish;
13     }
14 
15     // 構造函數定義鳥的顏色
16     public Sparrow(Color _color) {
17         color = _color;
18     }
19 
20     @Override
21     public Color getColor() {
22         return color;
23     }
24 }
25 
26 // 鳥巢,工廠方法模式
27 enum BirdNest {
28     Sparrow;
29     // 鳥類繁殖
30     public Bird reproduce() {
31         Desc bd = Sparrow.class.getAnnotation(Desc.class);
32         return bd == null ? new Sparrow() : new Sparrow(bd.c());
33     }
34 }

  上面程序聲明瞭一個Bird抽象類,而且標註了Desc註解,描述鳥類的顏色是白色,而後編寫一個麻雀Sparrow類,它有兩個構造函數,一個是默認的構造函數,也就是咱們常常看到的麻雀是淺灰色的,另一個構造函數是自定義麻雀的顏色,以後又定義了一個鳥巢(工廠方法模式),它是專門負責鳥類繁殖的,它的生產方法reproduce會根據實現類註解信息生成不一樣顏色的麻雀。咱們編寫一個客戶端調用,代碼以下:   

1 public static void main(String[] args) {
2         Bird bird = BirdNest.Sparrow.reproduce();
3         Color color = bird.getColor();
4         System.out.println("Bird's color is :" + color);
5     }

  如今問題是這段客戶端程序會打印出什麼來?由於採用了工廠方法模式,它最主要的問題就是bird變量到底採用了那個構造函數來生成,是無參構造函數仍是有參構造?若是咱們單獨看子類Sparrow,它沒有被添加任何註釋,那工廠方法中的bd變量就應該是null了,應該調用的是無參構造。是否是如此呢?咱們來看運行結果:「Bird‘s  Color  is White 」;

  白色?這是咱們添加到父類Bird上的顏色,爲何?這是由於咱們在註解上加了@Inherited註解,它表示的意思是咱們只要把註解@Desc加到父類Bird上,它的全部子類都會從父類繼承@Desc註解,不須要顯示聲明,這與Java的繼承有點不一樣,若Sparrow類繼承了Bird卻不用顯示聲明,只要@Desc註解釋可自動繼承的便可。

  採用@Inherited元註解有利有弊,利的地方是一個註解只要標註到父類,全部的子類都會自動具備父類相同的註解,整齊,統一併且便於管理,弊的地方是單單閱讀子類代碼,咱們無從知道爲什麼邏輯會被改變,由於子類沒有顯示標註該註解。整體上來講,使用@Inherited元註解弊大於利,特別是一個類的繼承層次較深時,若是註解較多,則很難判斷出那個註解對子類產生了邏輯劫持。

建議91:枚舉和註解結合使用威力更大

  咱們知道註解的寫法和接口很相似,都採用了關鍵字interface,並且都不能有實現代碼,常量定義默認都是public static final  類型的等,它們的主要不一樣點是:註解要在interface前加上@字符,並且不能繼承,不能實現,這常常會給咱們的開發帶來些障礙。  

  咱們來分析一下ACL(Access  Control   List,訪問控制列表)設計案例,看看如何避免這些障礙,ACL有三個重要元素:

  • 資源,有哪些信息是要被控制起來的。
  • 權限級別,不一樣的訪問者規劃在不一樣的級別中。
  • 控制器(也叫鑑權人),控制不一樣的級別訪問不一樣的資源。

  鑑權人是整個ACL的設計核心,咱們從最主要的鑑權人開始,代碼以下:   

interface Identifier{
    //無權訪問時的禮貌語
    String REFUSE_WORD  =  "您無權訪問";
    //鑑權
    public  boolean identify();
}

  這是一個鑑權人接口,定義了一個常量和一個鑑權方法。接下來應該實現該鑑權方法,但問題是咱們的權限級別和鑑權方法之間是緊耦合,若分拆成兩個類顯得有點囉嗦,怎麼辦?咱們能夠直接頂一個枚舉來實現,代碼以下:

 1 enum CommonIdentifier implements Identifier {
 2     // 權限級別
 3     Reader, Author, Admin;
 4 
 5     @Override
 6     public boolean identify() {
 7         return false;
 8     }
 9 
10 }

  定義了一個通用鑑權者,使用的是枚舉類型,而且實現了鑑權者接口。如今就剩下資源定義了,這很容易定義,資源就是咱們寫的類、方法等,以後再經過配置來決定哪些類、方法容許什麼級別的訪問,這裏的問題是:怎麼把資源和權限級別關聯起來呢?使用XML配置文件?是個方法,但對咱們的示例程序來講顯得太繁重了,若是使用註解會更簡潔些,不過這須要咱們首先定義出權限級別的註解,代碼以下:

1 @Retention(RetentionPolicy.RUNTIME)
2 @Target(ElementType.TYPE)
3 @interface Access{
4     //什麼級別能夠訪問,默認是管理員
5     CommonIdentifier level () default CommonIdentifier.Admin;
6 }

  該註解釋標註在類上面的,而且會保留到運行期。咱們定義一個資源類,代碼以下: 

@Access(level=CommonIdentifier.Author)
class Foo{
    
}

  Foo類只能是做者級別的人訪問。場景都定義完畢了,那咱們看看如何模擬ACL實現,代碼以下:

 1 public static void main(String[] args) {
 2         // 初始化商業邏輯
 3         Foo b = new Foo();
 4         // 獲取註解
 5         Access access = b.getClass().getAnnotation(Access.class);
 6         // 沒有Access註解或者鑑權失敗
 7         if (null == access || !access.level().identify()) {
 8             // 沒有Access註解或者鑑權失敗
 9             System.out.println(access.level().REFUSE_WORD);
10         }
11     }

  看看這段代碼,簡單,易讀,並且若是咱們是經過ClassLoader類來解釋該註解的,那會使咱們的開發更簡潔,全部的開發人員只要增長註解便可解決訪問控制問題。注意看加粗代碼,access是一個註解類型,咱們想使用Identifier接口的identity鑑權方法和REFUSE_WORD常量,但註解釋不能集成的,那怎麼辦?此處,可經過枚舉類型CommonIdentifier從中間作一個委派動做(Delegate),委派?你能夠然identity返回一個對象,或者在Identifier上直接定義一個常量對象,那就是「赤裸裸」 的委派了。

建議92:注意@Override不一樣版本的區別

  @Override註解用於方法的覆寫上,它是在編譯器有效,也就是Java編譯器在編譯時會根據註解檢查方法是否真的是覆寫,若是不是就報錯,拒絕編譯。該註解能夠很大程度地解決咱們的誤寫問題,好比子類和父類的方法名少寫一個字符,或者是數字0和字母O爲區分出來等,這基本是每一個程序員都曾將犯過的錯誤。在代碼中加上@Override註解基本上能夠杜絕出現此類問題,可是@Override有個版本問題,咱們來看以下代碼:

 1 interface Foo {
 2     public void doSomething();
 3 }
 4 
 5 class FooImpl implements Foo{
 6     @Override
 7     public void doSomething() {
 8         
 9     }
10 }

 這是一個簡單的@Override示例,接口中定義了一個doSomething方法,實現類FooImpl實現此方法,而且在方法前加上了@Override註解。這段代碼在Java1.6版本上編譯沒問題,雖然doSomething方法只是實現了接口的定義,嚴格來講並非覆寫,但@Override出如今這裏可減小代碼中出現的錯誤。

  可若是在Java1.5版本上編譯此段代碼可能會出現錯誤:

      The  method doSomeThing()  of type FooImpl must override  a superclass  method 

  注意,這是個錯誤,不能繼續編譯,緣由是Java1.5版本的@Override是嚴格遵照覆寫的定義:子類方法與父類方法必須具備相同的方法名、輸出參數、輸出參數(容許子類縮小)、訪問權限(容許子類擴大),父類必須是一個類,不能是接口,不然不能算是覆寫。而這在Java1.6就開放了不少,實現接口的方法也能夠加上@Override註解了,能夠避免粗枝大葉致使方法名稱與接口不一致的狀況發生。

  在多環境部署應用時,需呀考慮@Override在不一樣版本下表明的意義,若是是Java1.6版本的程序移植到1.5版本環境中,就須要刪除實現接口方法上的@Override註解。

相關文章
相關標籤/搜索