30潘靖、用enum代替int常量
枚舉類型是指由一組固定的常量組成合法值的類型穿剖。在java沒有引入枚舉類型前,表示枚舉類型的常用方法是聲明一組不同的int常量卦溢,每個(gè)類型成員一個(gè)常量糊余,這種方法稱作int枚舉模式。采用int枚舉模式的程序是十分脆弱的单寂,因?yàn)閕nt值是編譯時(shí)常量贬芥,若與枚舉常量關(guān)聯(lián)的int發(fā)生變化,客戶端就必須重新編譯凄贩。
java枚舉類型背后的思想:通過(guò)公有的靜態(tài)final域?yàn)槊總€(gè)枚舉常量導(dǎo)出實(shí)例的類誓军。因?yàn)闆]有可以訪問(wèn)的構(gòu)造器,枚舉類型是真正的final疲扎£鞘保客戶端既不能創(chuàng)建枚舉類型的實(shí)例,也不能對(duì)它進(jìn)行擴(kuò)展椒丧。枚舉類型是實(shí)例受控的壹甥,它們是單例的泛型化,本質(zhì)上是單元素的枚舉壶熏。枚舉提供了編譯時(shí)的類型安全句柠。
包含同名常量的多個(gè)枚舉類型可以在一個(gè)系統(tǒng)中和平共處,因?yàn)槊總€(gè)類型都有自己的命名空間∷葜埃可以增加或重新排列枚舉類型中的常量精盅,而無(wú)需重新編譯客戶端代碼,因?yàn)槌A恐挡]有被編譯到客戶端代碼中谜酒√厩危可以調(diào)用toString方法,將枚舉轉(zhuǎn)換成可打印的字符串僻族。
枚舉類型允許添加任意的方法和域粘驰,并實(shí)現(xiàn)任意的接口。枚舉類型默認(rèn)繼承Enum類(其實(shí)現(xiàn)了Comparable述么、Serializable接口)蝌数。為了將數(shù)據(jù)與枚舉常量關(guān)聯(lián)起來(lái),得聲明實(shí)例域度秘,并編寫一個(gè)將數(shù)據(jù)保存到域中的構(gòu)造器顶伞。枚舉天生就是不可變的,所有的域都必須是final的剑梳。
例如:
public enum Planet {
//括號(hào)中數(shù)值為傳遞給構(gòu)造器的參數(shù)
MERCURY(3.302e+23, 2.439e6),
VENUS(4.869e+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; //質(zhì)量kg
private final double radius; //半徑
private final double surfaceGravity; //表面重力枝哄,final常量構(gòu)造器中必須初始化
private static final double G = 6.673E-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;
}
//測(cè)試
public static void main(String[] args) {
double earthWeight = 175;
double mass = earthWeight/Planet.EARTH.surfaceGravity();
for(Planet p : Planet.values()) {
//java的printf方法中換行用%n, C語(yǔ)言中用\n
System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
}
}
}
上面的方法對(duì)大多數(shù)枚舉類型來(lái)說(shuō)足夠了阻荒,但有時(shí)你需要將本質(zhì)上不同的行為與每個(gè)常量關(guān)聯(lián)起來(lái)。這時(shí)通常需要在枚舉類型中聲明一個(gè)抽象的apply方法众羡,并在特定于常量的類主題中實(shí)現(xiàn)這個(gè)方法侨赡。這個(gè)方法被稱作特定于常量的方法實(shí)現(xiàn)。例如:
public enum Operation {
PULS("+") {
double apply(double x, double y) { return x + y; } //必須實(shí)現(xiàn)
},
MINUS("-") {
double apply(double x, double y) { return x - y; }
},
TIMES("*") {
double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
double apply(double x, double y) { return x / y; }
};
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() { return symbol; }
abstract double apply(double x, double y);
public static void main(String[] args) {
double x = 2.0;
double y = 4.0;
for(Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x,y));
}
}
枚舉類型中的抽象方法粱侣,在它的常量中必須被實(shí)現(xiàn)羊壹。除了編譯時(shí)常量之外,枚舉構(gòu)造器不可以訪問(wèn)枚舉的靜態(tài)域齐婴,因?yàn)闃?gòu)造器運(yùn)行時(shí)油猫,靜態(tài)域還沒被初始化。
特定于常量的方法柠偶,使得在枚舉常量中共享代碼變的更加困難情妖。例如:根據(jù)給定的工人的基本工資(按小時(shí)算)和工作時(shí)間,用枚舉計(jì)算工人當(dāng)天的工作報(bào)酬诱担。其中加班工資為平時(shí)的1.5倍毡证。
public enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURADAY, FRIDAY, SATURDAY, SUNDAY;
private static final int HOURS_PER_SHIFT = 8;
double pay(double hoursWorked, double payRate) {
switch(this) {
case SATURDAY: case SUNDAY :
return hoursWorked*payRate*1.5;
default :
return hoursWorked - HOURS_PER_SHIFT > 0
?(hoursWorked*payRate*1.5 - 0.5*HOURS_PER_SHIFT*payRate)
: hoursWorked*payRate;
}
}
public static void main(String[] args) {
System.out.println(PayrollDay.MONDAY.pay(10,10));
System.out.println(PayrollDay.SUNDAY.pay(10,10));
}
}
上面這段代碼雖然十分簡(jiǎn)潔,但是維護(hù)成本很高蔫仙。每將一個(gè)元素添加到該枚舉中料睛,就必須修改switch語(yǔ)句。可以使用策略枚舉來(lái)進(jìn)行優(yōu)化恤煞,例如:
public enum PayrollDay {
MONDAY(PayType.WEEKDAY),
TUESDAY(PayType.WEEKDAY),
WEDNESDAY(PayType.WEEKDAY),
THURADAY(PayType.WEEKDAY),
FRIDAY(PayType.WEEKDAY),
SATURDAY(PayType.WEEKEND),
SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) {
this.payType = payType;
}
double pay(double hoursWorked, double payRate) {
return payType.pay(hoursWorked, payRate);
}
//私有嵌套的枚舉類
private enum PayType {
WEEKDAY {
double pay(double hoursWorked, double payRate) {
return hoursWorked - HOURS_PER_SHIFT > 0
?(hoursWorked*payRate*1.5 - 0.5*HOURS_PER_SHIFT*payRate)
: hoursWorked*payRate;
}
},
WEEKEND {
double pay(double hoursWorked, double payRate) {
return hoursWorked * payRate * 1.5;
}
};
private static final int HOURS_PER_SHIFT = 8;
abstract double pay(double hoursWorked, double payRate);
}
public static void main(String[] args) {
System.out.println(PayrollDay.MONDAY.pay(10,10));
System.out.println(PayrollDay.SUNDAY.pay(10,10));
}
}
總之屎勘,與int常量相比,枚舉類型優(yōu)勢(shì)明顯居扒。許多枚舉都不需要顯式的構(gòu)造器或成員概漱。當(dāng)需要將不同的行為與每個(gè)常量關(guān)聯(lián)起來(lái)時(shí),可使用特定于常量的方法苔货。若多個(gè)枚舉常量同時(shí)共享相同的行為犀概,考慮使用策略枚舉。
31夜惭、用實(shí)例域代替序數(shù)
所有的枚舉都有一個(gè)ordinal方法姻灶,它返回枚舉常量在類中的位置。若常量進(jìn)行重排序诈茧,它們ordinal的返回值將發(fā)生變化产喉。所以,永遠(yuǎn)不要根據(jù)枚舉的序數(shù)導(dǎo)出與它關(guān)聯(lián)的值敢会,而是要將它保存在一個(gè)實(shí)例域中曾沈。
public enum Planet {
MERCURY(1),
VENUS(2),
EARTH(3),
MARS(4),
JUPITER(5),
SATURN(6),
URANUS(7),
NEPTUNE(8);
private final int numOrd;
Planet(int numOrd) {this.numOrd = numOrd; }
public int numOrd(){ return numOrd; }
}
Enum規(guī)范中談到ordinal時(shí)寫道:它是用于像EnumSet和EnumMap這種基于枚舉的數(shù)據(jù)結(jié)構(gòu)的方法,平時(shí)最好不要使用它鸥昏。
32塞俱、用EnumSet代替位域
若枚舉類型要用在集合中,可以使用EnumSet類吏垮。EnumSet類是專為枚舉類設(shè)計(jì)的集合類障涯,EnumSet中的所有元素都必須是單個(gè)枚舉類型中的枚舉值。若元素個(gè)數(shù)小于64膳汪,整個(gè)EnumSet就用一個(gè)long來(lái)表示唯蝶,所以它的性能比的上位域(通過(guò)位操作實(shí)現(xiàn))的性能。
import java.util.*;
public class Text {
public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
// Any Set could be passed in, but EnumSet is clearly best
public void applyStyles(Set<Style> styles) {
// Body goes here
}
// Sample use
public static void main(String[] args) {
Text text = new Text();
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
}
}
EnumSet類集位域的簡(jiǎn)潔和性能優(yōu)勢(shì)及枚舉的所有優(yōu)點(diǎn)與一身遗嗽。y應(yīng)使用EnumSet代替位域操作粘我。
33、用EnumMap代替序數(shù)索引
EnumMap是一種鍵值必須為枚舉類型的映射表痹换。雖然使用其它的Map實(shí)現(xiàn)(如HashMap)也能完成枚舉類型實(shí)例到值的映射征字,但是使用EnumMap會(huì)更加高效。由于枚舉類型實(shí)例的數(shù)量相對(duì)固定并且有限晴音,所以EnumMap使用數(shù)組來(lái)存放與枚舉類型對(duì)應(yīng)的值柔纵,這使得EnumMap的效率比其它的Map實(shí)現(xiàn)(如HashMap也能完成枚舉類型實(shí)例到值的映射)更高。
注意:EnumMap在內(nèi)部使用枚舉類型的ordinal()得到當(dāng)前實(shí)例的聲明次序锤躁,并使用這個(gè)次序維護(hù)枚舉類型實(shí)例對(duì)應(yīng)值在數(shù)組中的位置搁料。
例如:
import java.util.*;
public class DatabaseInfo {
private enum DBType { MYSQL, ORACLE, SQLSERVER }
private static final EnumMap<DBType, String> urls
= new EnumMap<>(DBType.class);
static {
urls.put(DBType.MYSQL, "jdbc:mysql://localhost/mydb");
urls.put(DBType.ORACLE, "jdbc:oracle:thin:@localhost:1521:sample");
urls.put(DBType.SQLSERVER, "jdbc:microsoft:sqlserver://localhost:1433;DatabaseName=mydb");
}
private DatabaseInfo() {}
public static String getURL(DBType type) {
return urls.get(type);
}
public static void main(String[] args) {
System.out.println(DatabaseInfo.getURL(DBType.SQLSERVER));
System.out.println(DatabaseInfo.getURL(DBType.MYSQL));
}
}
不要用序數(shù)(ordinal方法)來(lái)索引數(shù)組或详,而要使用
EnumMap
。若所表示的關(guān)系是多維的郭计,可以使用EnumMap<.., EnumMap<..>>
霸琴。一般情況下不要使用Enum.ordinal
。
34昭伸、用接口模擬可伸縮的枚舉
枚舉類型都默認(rèn)繼承自java.lang.Enum類梧乘。雖然無(wú)法編寫可擴(kuò)展的枚舉類型,卻可以通過(guò)編寫接口以及實(shí)現(xiàn)該接口的基礎(chǔ)枚舉類型庐杨,以實(shí)現(xiàn)對(duì)程序的擴(kuò)展选调。
例如:
import java.util.*;
//測(cè)試
public class Test {
public 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));
}
}
public static void main(String[] args) {
double x = 2.0;
double y = 4.0;
test(ExtendedOperation.class, x, y);
test(BasicOperation.class, x, y);
}
}
//接口
interface Operation {
double apply(double x, double y); //默認(rèn)為public的
}
//基本操作,實(shí)現(xiàn)Operation接口
enum BasicOperation implements Operation{
PULS("+") {
//訪問(wèn)權(quán)限必須為public灵份,否則報(bào)錯(cuò)
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; }
}
//擴(kuò)展操作仁堪,實(shí)現(xiàn)Operation接口
enum ExtendedOperation implements Operation {
EXP("^") {
public double apply(double x, double y) { return Math.pow(x,y); } //必須實(shí)現(xiàn)
},
REMAINDER("%") {
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; }
}
其中<T extends Enum<T> & Operation>
確保了Class對(duì)象既是枚舉類型又是Operation的子類型。
35填渠、注解優(yōu)先于命名模式
java1.5之前弦聂,一般使用命名模式來(lái)對(duì)程序進(jìn)行特殊處理。如氛什,JUnit測(cè)試框架要求用test作為測(cè)試方法名稱的開頭莺葫。使用命名模式有幾個(gè)缺點(diǎn):
- 文字拼寫錯(cuò)誤會(huì)導(dǎo)致失敗,且沒有任何提示枪眉。如捺檬,test寫成tset
- 無(wú)法確保它們只用于相應(yīng)的程序元素上。如贸铜,變量名使用test開頭
- 沒有提供將參數(shù)值與程序元素關(guān)聯(lián)起來(lái)的方法
注解很好的解決了這些問(wèn)題欺冀。關(guān)于注解的詳細(xì)用法請(qǐng)看 java基礎(chǔ)(二),Annotation(注解)
例如:
import java.util.*;
import java.lang.reflect.*;
import java.lang.annotation.*;
@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();
}
}
}
}
}
}
}
在利用 Method 對(duì)象的 invoke 方法調(diào)用目標(biāo)對(duì)象的方法時(shí), 若在目標(biāo)對(duì)象的方法內(nèi)部拋出異常, 會(huì)拋出 InvocationTargetException 異常, 該異常包裝了目標(biāo)對(duì)象的方法內(nèi)部拋出異常, 可以通過(guò)調(diào)用 InvocationTargetException 異常類的的 getTargetException() 方法得到原始的異常.
在編寫一個(gè)需要程序員給源文件添加信息的工具時(shí)萨脑,應(yīng)該定義一組適當(dāng)?shù)淖⒔猓皇鞘褂妹J健?/p>
36饺饭、堅(jiān)持使用Override注解
@Override注解只能用在方法聲明中渤早,它表示被注解的方法聲明覆蓋了超類型中的一個(gè)聲明。使用Override注解可以有效防止覆蓋方法時(shí)的錯(cuò)誤瘫俊。
例如:想要在String中覆蓋equals方法
//這是方法重載鹊杖,將產(chǎn)生編譯錯(cuò)誤
@Override
public boolean equals(String obj) {
....
}
//覆蓋
@Override
public boolean equals(Object obj) {
....
}
37、用標(biāo)記接口定義類型
標(biāo)記接口是指沒有任何屬性和方法的接口扛芽,它只用來(lái)表明類實(shí)現(xiàn)了某種屬性骂蓖。如,Serializable接口川尖,通過(guò)實(shí)現(xiàn)這個(gè)接口登下,類表明它的實(shí)例可以被寫到ObjectOutputStream。標(biāo)記注解是特殊類型的注解,其中不包含成員被芳。標(biāo)記注解的唯一目的就是標(biāo)記聲明缰贝。
- 標(biāo)記接口的優(yōu)點(diǎn):標(biāo)記接口允許你在編譯時(shí)捕捉在使用標(biāo)記注解的情況下要到運(yùn)行時(shí)才能捕捉到的錯(cuò)誤。
- 標(biāo)記注解的優(yōu)點(diǎn):便于擴(kuò)展畔濒,可以給已被使用的注解類型添加更多信息(元注解)剩晴。而接口實(shí)現(xiàn)后不可能再添加方法。
標(biāo)記接口與標(biāo)記注解如何選擇:
- 標(biāo)記接口和標(biāo)記注解都各有好處侵状。若想要定義一個(gè)任何新方法都不會(huì)與之關(guān)聯(lián)的類型赞弥,就是用標(biāo)記接口。若想要標(biāo)記成員元素而非類和接口(方法或成員變量)或未來(lái)可能要給標(biāo)記添加更多信息趣兄,就使用標(biāo)記注解绽左。