?
三十、用enum代替int常量:
?? ?? 枚舉類型是指由一組固定的常量組成合法值的類型,該特征是在Java 1.5 中開始被支持的,之前的Java代碼都是通過“公有靜態常量域字段”的方法來簡單模擬枚舉的,如:
? ? ? public static final int APPLE_FUJI = 0;
? ? ? public static final int APPLE_PIPPIN = 1;
? ? ? public static final int APPLE_GRANNY_SMITH = 2;
?? ?? ... ...
?? ?? public static final int ORANGE_NAVEL = 0;
? ? ? public static final int ORANGE_TEMPLE = 1;
? ? ? public static final int ORANGE_BLOOD = 2;
? ? ? 這樣的寫法是比較脆弱的。首先是沒有提供相應的類型安全性,如兩個邏輯上不相關的常量值之間可以進行比較或運算(APPLE_FUJI - ORANGE_TEMPLE),再有就是常量int是編譯時常量,被直接編譯到使用他們的客戶端中。如果與該常量關聯的int發生了變化,客戶端就必須重新編譯。如果沒有重新編譯,程序還是可以執行,但是他們的行為將不確定。
? ? ? 下面我們來看一下Java 1.5 中提供的枚舉的聲明方式:
? ? ? public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
? ? ? public enum Orange { NAVEL, TEMPLE, BLOOD }
? ? ? 和“公有靜態常量域字段”不同的是,如果函數的參數是枚舉類型,如Apple,那么他的實際值只能來自于該枚舉所聲明的枚舉值,即FUJI, PIPPIN, GRANNY_SMITH。如果試圖將Apple和Orange中的枚舉值進行比較,將會導致編譯錯誤。
? ? ? 和C/C++中提供的枚舉不同的是,Java中允許在枚舉中添加任意的方法和域,并實現任意的接口。下面先給出一個帶有域方法和域字段的枚舉聲明:
1 public enum Planet { 2 MERCURY(3.302e+23,2.439e6), 3 VENUS(4.869e+24,6.052e6), 4 EARTH(5.975e+24,6.378e6), 5 MARS(6.419e+23,3.393e6), 6 JUPITER(1.899e+27,7.149e7), 7 SATURN(5.685e+26,6.027e7), 8 URANUS(8.683e+25,2.556e7), 9 NEPTUNE(1.024e+26,2.477e7); 10 private final double mass; // 千克 11 private final double radius; // 米 12 private final double surfaceGravity; 13 private static final double G = 6.67300E-11; 14 Planet( double mass, double radius) { 15 this .mass = mass; 16 this .radius = radius; 17 surfaceGravity = G * mass / (radius * radius); 18 } 19 public double mass() { 20 return mass; 21 } 22 public double radius() { 23 return radius; 24 } 25 public double surfaceGravity() { 26 return surfaceGravity; 27 } 28 public double surfaceWeight( double mass) { 29 return mass * surfaceGravity; 30 } 31 }
? ? ? 在上面的枚舉示例代碼中,已經將數據和枚舉常量關聯起來了,因此需要聲明實例域字段,同時編寫一個帶有數據并將數據保存在域中的構造器。枚舉天生就是不可變的,因此所有的域字段都應該為final的。下面看一下該枚舉的應用示例:
1 public class WeightTable { 2 public static void main(String[] args) { 3 double earthWeight = Double.parseDouble(args[0]); 4 double mass = earthWeight/Planet.EARTH.surfaceGravity(); 5 for (Planet p : Planet.values()) 6 System.out.printf("Weight on %s is %f%n",p,p.surfaceWeight(mass)); 7 } 8 } 9 // Weight on MERCURY is 66.133672 10 // Weight on VENUS is 158.383926 11 // Weight on EARTH is 175.000000 12 // Weight on MARS is 66.430699 13 // Weight on JUPITER is 442.693902 14 // Weight on SATURN is 186.464970 15 // Weight on URANUS is 158.349709 16 // Weight on NEPTUNE is 198.846116
?? ?? 枚舉的靜態方法values()將按照聲明順序返回他的值數組。枚舉的toString方法返回每個枚舉值的聲明名稱。
? ? ? 在實際的編程中,我們常常需要針對不同的枚舉常量提供不同的數據操作行為,見如下代碼:
1 public enum Operation { 2 PLUS,MINUS,TIMES,DIVIDE; 3 double apply( double x, double y) { 4 switch ( this ) { 5 case PLUS: return x + y; 6 case MINUS: return x - y; 7 case TIMES: return x * y; 8 case DIVIDE: return x / y; 9 } 10 throw new AssertionError("Unknown op: " + this ); 11 } 12 }
?? ?? 上面的代碼已經表達出這種根據不同的枚舉值,執行不同的操作。但是上面的代碼在設計方面確實存在一定的缺陷,或者說漏洞,如果我們新增枚舉值的時候,所有和apply類似的域函數,都需要進行相應的修改,如有遺漏將會導致異常的拋出。幸運的是,Java的枚舉提供了一種更好的方法可以將不同的行為與每個枚舉常量關聯起來:在枚舉類型中聲明一個抽象的apply方法,并在特定于常量的類主體中,用具體的方法覆蓋每個常量的抽象apply方法,如:
1 public enum Operation { 2 PLUS { double apply( double x, double y) { return x + y;} }, 3 MINUS { double apply( double x, double y) { return x - y;} }, 4 TIMES { double apply( double x, double y) { return x * y;} }, 5 DIVIDE { double apply( double x, double y) { return x / y;} }; 6 abstract double apply( double x, double y); 7 }
? ? ? 這樣在添加新枚舉常量時就不會輕易忘記提供相應的apply方法了。我們在進一步看一下如何將枚舉常量和特定的數據進行關聯,見如下代碼:
1 public enum Operation { 2 PLUS("+") { double apply( double x, double y) { return x + y;} }, 3 MINUS("-") { double apply( double x, double y) { return x - y;} }, 4 TIMES("*") { double apply( double x, double y) { return x * y;} }, 5 DIVIDE("/") { double apply( double x, double y) { return x / y;} }; 6 private final String symbol; 7 Operation(String symbol) { 8 this .symbol = symbol; 9 } 10 @Override public String toString() { 11 return symbol; 12 } 13 abstract double apply( double x, double y); 14 }
? ? ? 下面給出以上代碼的應用示例:
1 public static void main(String[] args) { 2 double x = Double.parseDouble(args[0]); 3 double y = Double.parseDouble(args[1]); 4 for (Operation op : Operation.values()) 5 System.out.printf("%f %s %f = %f%n",x,op,y,op.apply(x,y)); 6 } 7 } 8 // 2.000000 + 4.000000 = 6.000000 9 // 2.000000 - 4.000000 = -2.000000 10 // 2.000000 * 4.000000 = 8.000000 11 // 2.000000 / 4.000000 = 0.500000
?? ?? 沒有類型有一個自動產生的valueOf(String)方法,他將常量的名字轉變為枚舉常量本身,如果在枚舉中覆蓋了toString方法(如上例),就需要考慮編寫一個fromString方法,將定制的字符串表示法變回相應的枚舉,見如下代碼:
1 public enum Operation { 2 PLUS("+") { double apply( double x, double y) { return x + y;} }, 3 MINUS("-") { double apply( double x, double y) { return x - y;} }, 4 TIMES("*") { double apply( double x, double y) { return x * y;} }, 5 DIVIDE("/") { double apply( double x, double y) { return x / y;} }; 6 private final String symbol; 7 Operation(String symbol) { 8 this .symbol = symbol; 9 } 10 @Override public String toString() { 11 return symbol; 12 } 13 abstract double apply( double x, double y); 14 // 新增代碼 15 private static final Map<String,Operation> stringToEnum = new HashMap<String,Operation>(); 16 static { 17 for (Operation op : values()) 18 stringToEnum.put(op.toString(),op); 19 } 20 public static Operation fromString(String symbol) { 21 return stringToEnum.get(symbol); 22 } 23 }
? ? ? 需要注意的是,我們無法在枚舉常量構造的時候將自身放入到Map中,這樣會導致編譯錯誤。與此同時,枚舉構造器不可以訪問枚舉的靜態域,除了編譯時的常量域之外。
?? ?
三十一、用實例域代替序數:
?? ?? Java中的枚舉提供了ordinal()方法,他返回每個枚舉常量在類型中的數字位置,如:
1 public enum Color { 2 WHITE,RED,GREEN,BLUE,ORANGE,BLACK; 3 public int indexOfColor() { 4 return ordinal() + 1; 5 } 6 }
? ? ? 上面的枚舉中提供了一個獲取顏色索引的方法(indexOfColor),該方法將返回顏色值在枚舉類型中的聲明位置,如果我們的外部程序依賴了該順序值,那么這將會是非常危險和脆弱的,因為一旦這些枚舉值的位置出現變化,或者在已有枚舉值的中間加入新的枚舉值時,都將導致該索引值的變化。該條目推薦使用實例域的方式來代替枚舉提供的序數值,見如下修改后的代碼:
1 public enum Color { 2 WHITE(1),RED(2),GREEN(3),ORANGE(4),BLACK(5); 3 private final int indexOfColor; 4 Color( int index) { 5 this .indexOfColor = index; 6 } 7 public int indexOfColor() { 8 return indexOfColor; 9 } 10 }
? ? ? Enum規范中談到ordinal時這么寫道:“大多數程序員都不需要這個方法。它是設計成用于像EnumSet和EnumMap這種基于枚舉的通用數據結構的。”除非你在編寫的是這種數據結構,否則最好避免使用ordinal()方法。
?? ?
三十二、用EnumSet代替位域:
? ? ? 下面的代碼給出了位域的實現方式:
1 public class Text { 2 public static final int STYLE_BOLD = 1 << 0; 3 public static final int STYLE_ITALIC = 1 << 1; 4 public static final int STYLE_UNDERLINE = 1 << 2; 5 public static final int STYLE_STRIKETHROUGH = 1 << 3; 6 public void applyStyles( int styles) { ... } 7 }
? ? ? 這種表示法讓你用OR位運算將幾個常量合并到一個集合中,使用方式如下:
? ? ? text.applyStyles(Text.STYLE_BOLD | Text.STYLE_ITALIC);
? ? ? Java中提供了EnumSet類,該類繼承自Set接口,同時也提供了豐富的功能,類型安全性,以及可以從任何其他Set實現中得到的互用性。但是在內部具體實現上,沒有EnumSet內容都表示為位矢量。如果底層的枚舉類型有64個或者更少的元素,整個EnumSet就用單個long來表示,因此他的性能也是可以比肩位域的。與此同時,他提供了大量的操作方法,其實現也是基于位操作的,但是相比于手工位操作,由于EnumSet替我們承擔了這部分的開發,從而也避免了一些容易出現的低級錯誤,代碼的美觀程度也會有所提升,見如下修改的代碼:
1 public class Text { 2 public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH } 3 public void applyStyles(Set<Style> styles) { ... } 4 }
? ? ? 新的使用方式如下:
? ? ? text.applyStyles(EnumSet.of(Style.BOLD,Style.ITALIC));
? ? ? 需要說明的是,EnumSet提供了豐富的靜態工廠來輕松創建集合。
?
三十三、用EnumMap代替序數索引:
?? ?? 前面的條目已經給出了盡量不要直接使用枚舉的ordinal()方法的原因,這里就不在做過多的贅述了。在這個條目中,只是再一次給出了ordinal()的典型用法,與此同時也再一次提供了一個更為合理的解決方案用于替換ordinal()方法,從而進一步證明我們在編碼過程中應該盡可能減少對枚舉中ordinal()函數的依賴。見如下代碼:
1 public class Herb { 2 public enum Type { ANNUAL, PERENNIAL, BIENNIAL } 3 private final String name; 4 private final Type type; 5 Herb(String name, Type type) { 6 this .name = name; 7 this .type = type; 8 } 9 @Override public String toString() { 10 return name; 11 } 12 } 13 public static void main(String[] args) { 14 Herb[] garden = getAllHerbsFromGarden(); 15 Set<Herb> herbsByType = (Set<Herb>[]) new Set[Herb.Type.values().length]; 16 for ( int i = 0; i < herbsByType.length; ++i) { 17 herbsByType[i] = new HashSet<Herb>(); 18 } 19 for (Herb h : garden) { 20 herbsByType[h.type.ordinal()].add(h); 21 } 22 for ( int i = 0; i < herbsByType.length; ++i) { 23 System.out.printf("%s: %s%n",Herb.Type.values()[i],herbByType[i]); 24 } 25 }
?? ?? 這里我需要簡單描述一下上面代碼的應用場景:在一個花園里面有很多的植物,它們被分成3類,分別是一年生(ANNUAL)、多年生(PERENNIAL)和兩年生(BIENNIAL),正好對應著Herb.Type中的枚舉值。現在我們需要做的是遍歷花園中的每一個植物,并將這些植物分為3類,最后再將分類后的植物分類打印出來。下面將提供另外一種方法,即通過EnumMap來實現和上面代碼相同的邏輯:
1 public static void main(String[] args) { 2 Herb[] garden = getAllHerbsFromGarden(); 3 Map<Herb.Type,Set<Herb>> herbsByType = 4 new EnumMap<Herb.Type,Set<Herb>>(Herb.Type. class ); 5 for (Herb.Type t : Herb.Type.values()) { 6 herbssByType.put(t, new HashSet<Herb>()); 7 } 8 for (Herb h : garden) { 9 herbsByType.get(h.type).add(h); 10 } 11 System.out.println(herbsByType); 12 }
?? ?? 和之前的代碼相比,這段代碼更加清晰,也更加安全,運行效率方面也是可以與使用ordinal()的方式想媲美的。
三十四、用接口模擬可伸縮的枚舉:
?? ?? 枚舉是無法被擴展(extends)的,這是一個無法回避的事實。如果我們的操作中存在一些基礎操作,如計算器中的基本運算類型(加減乘除)。然而對于有些用戶來講,他們也可以使用更高級的操作,如求冪和求余等。針對這樣的需求,該條目提出了一種非常巧妙的設計方案,即利用枚舉可以實現接口這一事實,我們將API的參數定義為該接口,而不是具體的枚舉類型,見如下代碼:
1 public interface Operation { 2 double apply( double x, double y); 3 } 4 public enum BasicOperation implements Operation { 5 PLUS("+") { 6 public double apply( double x, double y) { return x + y; } 7 }, 8 MINUS("-") { 9 public double apply( double x, double y) { return x - y; } 10 }, 11 TIMES("*") { 12 public double apply( double x, double y) { return x * y; } 13 }, 14 DIVIDE("/") { 15 public double apply( double x, double y) { return x / y; } 16 }; 17 private final String symbol; 18 BasicOperation(String symbol) { 19 this .symbol = symbol; 20 } 21 @Override public String toString() { 22 return symbol; 23 } 24 } 25 public enum ExtendedOperation implements Operation { 26 EXP("^") { 27 public double apply( double x, double y) { 28 return Math.pow(x,y); 29 } 30 }, 31 REMAINDER("%") { 32 public double apply( double x, double y) { 33 return x % y; 34 } 35 }; 36 private final String symbol; 37 ExtendedOperation(String symbol) { 38 this .symbol = symbol; 39 } 40 @Override public String toString() { 41 return symbol; 42 } 43 }
????? 通過以上的代碼可以看出,在任何可以使用BasicOperation的地方,我們也同樣可以使用ExtendedOperation,只要我們的API是基于Operation接口的,而非BasicOperation或ExtendedOperation。下面為以上代碼的應用示例:
1 public static void main(String[] args) { 2 double x = Double.parseDouble(args[0]); 3 double y = Double.parseDouble(args[1]); 4 test(ExtendedOperation. class ,x,y); 5 } 6 private static <T extends Enum<T> & Operation> void test( 7 Class<T> opSet, double x, double y) { 8 for (Operation op : opSet.getEnumConstants()) { 9 System.out.printf("%f %s %f = %f%n",x,op,y,op.apply(x,y)); 10 } 11 }
????? 注意,參數Class<T> opSet將推演出類型參數的實際類型,即上例中的ExtendedOperation。與此同時,test函數的參數類型限定確保了類型參數既是枚舉類型又是Operation的實現類,這正是遍歷元素和執行每個元素相關聯的操作所必須的。
?
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
