EffectiveJava(5)之枚舉和註解

注:本文是《Effective Java》學習的筆記。

Java支持兩種特殊用途的引用類型,一種是類,稱作枚舉類型。另一種是接口,稱作註解類型。

34.用enum代替int常量

枚舉類型有兩個特徵,一是它可能的值是有限的且預先定義的,二是枚舉值都有一個順序

枚舉之前常常使用int常量來存儲常量數值,它存在些許不足。int常量方式沒有類型安全性。沒有可描述性。int常量是編譯時常量,它們的int值會被編譯到使用它們的客戶端中,如果與int常量關聯的值發生了變化,客戶端必須重新編譯。

而對於字符串常量會導致性能問題,因爲它依賴於字符串的比較操作。

 

java枚舉類型的基本想法非常簡單:這些類通過 public static final 域爲每個枚舉變量導出了一個實例。枚舉類型沒有可以訪問的構造器,所以它是真正的final類。客戶端不能創建枚舉類型的實例,也不能對它進行擴展,因此不存在實例,而只存在聲明過的枚舉常量。換句話說,枚舉類型是實例受控的。它們是單例的泛型化,本質上是單元素的枚舉。

如下的一個枚舉

public enum Fruit {
    BANANA(4.0),APPLE(8.8),ORANGE(6.5);
    private double price;
    private Fruit(double fruitPrice){
        this.price = fruitPrice;
    }

    public static void main(String[] args) {
        System.out.println(BANANA.price + "--"+APPLE.price+"--"+ORANGE.price);
    }
}

編譯成字節碼文件後再反編譯的結果如下。 (沒截靜態static塊) javap -c Fruit  

public final class zy.service.effective.five.Fruit extends java.lang.Enum<zy.service.effective.five.Fruit> {
  public static final zy.service.effective.five.Fruit BANANA;

  public static final zy.service.effective.five.Fruit APPLE;

  public static final zy.service.effective.five.Fruit ORANGE;

  public static zy.service.effective.five.Fruit[] values();
    Code:
       0: getstatic     #1                  // Field $VALUES:[Lzy/service/effective/five/Fruit;
       3: invokevirtual #2                  // Method "[Lzy/service/effective/five/Fruit;".clone:()Ljava/lang/Object;
       6: checkcast     #3                  // class "[Lzy/service/effective/five/Fruit;"
       9: areturn

  public static zy.service.effective.five.Fruit valueOf(java.lang.String);
    Code:
       0: ldc           #4                  // class zy/service/effective/five/Fruit
       2: aload_0
       3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;

       6: checkcast     #4                  // class zy/service/effective/five/Fruit
       9: areturn
 

枚舉類型保證了編譯時的類型安全。

有一種好的方法可以將不同的行爲與每個枚舉常量關聯起來。在枚舉類中聲明一個抽象方法。在枚舉實例中重寫這個方法。改成下面這樣。你也可以不用抽象abstract 就可以不必須重寫。默認爲枚舉類中的方法體。

BANANA{
    @Override
    public double plusPrice(double a, double b) {
        return 0;
    }
},apple{};

public double plusPrice(double a,double b){
    return a+b;
}

枚舉可以嵌套枚舉來實現需求。例如算工時,週末算加班。

public enum PayrollDay {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
    SATURDAY(Paytype.WEEKEND), SUNDAY(Paytype.WEEKEND);
    private final Paytype paytype;

    PayrollDay(Paytype paytype) { //傳週末的入口。
        this.paytype = paytype;
    }

    PayrollDay() {   //默認是工作日   不加空參構造就得在實例上都添加paytype的類型。
        this(Paytype.WEEKDAY);
    }
    int pay(int mins,int payRate){     //調用計算好的對應的pay方法的值。
        return paytype.pay(mins, payRate);
    }

    private enum Paytype {    //區分週末計算方法和工作日的計算方法的枚舉。最終使用pay方法返回值    策略枚舉
        WEEKDAY {
            @Override
            int overtimePay(int mins, int payRate) {
                return 0;
            }
        }, WEEKEND {
            @Override
            int overtimePay(int mins, int payRate) {
                return 0;
            }
        };

        abstract int overtimePay(int mins, int payRate);

