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

枚舉和註解都是在Java1.5中引入的,枚舉改變了常量的聲明方式,註解耦合了數據和代碼。 java

建議83:推薦使用枚舉定義常量spring

建議84:使用構造函數協助描述枚舉項安全

建議85:當心switch帶來的空指針異常編輯器

建議86:在switch的default代碼塊中增長AssertError錯誤ide

建議87:使用valueOf前必須進行校驗函數

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

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

建議90:當心繼承註解this

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

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

建議83:推薦使用枚舉定義常量

常量聲明是每個項目都不可或缺的,在Java1.5以前,咱們只有兩種方式的聲明:類常量和接口常量。不過,在1.5版本之後有了改進,即新增了一種常量聲明方式:枚舉聲明常量,看以下代碼:

enum Season {
    SPRING, SUMMER, AUTUMN, WINTER;
}

提倡枚舉項所有大寫,字母之間用下劃線分割,這也是從常量的角度考慮的。

那麼枚舉常量與咱們常用的類常量和靜態常量相比有什麼優點?問得好,枚舉的優勢主要表如今四個方面:

一、枚舉常量簡單

二、枚舉常量屬於穩態型

public void describe(int s) {
        // s變量不能超越邊界,校驗條件
        if (s >= 0 && s < 4) {
            switch (s) {
            case Season.SPRING:
                System.out.println("this is spring");
                break;
            case Season.SUMMER:
                System.out.println("this is summer");
                break;
                ......
            }
        }
    }

對輸入值的檢查很吃力。

public void describe(Season s){
        switch(s){
        case Spring:
            System.out.println("this is "+Season.Spring);
            break;
        case Summer:
            System.out.println("this is summer"+Season.Summer);
            break;
                  ......
        }
    }

不用校驗,已經限定了是Season枚舉。

三、枚舉具備內置方法

public void query() {
    for (Season s : Season.values()) {
         System.out.println(s);
    }
}

經過values方法得到全部的枚舉項。

四、枚舉能夠自定義方法

關鍵是枚舉常量不只能夠定義靜態方法,還能夠定義非靜態方法。

雖然枚舉在不少方面比接口常量和類常量好用,可是有一點它是比不上接口常量和類常量的,那就是繼承,枚舉類型是不能繼承的,也就是說一個枚舉常量定義完畢後,除非修改重構,不然沒法作擴展,而接口常量和類常量則能夠經過繼承進行擴展。可是,通常常量在項目構建時就定義完畢了,不多會出現必須經過擴展才能實現業務邏輯的場景。

注意:在項目中推薦使用枚舉常量代替接口常量或類常量

建議84:使用構造函數協助描述枚舉項

枚舉描述:經過枚舉的構造函數,聲明每一個枚舉項必須具備的屬性和行爲,這是對枚舉項的描述和補充。

enum Role {
    Admin("管理員", new LifeTime(), new Scope()), User("普通用戶", new LifeTime(), new Scope());
    private String name;
    private LifeTime lifeTime;
    private Scope scope;
    /* setter和getter方法略 */

    Role(String _name, LifeTime _lifeTime, Scope _scope) {
        name = _name;
        lifeTime = _lifeTime;
        scope = _scope;
    }

}

class LifeTime {
}
class Scope {
}

這是一個角色定義類,描述了兩個角色:管理員和普通用戶,同時它還經過構造函數對這兩個角色進行了描述:

一、name:表示的是該角色的中文名稱

二、lifeTime:表示的是該角色的生命週期,也就是多長時間該角色失效

三、scope:表示的該角色的權限範圍

這樣一個描述可使開發者對Admin和User兩個常量有一個立體多維度的認知,有名稱,有周期,還有範圍,並且還能夠在程序中方便的得到此類屬性。因此,推薦你們在枚舉定義中爲每一個枚舉項定義描述,特別是在大規模的項目開發中,大量的常量定義使用枚舉項描述比在接口常量或類常量中增長註釋的方式友好的多,簡潔的多。

建議85:當心switch帶來的空指針異常

使用枚舉定義常量時。會伴有大量switch語句判斷,目的是爲了每一個枚舉項解釋其行爲,例如這樣一個方法: 

public static void doSports(Season season) {
    switch (season) {
        case Spring:
            System.out.println("春天放風箏");
            break;
        case Summer:
            System.out.println("夏天游泳");
            break;
        case Autumn:
            System.out.println("秋天是收穫的季節");
            break;
        case Winter:
            System.out.println("冬天滑冰");
            break;
        default:
            System.out.println("輸出錯誤");
            break;
    }
}
public static void main(String[] args) {
    doSports(null);
}
Exception in thread "main" java.lang.NullPointerException
    at com.book.study85.Client85.doSports(Client85.java:8)
    at com.book.study85.Client85.main(Client85.java:28)

輸入null時應該default的啊,爲何空指針異常呢?

目前Java中的switch語句只能判斷byte、short、char、int類型(JDk7容許使用String類型),這是Java編譯器的限制。問題是爲何枚舉類型也能夠跟在switch後面呢?

由於編譯時,編譯器判斷出switch語句後跟的參數是枚舉類型,而後就會根據枚舉的排序值繼續匹配,也就是或上面的代碼與如下代碼相同: 

public static void doSports(Season season) {
    switch (season.ordinal()) {//枚舉的排序值
        case season.Spring.ordinal():
            System.out.println("春天放風箏");
            break;
        case season.Summer.ordinal():
            System.out.println("夏天游泳");
            break;
            //......
    }
}

switch語句是先計算season變量的排序值,而後與枚舉常量的每一個排序值進行對比,在咱們的例子中season是null,沒法執行ordinal()方法,因而就報空指針異常了。問題清楚了,解決很簡單,在doSports方法中判斷輸入參數是否爲null便可。

