Java 1.5發行版本新增了兩個引用類型家族:枚舉類型(Enumerate類)和注解類型(Annotation接口)。
第三十條、用enum代替int常量
-
枚舉類型是指由一組固定的常量組成合法值的類型。替代之前的具名的int常量。
public static final int APPLE_FUJI = 0;
這種方式稱作int枚舉模式,存在諸多的不足:
程序十分脆弱,因為int枚舉是編譯時常量,被編譯到使用它們的客戶端中。如果與枚舉常量關聯的int發生了變化,客戶端就必須重新編譯,否則,程序行為會變得不確定性。(還有個變體是String枚舉模式) -
java的枚舉類型是功能十分齊全的類,本質上是int值。
基本想法是:通過公有的靜態final域為每個枚舉常量導出實例的類。枚舉類型是實例受控的,它們是單例的泛型化,本質上是單元素的枚舉。枚舉提供了編譯時的類型安全,如果聲明一個參數的類型為Apple,就可以保證,被傳到該參數上的任何非null的對象引用一定屬于三個有效的Apple值之一。試圖傳遞類型錯誤的值時,會導致編譯時錯誤。
public enum Apple{FUJI,PIPPIN,GERNNY_SMITH}
包含同名變量的多個枚舉類型可以在一個系統中和平共處,因為每個類型都有自己的命名空間。 -
除了彌補int枚舉常量的不足,枚舉類型還允許添加任意的方法和域(近似于類),并實現任意的接口。它們提供了所有Object方法的高級實現,實現了Comparable和Serializable接口。我們為啥要將方法或者域加入到枚舉類型中?將數據與它的常量關聯起來。可以用任何適當的方法來增強枚舉類型。
/** * Created by laneruan on 2017/7/10. * 一個枚舉類型的例子 * 為了將數據與枚舉常量關聯起來,得聲明實例域,并編寫一個帶有數據并將數據保存在域中的構造器。 */ public class EnumPlanet { public enum Planet{ MERCURY(3.302e+23,2.439e6), VENUS(4.8693+24,6.052e6), EARTH(5.975e+24,6.378e6), MARS(6.419e+23,3.393e6), JUPITER(1.899e+27,7.149e7), SATURN(5.685e+26,6.027e7), URANUS(8.683e+25,2.556e7), NEPTUNE(1.024e+26,2.477e7); private final double mass; private final double radius; private final double surfaceGravity; private static final double G = 6.67300E-11; Planet(double mass,double radius){ this.mass = mass; this.radius = radius; surfaceGravity = G * mass/(radius * radius); } public double mass(){return mass;} public double radius(){return radius;} public double surfaceGravity(){return surfaceGravity;} public double surfaceWeight(double mass){ return mass * surfaceGravity; } } public static void main(String[] args){ double earthWeight = Double.parseDouble(args[0]); double mass = earthWeight/Planet.EARTH.surfaceGravity(); for (Planet p : Planet.values()){ System.out.println("Weight on " + p +" is "+p.surfaceWeight(mass)); } } }
與枚舉常量關聯的有些行為,可能只需要用在定義了枚舉的類或者包中,這種行為最好被實現成私有的或者包級私有的方法。
如果一個枚舉具有普遍適用性,它就應該成為一個頂層類。如果它是被用在一個特定的頂層類中,它就應該成為該頂層類的一個成員類。 -
特定于常量的方法實現:將不同的行為與每個枚舉常量關聯起來。
public 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), }
什么時候應該使用枚舉類型?
每當需要一組固定常量的時候。包括:天然的枚舉類型;在編譯的時候就知道其所有可能值的集合。總結:與int常量相比,枚舉類型的優勢不言而喻:易讀,更加安全。功能更加強大。許多枚舉都不需要顯式的構造器或者成員,但許多其他枚舉類型則受益于“每個常量與屬性的關聯”以及“提供行為受這個屬性影響的方法”。
第三十一條、用實例域代替序數
許多枚舉天生就與一個單獨的int值相關聯,所有的枚舉都有一個ordinal方法,它返回每個枚舉常量在類型中的數字位置。可以試著從序數中得到關聯的int值。最好避免使用ordinal方法。
永遠不要根據枚舉的序數導出與它想關聯的值,而是要將它保存在一個實例域中:
public enum Ensemble{
SOLO(1),DUET(2),TRIO(3),QUARTET(4),QUINTET(5),
SETET(6),SEPET(7),OCTET(8),NONET(9),DECTET(10);
private final int numberOfMusicians;
Ensemble(int size){
this.numberOfMusicians = size;
}
public int numberOfMusicians(){return numberOfMusicians;}
// public int numberOfMusicians(){return ordinal()+1;}
}
第三十二條、用EnumSet代替位域
需要傳遞多組常量集時,java.util包提供了EnumSet類來有效地表示從單個枚舉類型中提取多個值的多個集合,這個類實現了Set接口,提供了豐富的功能。
class Text{
public enum Style {BOLD,ITALIC,UNDERLINE,STRIKETHROUGH}
public void applyStyles(Set<Style> styles){}
}
text.applyStyles(EnumSet.of(Style.BOLD,Style.ITALIC));
總結:正是因為枚舉類型要用在集合中,所以沒有理由用位域來表示他。
第三十三條、用EnumMap來代替序數索引
最好不要用序數來索引數組,而要使用EnumMap。
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* 假設有個香草的數組,表示一座花園中的植物,想要按照類型(一年生、兩年生、多年生)
* 進行組織之后將這些植物列出來。
* 注意的是:EnumMap采用了鍵類型的Class對象構造器:這是一個有限制的類型令牌,
* 提供了運行時的泛型信息。
*/
public class EnumMapHerb {
public enum Type{ANNUAL,PERENNIAL,BIENNIAL}
private final String name;
private final Type type;
EnumMapHerb(String name,Type type){
this.name = name;
this.type = type;
}
@Override
public String toString(){
return name;
}
public static void main(String[] args){
EnumMapHerb[] garden = null;
Map<Type,Set<EnumMapHerb>> herbByType =
new EnumMap<Type, Set<EnumMapHerb>>(EnumMapHerb.Type.class);
for(EnumMapHerb.Type t :EnumMapHerb.Type.values()){
herbByType.put(t,new HashSet<EnumMapHerb>());
}
for(EnumMapHerb h:garden){
herbByType.get(h.type).add(h);
}
}
}
第三十四條、用接口模擬可伸縮的枚舉
-
以操作碼opcode為例:它的元素表示在某種機器上的那些操作。有時候,要盡可能地讓API的用戶提供他們自己的操作,這樣可以有效地擴展API提供的操作集。可以利用枚舉類型來實現這種效果:
public interface Operation{ double apply(double x,double y); } public enum BasicOperation implements Operation{ PLUS("+"){ public double apply(double x,double y){ return x+y; } }, MINUS("-"){ public double apply(double x,double y){ return x-y; } }, TIMES("*"){ public double apply(double x,double y){ return x*y; } }, DIVIDE("/"){ 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; } }
雖然枚舉類型BasicOperaion不是可擴展的,但是接口類型是可擴展的,你可以定義另一個枚舉類型,它實現這個接口,并用這個新類型的實例代替基本類型。
public enum ExtendOperation implements Operation{ EXP("^"){ public double apply(double x,double y){ return Math.pow(x,y); } }, REMAINDER("%"){ public double apply(double x,double y){ return x % y; } }; private final String symbol; ExtendOperation(String symbol){ this.symbol = symbol; } @Override public String toString(){ return symbol; } }
以下是該類型的使用:
public static void main(String[] args){ double x = Double.parseDouble(args[0]); double y = Double.parseDouble(args[1]); test(ExtendOperation.class,x,y); } //很復雜的聲明確保了對象既表示枚舉又表示Operation的子類型 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)); } }
第二種使用方法是使用Collection<? extends Operation>,這是個有限制的通配符類型:
public static void main(String[] args){ double x = Double.parseDouble(args[0]); double y = Double.parseDouble(args[1]); test(Arrays.asList(ExtendOperation.values()),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)); } }
這樣得到的代碼不復雜且更靈活:允許操作者將多個實現類型的操作合并到一起。
- 總結:雖然無法編寫可擴展的枚舉類型,但可以通過編寫接口以及實現該接口的基礎枚舉類型,對它進行模擬。
第三十五條、注解優先于命名模式(Naming Pattern)
Java 1.5之前一般使用命名模式表明有些程序元素需要通過某種工具或者框架進行特殊處理。這種模式缺點很明顯:文字拼寫錯誤很容易發生且難以發覺;無法確保它們只用于相應的程序元素上;沒有提供將參數值與程序元素相關聯起來的好方法。
-
注解很好地解決了這些問題:
假設想要定義一個注解類型RunTests來指定簡單的測試,它們自動運行,并在拋出異常時失敗:import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) //表明注解應在運行時保留,元注解 @Target(ElementType.METHOD) //表明只有在方法中聲明才是合法的,元注解 public @interface RunTests { }
現實應用中的Test注解,稱作標記注解(marker annotation)。
總結:大多數程序員不需要定義注解類型,所有的程序員都應該使用Java平臺所提供的預定義的注解類型,還要考慮使用IDE或者靜態分析工具所提供的任何注解。
第三十六條、堅持使用@Override
注解
Override注解只能在方法聲明時使用,表示被注解的方法聲明覆蓋了超類中的一個聲明。堅持使用這一注解,可以防止一大類的非法錯誤。
現代的IDE提供了堅持使用
@Override
的另一種理由:IDE具有自動檢查功能,稱作代碼檢驗(code inspection),當有一個方法沒有@Override
注解卻覆蓋了超類方法時,IDE會產生一條警告提醒你警惕無意識的覆蓋。總結:如果你想要的每個方法聲明中使用Override注解來覆蓋超類聲明,編譯器就可以替你防止大量的錯誤。但有一個例外:在具體的類中,不必標注你確信覆蓋了抽象方法聲明的方法。
第三十七條、用標記接口定義類型
標記接口(Marker Interface)是沒有包含方法聲明的接口,只是指明(或者標明)一個類實現了具有某種屬性的接口。例:Serializable接口,通過實現這個接口,類表明它的實例可以被寫到ObjectOutputStream(即“被序列化”)
-
標記注解和標記接口:
- 標記接口的優點:
- 標記接口定義的類型是由被標記類的實例實現的;標記注解則沒有定義這樣的類型。這個類型允許你在編譯時捕捉在使用標記注解的情況要到運行時才能捕捉到的錯誤。
- 他們可以更加精確地進行鎖定。
- 標記注解的優點:
- 它可以通過默認的方式添加一個或者多個注解類型元素,給已被使用的注解類型添加更多的信息。隨著時間的遷移,簡單的標記注解可以演變成更加豐富的注解類型,這在標記接口中是不可能的;
- 它們是更大的注解機制的一部分。因此,標記注解在那些支持注解作為編程元素之一的框架中同樣具有一致性。
- 標記接口的優點:
-
何時使用標記注解和標記接口?
如果標記是應用到任何程序元素而不是類或者接口,就必須使用注解,因為只有類和接可以用來實現或者擴展接口。如果標記只應用在類和接口,思考下:我要編寫一個或者多個只接受有這種標記的方法嗎?如果是這樣,優先使用標記接口。如果不是,再思考下:是否要永遠限制這個標記只用于特殊接口的元素?如果是,則優先使用標記接口。 最后選擇使用標記注解。
總結:標記接口和標記注解都有用處,如何選擇見上。