        int pay(int mins, int payRate) {    
            int basePay = mins * payRate;
            return basePay + overtimePay(mins, payRate);
        }
    }
}

枚舉中的switch語句適合於給外部的枚舉類型增加特定於常量的行爲。

public static Paytype inverse(PayrollDay payrollDay){
    switch (payrollDay){
        case MONDAY:return MONDAY.paytype;
        case ......
        default:throw new AssertionError("unknown payrollDay " +payrollDay);
    }
}

枚舉使用場景:每當需要一組固定常量,並且在編譯時就知道其成員的時候,就應該使用枚舉。枚舉類型中的常量集並不一定要始終保持不變。

35.用實例域代替序數

避免使用ordinal() 這個方法我們知道是用來返回枚舉實例下標的。但是要是順序一換就涼了。

永遠不要根據枚舉的序數導出與它關聯的值,而是要將它保存在一個實例域中。

public enum Week {
    MONDAY(1),FRIDAY(5);
    private int number;
    Week(int number){
        this.number = number;
    }
    public int getNumber(){
        return number;
    }
}

36.用EnumSet代替位域

用OR位運算將幾個常量合併到一個集合中,稱作位域。

例如  2|4  == 6  位域有很多缺點。 因爲要翻譯成二進制再計算就很煩。還要預算位數來選擇存儲類型。

EnumSet類有效地表示從單個枚舉類型中提取的多個值的多個集合。  使用位向量來編寫的。

正是因爲枚舉類型要用在集合中,所以沒有理由用位域來表示它。EnumSet內部用位操作,所有性能絕對夠用。

public static void enumSetTest(Set<PayrollDay> days){
    days.forEach(s-> System.out.println(s.name()));
}
public static void main(String[] args) {
    enumSetTest(EnumSet.of(PayrollDay.MONDAY,PayrollDay.FRIDAY));
}

可以看到EnumSet可以工廠構造。只需傳入相同泛型的元素即可。因爲他是個抽象類不能new

EnumSet集位域的簡潔和性能優勢及枚舉類型的所有優點於一身。

但是EnumSet有一個缺點,無法創建不可變的EnumSet 可以用Collections.unmodifiableSet將EnumSet封裝起來。

這裏有一個EnumSet的例子:https://www.cnblogs.com/swiftma/p/6044718.html

例子講的大概是:統計 哪一天沒有一個人上班,哪一天只有一個人上班。通過 Day周1-7枚舉加上勞動者工作具體信息使用EnumSet進行篩選。這一應用場景。

37.用EnumMap代替序數索引

最好不要用序數也就是 ordinal 來當作枚舉數組的索引。  下面時EnumMap的幾個實例變量。

private final Class<K> keyType;
private transient K[] keyUniverse;
private transient Object[] vals;
private transient int size = 0;

舉個用到EnumMap的例子,用來統計每個瓶子的規格的數量。

@Getter
@Setter
@AllArgsConstructor
public class Bottle{
    private double price;
    private Type type;
    enum Type {
        SMALL,MID,BIG;   //規格枚舉
    }
    public static Map<Type,Integer> getBottleNum(List<Bottle> bottles){ //計算規格的方法,相當於分組
        final Map<Type,Integer> map = new EnumMap<>(Type.class);
        bottles.forEach(bottle -> {
            Type type = bottle.getType();   //當前杯子的規格
            Integer num = map.get(type);    //當前規格的數量
            if(Objects.nonNull(num)){
                map.put(type, num+1);
            }else{
                map.put(type, 1);
            }
        });
        return map;
    }
    public static void main(String[] args) {
        Bottle bottle = new Bottle(12,Type.SMALL);   //測試數據
        Bottle bottle1 = new Bottle(13,Type.SMALL);
        Bottle bottle2 = new Bottle(14,Type.BIG);
        List<Bottle> list = new ArrayList<>();
        list.add(bottle);
        list.add(bottle1);
        list.add(bottle2);
        Map<Type, Integer> map = getBottleNum(list);
        System.out.println(map);                     //按照枚舉定義的順序輸出。{SMALL=2, BIG=1}
    } 
}

如上用stream + lambda 按照 Type 分組也可以做到。只不過 value存儲的是一個集合,而不是一個Integer

EnumMap內部有兩個數組,長度相同,一個表示所有可能的鍵,一個表示對應的值,值爲null表示沒有該鍵值對,鍵都有一個對應的索引,根據索引可直接訪問和操作其鍵和值,效率很高。 

38.用接口模擬可擴展的枚舉

由於接口不能擴展,所以使用接口來擴展枚舉就成了解決方式。

雖然無法編寫可擴展的枚舉類型,卻可以通過編寫接口以及實現該接口的基礎枚舉類型來對它進行模擬。

public enum ExtendEnum implements ExtendInterface{
    ONE("first"){
        @Override
        public void say() {
            System.out.println(1);
        }
    },TWO("second"),THREE("third");
    private String name;
    ExtendEnum(String name){
        this.name = name;
    }
    @Override
    public void say() {   //這裏重寫則實例可以默認成這個方法,或者直接實例重寫。like ONE
        
    }
}

39.註解優先於命名模式

命名模式就是一個規則,比如以test開頭的函數可以作爲Junit單元測試 。可想而知很不方便,而且易出錯。總之不要這麼規則一個東西。

而註解則是靈活的。比如你向單元測試。方法上面加個@Test註解來完成。

 

關於註解我記載在這篇博客上了:https://blog.csdn.net/finalheart/article/details/86552207

 

40.堅持使用Override註解

在你想要覆蓋超類聲明的每個方法聲明中使用Override註解。

例如  equals方法。注意它的參數類型是Object 如果不是Object而是別的的話就是重載而不是重寫了。

public boolean equals(Object obj) {}

而如果你知道這個就是在重寫父類,那麼就添加上@Override註解。這樣編輯器會幫助你檢查這個錯誤。

很不錯的建議。

41.用標記接口定義類型

標記接口是不包含方法聲明的接口,它只是指明一個類實現了具有某種屬性的接口。例如serializable等。

標記接口定義的類型是由被標記類的實例實現的;標記註解則沒有定義這樣的類型。

標記接口勝過標記註解的另一個優點是,他們可以被更加精確的進行鎖定。

標記註解勝過標記接口的最大優點在於,他們是更大的註解機制的一部分。

如果你發現自己在編寫的是目標爲ElementType.TYPE的標記註解類型,就要花點時間考慮清楚,它是否真的應該爲註解類型,想想標記接口是否會更加合適。

對類和接口的標記用標記接口,對其他的可以使用標記註解。

 

 

參考:https://www.cnblogs.com/swiftma/p/6044672.html

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章