建議86:在switch的default代碼塊中增長AssertError錯誤

建議87:使用valueOf前必須進行校驗

咱們知道每一個枚舉項都是java.lang.Enum的子類,均可以訪問Enum類提供的方法,好比hashCode、name、valueOf等,其中valueOf方法會把一個String類型的名稱轉換爲枚舉項,也就是在枚舉項中查找出字面值與參數相等的枚舉項。雖然這個方法簡單,可是JDK卻作了一個對於開發人員來講並不簡單的處理,咱們來看代碼:  

package OSChina.Client;

import java.util.Arrays;
import java.util.List;

public class Client15 {
    enum Season{
        SPRING,SUMMER,AUTUMN,WINTER
    }
    public static void main(String[] args) {
        List<String> params = Arrays.asList("SPRING","summer");
        for (String name:params){
            Season s = Season.valueOf(name);
            if(null!=s){
                System.out.println(s);
            }else {
                System.out.println("無相關枚舉項");
            }
        }
    }
}

看着沒問題啊,summer不在Season裏,就輸出無相關枚舉項就完事了嘛。。。

valueOf方法先經過反射從枚舉類的常量聲明中查找,若找到就直接返回,若找不到排除無效參數異常。valueOf本意是保護編碼中的枚舉安全性,使其不產生空枚舉對象,簡化枚舉操做,但卻引入了一個沒法避免的IllegalArgumentException異常。

解決此問題的方法:

一、拋異常

package OSChina.Client;

import java.util.Arrays;
import java.util.List;

public class Client15 {
    enum Season{
        SPRING,SUMMER,AUTUMN,WINTER
    }
    public static void main(String[] args) {
        List<String> params = Arrays.asList("SPRING","summer");
        try{
            for (String name:params){
                Season s = Season.valueOf(name);
                System.out.println(s);
            }
        }catch (IllegalArgumentException e){
            e.printStackTrace();
            System.out.println("無相關枚舉項");
        }

    }
}

二、擴展枚舉類:

枚舉中是能夠定義方法的,那就在枚舉項中自定義一個contains方法就能夠。

package OSChina.Client;

import java.util.Arrays;
import java.util.List;

public class Client15 {
    enum Season{
        SPRING,SUMMER,AUTUMN,WINTER;
        public static boolean contains(String name){
            for (Season s:Season.values()){
                if(s.name().equals(name)){
                    return true;
                }
            }
            return false;
        }
    }
    public static void main(String[] args) {
        List<String> params = Arrays.asList("SPRING","summer");
        for (String name:params){
            if(Season.contains(name)){
                Season s = Season.valueOf(name);
                System.out.println(s);
            }else {
                System.out.println("無相關枚舉項");
            }
        }
    }
}

我的感受第二種方法更好一些!

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

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

//抽象產品
interface Car{
    
}
//具體產品類
class FordCar implements Car{
    
}
//具體產品類
class BuickCar implements Car{
    
}
//工廠類
class CarFactory{
    //生產汽車
    public static Car createCar(Class<? extends Car> c){
        try {
            return c.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }
}

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

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

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

一、枚舉非靜態方法實現工廠方法模式

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

enum CarFactory {
    // 定義生產類能生產汽車的類型
    FordCar, BuickCar;
    // 生產汽車
    public Car create() {
        switch (this) {
        case FordCar:
            return new FordCar();
        case BuickCar:
            return new BuickCar();
        default:
            throw new AssertionError("無效參數");
        }
    }
}

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

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

二、經過抽象方法生成產品

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

enum CarFactory {
    // 定義生產類能生產汽車的類型
    FordCar{
        public Car create(){
            return new FordCar();
        }
    },
    BuickCar{
        public Car create(){
            return new BuickCar();
        }
    };
    //抽象生產方法
    public abstract Car create();
}

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

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

一、避免錯誤調用的發生:通常工廠方法模式中的生產方法,能夠接收三種類型的參數:類型參數、String參數、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的實現類。這嚴重違反了迪克特原則,也就是最少知識原則:一個對象該對其它對象有最少的瞭解。

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

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

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

Java集合之EnumSet

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

建議90:當心繼承註解

Java從1.5版本開始引入註解(Annotation),@Inheruted,它表示一個註解是否能夠自動繼承。

淺談java註解<最通俗易懂的講解>

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

註解的寫法和接口很類似,都採用關鍵字interface,並且都不能有實現代碼,常量定義默認都是public static final類型的,它們的主要不一樣點是:註解要在interface前加@字符,並且不能繼承,不能實現。

咱們舉例說明一下,以訪問權限列表爲例:

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

public enum CommonIdentifier implements Identifier {
    // 權限級別
    Reader, Author, Admin;
    @Override
    public boolean identify() {
        return false;
    }
}
package OSChina.reflect;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Access {
    //什麼級別能夠訪問,默認是管理員
    CommonIdentifier level () default CommonIdentifier.Admin;
}
package OSChina.reflect;

@Access(level=CommonIdentifier.Author)
public class Foo {
}
package OSChina.reflect;

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

0b1c0904e1ec26eb288db4e67d48ae2ea18.jpg

看看上面的代碼,簡單易懂,全部的開發人員只要增長註解便可解決訪問控制問題。

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

@Override註解用於方法的覆寫上,它是在編譯器有效,也就是Java編譯器在編譯時會根據註解檢查方法是否真的是覆寫,若是不是就報錯,拒絕編譯。

 

編寫高質量代碼:改善Java程序的151個建議@目錄

相關文章
相關標籤/搜索