30、用enum代替int常量
枚舉類型是指由一組固定的常量組成合法值的類型。在java沒有引入枚舉類型前,表示枚舉類型的常用方法是聲明一組不同的int常量,每個類型成員一個常量,這種方法稱作int枚舉模式。采用int枚舉模式的程序是十分脆弱的,因為int值是編譯時常量,若與枚舉常量關(guān)聯(lián)的int發(fā)生變化,客戶端就必須重新編譯。
java枚舉類型背后的思想:通過公有的靜態(tài)final域為每個枚舉常量導(dǎo)出實例的類。因為沒有可以訪問的構(gòu)造器,枚舉類型是真正的final。客戶端既不能創(chuàng)建枚舉類型的實例,也不能對它進行擴展。枚舉類型是實例受控的,它們是單例的泛型化,本質(zhì)上是單元素的枚舉。枚舉提供了編譯時的類型安全。
包含同名常量的多個枚舉類型可以在一個系統(tǒng)中和平共處,因為每個類型都有自己的命名空間。可以增加或重新排列枚舉類型中的常量,而無需重新編譯客戶端代碼,因為常量值并沒有被編譯到客戶端代碼中。可以調(diào)用toString方法,將枚舉轉(zhuǎn)換成可打印的字符串。
枚舉類型允許添加任意的方法和域,并實現(xiàn)任意的接口。枚舉類型默認(rèn)繼承Enum類(其實現(xiàn)了Comparable、Serializable接口)。為了將數(shù)據(jù)與枚舉常量關(guān)聯(lián)起來,得聲明實例域,并編寫一個將數(shù)據(jù)保存到域中的構(gòu)造器。枚舉天生就是不可變的,所有的域都必須是final的。
例如:
public enum Planet {
//括號中數(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;
}
//測試
public static void main(String[] args) {
double earthWeight = 175;
double mass = earthWeight/Planet.EARTH.surfaceGravity();
for(Planet p : Planet.values()) {
//java的printf方法中換行用%n, C語言中用\n
System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
}
}
}
上面的方法對大多數(shù)枚舉類型來說足夠了,但有時你需要將本質(zhì)上不同的行為與每個常量關(guān)聯(lián)起來。這時通常需要在枚舉類型中聲明一個抽象的apply方法,并在特定于常量的類主題中實現(xiàn)這個方法。這個方法被稱作特定于常量的方法實現(xiàn)。例如:
public enum Operation {
PULS("+") {
double apply(double x, double y) { return x + y; } //必須實現(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));
}
}
枚舉類型中的抽象方法,在它的常量中必須被實現(xiàn)。除了編譯時常量之外,枚舉構(gòu)造器不可以訪問枚舉的靜態(tài)域,因為構(gòu)造器運行時,靜態(tài)域還沒被初始化。
特定于常量的方法,使得在枚舉常量中共享代碼變的更加困難。例如:根據(jù)給定的工人的基本工資(按小時算)和工作時間,用枚舉計算工人當(dāng)天的工作報酬。其中加班工資為平時的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));
}
}
上面這段代碼雖然十分簡潔,但是維護成本很高。每將一個元素添加到該枚舉中,就必須修改switch語句。可以使用策略枚舉來進行優(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)勢明顯。許多枚舉都不需要顯式的構(gòu)造器或成員。當(dāng)需要將不同的行為與每個常量關(guān)聯(lián)起來時,可使用特定于常量的方法。若多個枚舉常量同時共享相同的行為,考慮使用策略枚舉。
31、用實例域代替序數(shù)
所有的枚舉都有一個ordinal方法,它返回枚舉常量在類中的位置。若常量進行重排序,它們ordinal的返回值將發(fā)生變化。所以,永遠不要根據(jù)枚舉的序數(shù)導(dǎo)出與它關(guān)聯(lián)的值,而是要將它保存在一個實例域中。
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時寫道:它是用于像EnumSet和EnumMap這種基于枚舉的數(shù)據(jù)結(jié)構(gòu)的方法,平時最好不要使用它。
32、用EnumSet代替位域
若枚舉類型要用在集合中,可以使用EnumSet類。EnumSet類是專為枚舉類設(shè)計的集合類,EnumSet中的所有元素都必須是單個枚舉類型中的枚舉值。若元素個數(shù)小于64,整個EnumSet就用一個long來表示,所以它的性能比的上位域(通過位操作實現(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類集位域的簡潔和性能優(yōu)勢及枚舉的所有優(yōu)點與一身。y應(yīng)使用EnumSet代替位域操作。
33、用EnumMap代替序數(shù)索引
EnumMap是一種鍵值必須為枚舉類型的映射表。雖然使用其它的Map實現(xiàn)(如HashMap)也能完成枚舉類型實例到值的映射,但是使用EnumMap會更加高效。由于枚舉類型實例的數(shù)量相對固定并且有限,所以EnumMap使用數(shù)組來存放與枚舉類型對應(yīng)的值,這使得EnumMap的效率比其它的Map實現(xiàn)(如HashMap也能完成枚舉類型實例到值的映射)更高。
注意:EnumMap在內(nèi)部使用枚舉類型的ordinal()得到當(dāng)前實例的聲明次序,并使用這個次序維護枚舉類型實例對應(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方法)來索引數(shù)組,而要使用
EnumMap
。若所表示的關(guān)系是多維的,可以使用EnumMap<.., EnumMap<..>>
。一般情況下不要使用Enum.ordinal
。
34、用接口模擬可伸縮的枚舉
枚舉類型都默認(rèn)繼承自java.lang.Enum類。雖然無法編寫可擴展的枚舉類型,卻可以通過編寫接口以及實現(xiàn)該接口的基礎(chǔ)枚舉類型,以實現(xiàn)對程序的擴展。
例如:
import java.util.*;
//測試
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的
}
//基本操作,實現(xiàn)Operation接口
enum BasicOperation implements Operation{
PULS("+") {
//訪問權(quán)限必須為public,否則報錯
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; }
}
//擴展操作,實現(xiàn)Operation接口
enum ExtendedOperation implements Operation {
EXP("^") {
public double apply(double x, double y) { return Math.pow(x,y); } //必須實現(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對象既是枚舉類型又是Operation的子類型。
35、注解優(yōu)先于命名模式
java1.5之前,一般使用命名模式來對程序進行特殊處理。如,JUnit測試框架要求用test作為測試方法名稱的開頭。使用命名模式有幾個缺點:
- 文字拼寫錯誤會導(dǎo)致失敗,且沒有任何提示。如,test寫成tset
- 無法確保它們只用于相應(yīng)的程序元素上。如,變量名使用test開頭
- 沒有提供將參數(shù)值與程序元素關(guān)聯(lián)起來的方法
注解很好的解決了這些問題。關(guān)于注解的詳細(xì)用法請看 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 對象的 invoke 方法調(diào)用目標(biāo)對象的方法時, 若在目標(biāo)對象的方法內(nèi)部拋出異常, 會拋出 InvocationTargetException 異常, 該異常包裝了目標(biāo)對象的方法內(nèi)部拋出異常, 可以通過調(diào)用 InvocationTargetException 異常類的的 getTargetException() 方法得到原始的異常.
在編寫一個需要程序員給源文件添加信息的工具時,應(yīng)該定義一組適當(dāng)?shù)淖⒔猓皇鞘褂妹J健?/p>
36、堅持使用Override注解
@Override注解只能用在方法聲明中,它表示被注解的方法聲明覆蓋了超類型中的一個聲明。使用Override注解可以有效防止覆蓋方法時的錯誤。
例如:想要在String中覆蓋equals方法
//這是方法重載,將產(chǎn)生編譯錯誤
@Override
public boolean equals(String obj) {
....
}
//覆蓋
@Override
public boolean equals(Object obj) {
....
}
37、用標(biāo)記接口定義類型
標(biāo)記接口是指沒有任何屬性和方法的接口,它只用來表明類實現(xiàn)了某種屬性。如,Serializable接口,通過實現(xiàn)這個接口,類表明它的實例可以被寫到ObjectOutputStream。標(biāo)記注解是特殊類型的注解,其中不包含成員。標(biāo)記注解的唯一目的就是標(biāo)記聲明。
- 標(biāo)記接口的優(yōu)點:標(biāo)記接口允許你在編譯時捕捉在使用標(biāo)記注解的情況下要到運行時才能捕捉到的錯誤。
- 標(biāo)記注解的優(yōu)點:便于擴展,可以給已被使用的注解類型添加更多信息(元注解)。而接口實現(xiàn)后不可能再添加方法。
標(biāo)記接口與標(biāo)記注解如何選擇:
- 標(biāo)記接口和標(biāo)記注解都各有好處。若想要定義一個任何新方法都不會與之關(guān)聯(lián)的類型,就是用標(biāo)記接口。若想要標(biāo)記成員元素而非類和接口(方法或成員變量)或未來可能要給標(biāo)記添加更多信息,就使用標(biāo)記注解。