Java 1.5發(fā)行版本新增了兩個(gè)引用類型家族:枚舉類型(Enumerate類)和注解類型(Annotation接口)蛤铜。
第三十條蓬痒、用enum代替int常量
-
枚舉類型是指由一組固定的常量組成合法值的類型。替代之前的具名的int常量闯睹。
public static final int APPLE_FUJI = 0;
這種方式稱作int枚舉模式戏羽,存在諸多的不足:
程序十分脆弱,因?yàn)閕nt枚舉是編譯時(shí)常量楼吃,被編譯到使用它們的客戶端中始花。如果與枚舉常量關(guān)聯(lián)的int發(fā)生了變化,客戶端就必須重新編譯所刀,否則衙荐,程序行為會(huì)變得不確定性饺藤。(還有個(gè)變體是String枚舉模式) -
java的枚舉類型是功能十分齊全的類耻矮,本質(zhì)上是int值。
基本想法是:通過公有的靜態(tài)final域?yàn)槊總€(gè)枚舉常量導(dǎo)出實(shí)例的類曾撤。枚舉類型是實(shí)例受控的斩披,它們是單例的泛型化溜族,本質(zhì)上是單元素的枚舉讹俊。枚舉提供了編譯時(shí)的類型安全,如果聲明一個(gè)參數(shù)的類型為Apple煌抒,就可以保證仍劈,被傳到該參數(shù)上的任何非null的對(duì)象引用一定屬于三個(gè)有效的Apple值之一。試圖傳遞類型錯(cuò)誤的值時(shí)寡壮,會(huì)導(dǎo)致編譯時(shí)錯(cuò)誤贩疙。
public enum Apple{FUJI,PIPPIN,GERNNY_SMITH}
包含同名變量的多個(gè)枚舉類型可以在一個(gè)系統(tǒng)中和平共處,因?yàn)槊總€(gè)類型都有自己的命名空間况既。 -
除了彌補(bǔ)int枚舉常量的不足这溅,枚舉類型還允許添加任意的方法和域(近似于類),并實(shí)現(xiàn)任意的接口棒仍。它們提供了所有Object方法的高級(jí)實(shí)現(xiàn)悲靴,實(shí)現(xiàn)了Comparable和Serializable接口。我們?yōu)樯兑獙⒎椒ɑ蛘哂蚣尤氲矫杜e類型中莫其?將數(shù)據(jù)與它的常量關(guān)聯(lián)起來癞尚。可以用任何適當(dāng)?shù)姆椒▉碓鰪?qiáng)枚舉類型乱陡。
/** * Created by laneruan on 2017/7/10. * 一個(gè)枚舉類型的例子 * 為了將數(shù)據(jù)與枚舉常量關(guān)聯(lián)起來浇揩,得聲明實(shí)例域,并編寫一個(gè)帶有數(shù)據(jù)并將數(shù)據(jù)保存在域中的構(gòu)造器蛋褥。 */ 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)); } } }
與枚舉常量關(guān)聯(lián)的有些行為临燃,可能只需要用在定義了枚舉的類或者包中,這種行為最好被實(shí)現(xiàn)成私有的或者包級(jí)私有的方法烙心。
如果一個(gè)枚舉具有普遍適用性膜廊,它就應(yīng)該成為一個(gè)頂層類。如果它是被用在一個(gè)特定的頂層類中淫茵,它就應(yīng)該成為該頂層類的一個(gè)成員類爪瓜。 -
特定于常量的方法實(shí)現(xiàn):將不同的行為與每個(gè)枚舉常量關(guān)聯(lián)起來。
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), }
什么時(shí)候應(yīng)該使用枚舉類型匙瘪?
每當(dāng)需要一組固定常量的時(shí)候铆铆。包括:天然的枚舉類型;在編譯的時(shí)候就知道其所有可能值的集合丹喻。總結(jié):與int常量相比薄货,枚舉類型的優(yōu)勢(shì)不言而喻:易讀,更加安全碍论。功能更加強(qiáng)大谅猾。許多枚舉都不需要顯式的構(gòu)造器或者成員,但許多其他枚舉類型則受益于“每個(gè)常量與屬性的關(guān)聯(lián)”以及“提供行為受這個(gè)屬性影響的方法”。
第三十一條税娜、用實(shí)例域代替序數(shù)
許多枚舉天生就與一個(gè)單獨(dú)的int值相關(guān)聯(lián)坐搔,所有的枚舉都有一個(gè)ordinal方法,它返回每個(gè)枚舉常量在類型中的數(shù)字位置敬矩「判校可以試著從序數(shù)中得到關(guān)聯(lián)的int值。最好避免使用ordinal方法弧岳。
永遠(yuǎn)不要根據(jù)枚舉的序數(shù)導(dǎo)出與它想關(guān)聯(lián)的值凳忙,而是要將它保存在一個(gè)實(shí)例域中:
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代替位域
需要傳遞多組常量集時(shí)禽炬,java.util包提供了EnumSet類來有效地表示從單個(gè)枚舉類型中提取多個(gè)值的多個(gè)集合消略,這個(gè)類實(shí)現(xiàn)了Set接口,提供了豐富的功能瞎抛。
class Text{
public enum Style {BOLD,ITALIC,UNDERLINE,STRIKETHROUGH}
public void applyStyles(Set<Style> styles){}
}
text.applyStyles(EnumSet.of(Style.BOLD,Style.ITALIC));
總結(jié):正是因?yàn)槊杜e類型要用在集合中,所以沒有理由用位域來表示他却紧。
第三十三條桐臊、用EnumMap來代替序數(shù)索引
最好不要用序數(shù)來索引數(shù)組,而要使用EnumMap晓殊。
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* 假設(shè)有個(gè)香草的數(shù)組断凶,表示一座花園中的植物,想要按照類型(一年生巫俺、兩年生认烁、多年生)
* 進(jìn)行組織之后將這些植物列出來。
* 注意的是:EnumMap采用了鍵類型的Class對(duì)象構(gòu)造器:這是一個(gè)有限制的類型令牌介汹,
* 提供了運(yùn)行時(shí)的泛型信息却嗡。
*/
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為例:它的元素表示在某種機(jī)器上的那些操作嘹承。有時(shí)候窗价,要盡可能地讓API的用戶提供他們自己的操作,這樣可以有效地?cái)U(kuò)展API提供的操作集叹卷『掣郏可以利用枚舉類型來實(shí)現(xiàn)這種效果:
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不是可擴(kuò)展的,但是接口類型是可擴(kuò)展的骤竹,你可以定義另一個(gè)枚舉類型帝牡,它實(shí)現(xiàn)這個(gè)接口,并用這個(gè)新類型的實(shí)例代替基本類型蒙揣。
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); } //很復(fù)雜的聲明確保了對(duì)象既表示枚舉又表示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>靶溜,這是個(gè)有限制的通配符類型:
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)); } }
這樣得到的代碼不復(fù)雜且更靈活:允許操作者將多個(gè)實(shí)現(xiàn)類型的操作合并到一起。
- 總結(jié):雖然無法編寫可擴(kuò)展的枚舉類型鸣奔,但可以通過編寫接口以及實(shí)現(xiàn)該接口的基礎(chǔ)枚舉類型墨技,對(duì)它進(jìn)行模擬惩阶。
第三十五條、注解優(yōu)先于命名模式(Naming Pattern)
Java 1.5之前一般使用命名模式表明有些程序元素需要通過某種工具或者框架進(jìn)行特殊處理扣汪。這種模式缺點(diǎn)很明顯:文字拼寫錯(cuò)誤很容易發(fā)生且難以發(fā)覺断楷;無法確保它們只用于相應(yīng)的程序元素上;沒有提供將參數(shù)值與程序元素相關(guān)聯(lián)起來的好方法崭别。
-
注解很好地解決了這些問題:
假設(shè)想要定義一個(gè)注解類型RunTests來指定簡(jiǎn)單的測(cè)試冬筒,它們自動(dòng)運(yùn)行,并在拋出異常時(shí)失斆┲鳌:import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) //表明注解應(yīng)在運(yùn)行時(shí)保留舞痰,元注解 @Target(ElementType.METHOD) //表明只有在方法中聲明才是合法的,元注解 public @interface RunTests { }
現(xiàn)實(shí)應(yīng)用中的Test注解诀姚,稱作標(biāo)記注解(marker annotation)响牛。
總結(jié):大多數(shù)程序員不需要定義注解類型,所有的程序員都應(yīng)該使用Java平臺(tái)所提供的預(yù)定義的注解類型赫段,還要考慮使用IDE或者靜態(tài)分析工具所提供的任何注解呀打。
第三十六條、堅(jiān)持使用@Override
注解
Override注解只能在方法聲明時(shí)使用糯笙,表示被注解的方法聲明覆蓋了超類中的一個(gè)聲明贬丛。堅(jiān)持使用這一注解,可以防止一大類的非法錯(cuò)誤给涕。
現(xiàn)代的IDE提供了堅(jiān)持使用
@Override
的另一種理由:IDE具有自動(dòng)檢查功能豺憔,稱作代碼檢驗(yàn)(code inspection),當(dāng)有一個(gè)方法沒有@Override
注解卻覆蓋了超類方法時(shí)够庙,IDE會(huì)產(chǎn)生一條警告提醒你警惕無意識(shí)的覆蓋恭应。總結(jié):如果你想要的每個(gè)方法聲明中使用Override注解來覆蓋超類聲明,編譯器就可以替你防止大量的錯(cuò)誤首启。但有一個(gè)例外:在具體的類中暮屡,不必標(biāo)注你確信覆蓋了抽象方法聲明的方法。
第三十七條毅桃、用標(biāo)記接口定義類型
標(biāo)記接口(Marker Interface)是沒有包含方法聲明的接口褒纲,只是指明(或者標(biāo)明)一個(gè)類實(shí)現(xiàn)了具有某種屬性的接口。例:Serializable接口钥飞,通過實(shí)現(xiàn)這個(gè)接口莺掠,類表明它的實(shí)例可以被寫到ObjectOutputStream(即“被序列化”)
-
標(biāo)記注解和標(biāo)記接口:
- 標(biāo)記接口的優(yōu)點(diǎn):
- 標(biāo)記接口定義的類型是由被標(biāo)記類的實(shí)例實(shí)現(xiàn)的;標(biāo)記注解則沒有定義這樣的類型读宙。這個(gè)類型允許你在編譯時(shí)捕捉在使用標(biāo)記注解的情況要到運(yùn)行時(shí)才能捕捉到的錯(cuò)誤彻秆。
- 他們可以更加精確地進(jìn)行鎖定。
- 標(biāo)記注解的優(yōu)點(diǎn):
- 它可以通過默認(rèn)的方式添加一個(gè)或者多個(gè)注解類型元素,給已被使用的注解類型添加更多的信息唇兑。隨著時(shí)間的遷移酒朵,簡(jiǎn)單的標(biāo)記注解可以演變成更加豐富的注解類型,這在標(biāo)記接口中是不可能的;
- 它們是更大的注解機(jī)制的一部分扎附。因此蔫耽,標(biāo)記注解在那些支持注解作為編程元素之一的框架中同樣具有一致性。
- 標(biāo)記接口的優(yōu)點(diǎn):
-
何時(shí)使用標(biāo)記注解和標(biāo)記接口留夜?
如果標(biāo)記是應(yīng)用到任何程序元素而不是類或者接口匙铡,就必須使用注解,因?yàn)橹挥蓄惡徒涌梢杂脕韺?shí)現(xiàn)或者擴(kuò)展接口碍粥。如果標(biāo)記只應(yīng)用在類和接口鳖眼,思考下:我要編寫一個(gè)或者多個(gè)只接受有這種標(biāo)記的方法嗎?如果是這樣嚼摩,優(yōu)先使用標(biāo)記接口钦讳。如果不是,再思考下:是否要永遠(yuǎn)限制這個(gè)標(biāo)記只用于特殊接口的元素枕面?如果是蜂厅,則優(yōu)先使用標(biāo)記接口。 最后選擇使用標(biāo)記注解膊畴。
總結(jié):標(biāo)記接口和標(biāo)記注解都有用處,如何選擇見上病游。