Effective Java(五)

五、枚舉和註解

1. 用enum代替int常數

        枚舉類型是指由一組固定的常量組成合法值的類型,在編程語言沒有引入枚舉之前,表示枚舉類型的常用模式是聲明一組具名的int常量,每個類型成員一個常量:

public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;

        這種方法稱作int枚舉模式,它存在着諸多不足:

  • 類型安全性問題  可能會傳遞錯誤的值
  • 沒有自己的命名空間  一般只能通過前綴的形式區分
  • 採用int枚舉模式的程序十分脆弱  int枚舉是編譯時常量,被編譯到使用它們的客戶端中,如果枚舉常量值發生了變化,客戶端必須重新編譯纔行
  • 無法提供便利的方法打印信息  int枚舉的打印信息只是數字

        String枚舉模式是int枚舉模式的變體,雖然它可以提供可打印的字符串,但存在性能及書寫時的安全性問題。

        Java1.5開始,提供了枚舉類型

public enum Apple {
    FUJI,
    PIPPIN,
    GRANNY_SMITH
}

         枚舉類型不僅可以避免int枚舉模式和String枚舉模式的缺點,還可以提供許多額外的好處:

(1)提供編譯時的類型安全
        如果聲明一個參數的類型爲枚舉類型Apple,就可以保證,被傳遞到該參數上的任何非null的對象引用一定屬於三個有效的Apple之一。試圖傳遞類型錯誤的值時,會導致編譯錯誤。
(2)每個枚舉類型都有自己的命名空間
        枚舉類是獨立的類型,有自己的命名空間,可以增加或者重新排列枚舉類型中的常量。
(3)可提供便利的打印信息
        通過toString(),可以將枚舉轉換成可打印的字符串。
(4)允許添加任意的方法和域,並實現任意的接口
        枚舉是一種類型,可以擁有自己的方法和域,並實現接口。

        枚舉的缺點:
        裝載和初始化枚舉時會有空間和時間的成本

        在枚舉中添加域和方法的動機:

  • 想將數據與它的常量關聯起來
  • 添加方法增強枚舉類型功能

        如果一個枚舉具有普適性,就應該成爲一個頂層類;如果它只是被用在一個特定的頂層類中,就應該成爲該頂層類的一個成員類

        在枚舉類中添加方法時,這些方法是枚舉常量共有的,但有時每個常量都會關聯本質上完全不同的行爲,可以使用特定於常量的方法實現來完成。它的實現過程如下:

  • 在枚舉類型中聲明一個抽象的方法
  • 在特定常量的類主體中,用具體的方法實現抽象方法
enum Operation {
    PLUS {
        @Override
        double apply(double x, double y) {
            return x + y;
        }
    },
    
    MINUS {
        @Override
        double apply(double x, double y) {
            return x - y;
        }
    },
    
    TIMES {
        @Override
        double apply(double x, double y) {
            return x * y;
        }
    },
    
    DIVIDE {
        @Override
        double apply(double x, double y) {
            return x / y;
        }
    };

    abstract double apply(double x, double y);
}

2. 用實例域代替序數

        所有的枚舉都有一個ordinal方法,它返回每個枚舉常量在類型中的數組位序。
        依賴ordinal()返回的枚舉常量序數會使得代碼極難維護。因爲枚舉常量可能會進行重新排序,也可能會添加新的枚舉常量。

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

//不當的使用方式
public enum Ensemble {
    SOLO, DUET, TRIO, QUARTET, QUINTET,
    SEXTET, SEPTET, OCTET, NONET, DECTET;
    
    //依賴ordinal()返回與枚舉常量關聯的值
    public int numberOfMusicians() {
        return ordinal() + 1;
    }
}
//推薦的使用方式
public enum Ensemble {
    SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
    SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
    NONET(9), DECTET(10), TRIPLE_QUARTET(12);
    
    private final int numberOfMusicians;
    
    Ensemble(int size) {
        this.numberOfMusicians = size;
    }
    
    public int numberOfMusicians() {
        return numberOfMusicians;
    }
}

3. 用EnumSet代替位域

        如果一個枚舉類型的元素主要用在集合中,可能會使用int枚舉模式:

public class Text {
    public static final int STYLE_BOLD = 1 << 0;          //1
    public static final int STYLE_ITALIC = 1 << 1;        //2
    public static final int STYLE_UNDERLINE = 1 << 2;     //4
    public static final int STYLE_STRIKETHROUGH = 1 << 3; //8
    
    public void applyStyles(int styles) {
        ...
    }
}

        這種表示法讓你用or位運算符將幾個常量合併到一個集合中,這個集合稱作位域

text.applyStyles(STYLE_BOLD | STYLE_ITALIC);

        位域表示法也允許利用位操作,執行像交集、並集這樣的集合操作。但位域具有int枚舉常量所有的缺點,甚至更多。位域以數字形式打印時,翻譯位域比翻譯int枚舉常量要困難的多,遍歷位域表示的所有元素也相當不容易。

        Set是一種集合,只能向其中添加不重複的對象,enum也要求其成員都是唯一的,看起來也具有集合的行爲,但不能從enum中刪除/添加元素。Java1.5引入了EnumSet替代傳統的基於int枚舉類型的位域集合,它表示從單個枚舉類型中提取多個枚舉值的集合
        EnumSet是與enum類型一起使用的專用Set類型,EnumSet中的所有元素都必須來自同一個enum。

        使用EnumSet代替位域後的代碼更加簡短、更加清楚、更加安全:

public class Text {
    public enum Style {
        BOLD, ITALIC, UNDERLINE, STRIKETHROUGH
    }
    
    public void applyStyles(Set<Style> styles) {
        ...
    }
}
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

4. 用EnumMap代替序數索引

        有時候,會見到利用枚舉常量的序數作爲數組下標來索引數組的代碼,對應映射關係如下圖所示:

        Enum序數作爲數組索引,這種方法的確可行,但是隱藏着很多問題:

  • 數組不能與泛型兼容,使其使用受限
  • 數組不知道它的索引代表着什麼,需要手工標註
  • 錯誤的索引值會引發數組越界異常

        Java1.5版本引入了EnumMap類型,它是一種特殊的Map,它要求其中的key必須來自一個enum,使用enum實例作爲鍵在EnumMap中進行各種操作。EnumMap在運行速度方面可以與數組相媲美,它在內部實現中使用了數組,但是它對程序員隱藏了實現細節,它具有Map的豐富功能、類型安全,以及數組的快速訪問。映射關係如下圖:

        最好不要用序數來索引數組,而要使用EnumMap。
        應用程序的程序員在一般情況下都不使用Enum.ordinal()。 

public class Herb {
	public enum Type {
		ANNUAL, PERENNIAL, BIENNIAL
	}

	private final String name;
	private final Type type;

	Herb(String name, Type type) {
		this.name = name;
		this.type = type;
	}

	@Override
	public String toString() {
		return name;
	}

	public static void main(String[] args) {
		Herb[] garden = { new Herb("Basil", Type.ANNUAL),
				new Herb("Carroway", Type.BIENNIAL),
				new Herb("Dill", Type.ANNUAL),
				new Herb("Lavendar", Type.PERENNIAL),
				new Herb("Parsley", Type.BIENNIAL),
				new Herb("Rosemary", Type.PERENNIAL) };

		// Using an EnumMap to associate data with an enum - Page 162
		Map<Herb.Type, Set<Herb>> herbsByType = new EnumMap<Herb.Type, Set<Herb>>(
				Herb.Type.class);
		for (Herb.Type t : Herb.Type.values())
			herbsByType.put(t, new HashSet<Herb>());
		for (Herb h : garden)
			herbsByType.get(h.type).add(h);
		System.out.println(herbsByType);
	}
}

5. 用接口模擬可伸縮的枚舉

        枚舉類型不可擴展,但有時又需要枚舉類型具備可伸縮的特性,一種好的方法就是利用接口:

public interface Operation {
    double apply(double x, double y);
}
public enum BasicOperation implements Operation {
    PLUS("+")  {
        @Override
        public double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS("-")  {
        @Override
        public double apply(double x, double y) {
            return x - y;
        }
    },
    TIMES("*")  {
        @Override
        public double apply(double x, double y) {
            return x * y;
        }
    },
    DIVIDE("/")  {
        @Override
        public double apply(double x, double y) {
            return x / y;
        }
    };
    
    private final String symbol;
    BasicOperation(String symbol) {
        this.symbol = symbol;
    }

    @Override
    public String toString() {
        return symbol;
    }
}

 

public enum ExtendedOperation implements Operation {
    EXP("^") {
        @Override
        public double apply(double x, double y) {
            return Math.pow(x, y);
        }
    },
    REMAINDER("%") {
        @Override
        public double apply(double x, double y) {
            return x % y;
        }
    };
    
    private final String symbol;
    ExtendedOperation(String symbol) {
        this.symbol = symbol;
    }
    
    @Override
    public String toString() {
        return symbol;
    }
}

        只要API是被寫成採用接口類型(Operation)而非實現(BasicOperation),那麼在可以使用基礎操作的任何地方,都可以使用新的操作。


public static void main(String[] args) {
        double x = Double.parseDouble(args[0]);
        double y = Double.parseDouble(args[1]);
        //test(ExtendedOperation.class, x, y);  //方式一
        test(Arrays.asList(ExtendedOperation.values()), x, y);  //方式二
    }
    
    //方式一
    private static <T extends Enum<T> & Operation> void test(Class<T> opSet, double x, double y) {
        for (Operation op : opSet.getEnumConstants()) {
            System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
        }
    }

    //方式二
    private static void test(Collection<? extends Operation> opSet, double x, double y) {
        for (Operation op : opSet) {
            System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
        }
    }
}

6. 註解優先於命名模式

        Java1.5版本之前,一般使用命名模式表明有些程序元素需要通過某種工具或者框架進行特殊處理。例如,JUnit4之前原本要求測試方法要以test作爲開頭。這種方法可行,但有幾個很嚴重的缺點:

  • 文字拼寫錯誤會導致失敗,且沒有任何提示。
  • 無法確保它們只用於相應的程序元素上。如將某個類稱作testSafetyMechanisms,希望JUnit可以自動地測試它的所有方法,而不管類中的方法名字是什麼。雖然JUnit不會出錯,但也不會執行測試。
  • 沒有提供將參數值與程序元素關聯起來的好方法。

        註解很好地解決命名模式的所有問題,因此,Java1.5版本後,JUnit4使用註解代替命名模式,重新實現了整個測試框架,使之更加強大、易用。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface ExceptionTest {
    Class<? extends Exception>[] value();
}

class Sample {
    @ExceptionTest( { IndexOutOfBoundsException.class,
         NullPointerException.class})
    public static void doublyBad() {
        //List<String> list = new ArrayList<>();
        List<String> list = null;
        list.add(5,null);
    }
}

public class RunTest {
    public static void main(String[] args) throws Exception {
        int tests = 0;
        int passed = 0;
        Class<?> testClass = Class.forName("Sample");
        for(Method m : testClass.getDeclaredMethods()) {
            if(m.isAnnotationPresent(ExceptionTest.class)) {
                tests++;
                try{
                    m.invoke(null);
                }catch (InvocationTargetException ite) {
                    //Throwable exc = ite.getTargetException();
                    Throwable exc = ite.getCause();
                    Class<? extends Exception>[] excTypes 
                        = m.getAnnotation(ExceptionTest.class).value();
                    for(Class<? extends Exception> excType : excTypes) {
                        if(excType.isInstance(exc)) {
                            excType.newInstance().printStackTrace();
                        }
                    }
                }
            }
        }
    }
}

7. 堅持使用Override註解

        Override註解只能用在方法聲明中,它表示被註解的方法聲明覆蓋(重寫)了超類型中的一個方法聲明。堅持使用這個註解,可以防止一大類的非法錯誤。這類錯誤基本上都是由於不小心而造成的,使用Override註解後,編譯器會做自動檢查,可以避免這類無意識的錯誤。

        使用Override註解可以有效防止覆蓋方法時的錯誤。

        例如:想要在String中覆蓋equals方法

//這是方法重載,將產生編譯錯誤
@Override
public boolean equals(String obj) {
    ....
}

//覆蓋
@Override
public boolean equals(Object obj) {
    ....
}

8. 用標記接口定義類型

        標記接口是沒有包含方法聲明的接口,它只是標明一個類實現了具有某種屬性的接口。例如,通過實現Serializable接口,表明類的實例可以被序列化。

標記接口勝過標記註解的兩大優點:

  • 標記接口定義的類型是由被標記類的實例實現的,允許在編譯時發現標記接口的使用錯誤。
  • 標記接口可以被更加精確地進行鎖定,它可以用來標記某類特殊接口的實現。

標記註解勝過標記接口的兩大優點:

  • 它可以通過默認的方式添加一個或者多個註解類型元素,給已被使用的註解類型添加更多信息。
  • 它是更大的註解機制的一部分,在那些支持註解作爲編程元素的框架中具有一致性。

標記接口和標記註解的使用選擇:

  • 如果標記是用到程序元素而不是類或接口,要使用註解;
  • 如果標記只應用給類和接口,就該優先使用接口。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章