?
三十、用enum代替int常量:
?? ?? 枚舉類型是指由一組固定的常量組成合法值的類型,該特征是在Java 1.5 中開始被支持的,之前的Java代碼都是通過“公有靜態(tài)常量域字段”的方法來簡單模擬枚舉的,如:
? ? ? 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;
? ? ? 這樣的寫法是比較脆弱的。首先是沒有提供相應(yīng)的類型安全性,如兩個(gè)邏輯上不相關(guān)的常量值之間可以進(jìn)行比較或運(yùn)算(APPLE_FUJI - ORANGE_TEMPLE),再有就是常量int是編譯時(shí)常量,被直接編譯到使用他們的客戶端中。如果與該常量關(guān)聯(lián)的int發(fā)生了變化,客戶端就必須重新編譯。如果沒有重新編譯,程序還是可以執(zhí)行,但是他們的行為將不確定。
? ? ? 下面我們來看一下Java 1.5 中提供的枚舉的聲明方式:
? ? ? public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
? ? ? public enum Orange { NAVEL, TEMPLE, BLOOD }
? ? ? 和“公有靜態(tài)常量域字段”不同的是,如果函數(shù)的參數(shù)是枚舉類型,如Apple,那么他的實(shí)際值只能來自于該枚舉所聲明的枚舉值,即FUJI, PIPPIN, GRANNY_SMITH。如果試圖將Apple和Orange中的枚舉值進(jìn)行比較,將會(huì)導(dǎo)致編譯錯(cuò)誤。
? ? ? 和C/C++中提供的枚舉不同的是,Java中允許在枚舉中添加任意的方法和域,并實(shí)現(xiàn)任意的接口。下面先給出一個(gè)帶有域方法和域字段的枚舉聲明:
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 }
? ? ? 在上面的枚舉示例代碼中,已經(jīng)將數(shù)據(jù)和枚舉常量關(guān)聯(lián)起來了,因此需要聲明實(shí)例域字段,同時(shí)編寫一個(gè)帶有數(shù)據(jù)并將數(shù)據(jù)保存在域中的構(gòu)造器。枚舉天生就是不可變的,因此所有的域字段都應(yīng)該為final的。下面看一下該枚舉的應(yīng)用示例:
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
?? ?? 枚舉的靜態(tài)方法values()將按照聲明順序返回他的值數(shù)組。枚舉的toString方法返回每個(gè)枚舉值的聲明名稱。
? ? ? 在實(shí)際的編程中,我們常常需要針對(duì)不同的枚舉常量提供不同的數(shù)據(jù)操作行為,見如下代碼:
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 }
?? ?? 上面的代碼已經(jīng)表達(dá)出這種根據(jù)不同的枚舉值,執(zhí)行不同的操作。但是上面的代碼在設(shè)計(jì)方面確實(shí)存在一定的缺陷,或者說漏洞,如果我們新增枚舉值的時(shí)候,所有和apply類似的域函數(shù),都需要進(jìn)行相應(yīng)的修改,如有遺漏將會(huì)導(dǎo)致異常的拋出。幸運(yùn)的是,Java的枚舉提供了一種更好的方法可以將不同的行為與每個(gè)枚舉常量關(guān)聯(lián)起來:在枚舉類型中聲明一個(gè)抽象的apply方法,并在特定于常量的類主體中,用具體的方法覆蓋每個(gè)常量的抽象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 }
? ? ? 這樣在添加新枚舉常量時(shí)就不會(huì)輕易忘記提供相應(yīng)的apply方法了。我們?cè)谶M(jìn)一步看一下如何將枚舉常量和特定的數(shù)據(jù)進(jìn)行關(guān)聯(lián),見如下代碼:
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 }
? ? ? 下面給出以上代碼的應(yīng)用示例:
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
?? ?? 沒有類型有一個(gè)自動(dòng)產(chǎn)生的valueOf(String)方法,他將常量的名字轉(zhuǎn)變?yōu)槊杜e常量本身,如果在枚舉中覆蓋了toString方法(如上例),就需要考慮編寫一個(gè)fromString方法,將定制的字符串表示法變回相應(yīng)的枚舉,見如下代碼:
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 }
? ? ? 需要注意的是,我們無法在枚舉常量構(gòu)造的時(shí)候?qū)⒆陨矸湃氲組ap中,這樣會(huì)導(dǎo)致編譯錯(cuò)誤。與此同時(shí),枚舉構(gòu)造器不可以訪問枚舉的靜態(tài)域,除了編譯時(shí)的常量域之外。
?? ?
三十一、用實(shí)例域代替序數(shù):
?? ?? Java中的枚舉提供了ordinal()方法,他返回每個(gè)枚舉常量在類型中的數(shù)字位置,如:
1 public enum Color { 2 WHITE,RED,GREEN,BLUE,ORANGE,BLACK; 3 public int indexOfColor() { 4 return ordinal() + 1; 5 } 6 }
? ? ? 上面的枚舉中提供了一個(gè)獲取顏色索引的方法(indexOfColor),該方法將返回顏色值在枚舉類型中的聲明位置,如果我們的外部程序依賴了該順序值,那么這將會(huì)是非常危險(xiǎn)和脆弱的,因?yàn)橐坏┻@些枚舉值的位置出現(xiàn)變化,或者在已有枚舉值的中間加入新的枚舉值時(shí),都將導(dǎo)致該索引值的變化。該條目推薦使用實(shí)例域的方式來代替枚舉提供的序數(shù)值,見如下修改后的代碼:
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規(guī)范中談到ordinal時(shí)這么寫道:“大多數(shù)程序員都不需要這個(gè)方法。它是設(shè)計(jì)成用于像EnumSet和EnumMap這種基于枚舉的通用數(shù)據(jù)結(jié)構(gòu)的。”除非你在編寫的是這種數(shù)據(jù)結(jié)構(gòu),否則最好避免使用ordinal()方法。
?? ?
三十二、用EnumSet代替位域:
? ? ? 下面的代碼給出了位域的實(shí)現(xiàn)方式:
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位運(yùn)算將幾個(gè)常量合并到一個(gè)集合中,使用方式如下:
? ? ? text.applyStyles(Text.STYLE_BOLD | Text.STYLE_ITALIC);
? ? ? Java中提供了EnumSet類,該類繼承自Set接口,同時(shí)也提供了豐富的功能,類型安全性,以及可以從任何其他Set實(shí)現(xiàn)中得到的互用性。但是在內(nèi)部具體實(shí)現(xiàn)上,沒有EnumSet內(nèi)容都表示為位矢量。如果底層的枚舉類型有64個(gè)或者更少的元素,整個(gè)EnumSet就用單個(gè)long來表示,因此他的性能也是可以比肩位域的。與此同時(shí),他提供了大量的操作方法,其實(shí)現(xiàn)也是基于位操作的,但是相比于手工位操作,由于EnumSet替我們承擔(dān)了這部分的開發(fā),從而也避免了一些容易出現(xiàn)的低級(jí)錯(cuò)誤,代碼的美觀程度也會(huì)有所提升,見如下修改的代碼:
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提供了豐富的靜態(tài)工廠來輕松創(chuàng)建集合。
?
三十三、用EnumMap代替序數(shù)索引:
?? ?? 前面的條目已經(jīng)給出了盡量不要直接使用枚舉的ordinal()方法的原因,這里就不在做過多的贅述了。在這個(gè)條目中,只是再一次給出了ordinal()的典型用法,與此同時(shí)也再一次提供了一個(gè)更為合理的解決方案用于替換ordinal()方法,從而進(jìn)一步證明我們?cè)诰幋a過程中應(yīng)該盡可能減少對(duì)枚舉中ordinal()函數(shù)的依賴。見如下代碼:
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 }
?? ?? 這里我需要簡單描述一下上面代碼的應(yīng)用場景:在一個(gè)花園里面有很多的植物,它們被分成3類,分別是一年生(ANNUAL)、多年生(PERENNIAL)和兩年生(BIENNIAL),正好對(duì)應(yīng)著Herb.Type中的枚舉值。現(xiàn)在我們需要做的是遍歷花園中的每一個(gè)植物,并將這些植物分為3類,最后再將分類后的植物分類打印出來。下面將提供另外一種方法,即通過EnumMap來實(shí)現(xiàn)和上面代碼相同的邏輯:
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 }
?? ?? 和之前的代碼相比,這段代碼更加清晰,也更加安全,運(yùn)行效率方面也是可以與使用ordinal()的方式想媲美的。
三十四、用接口模擬可伸縮的枚舉:
?? ?? 枚舉是無法被擴(kuò)展(extends)的,這是一個(gè)無法回避的事實(shí)。如果我們的操作中存在一些基礎(chǔ)操作,如計(jì)算器中的基本運(yùn)算類型(加減乘除)。然而對(duì)于有些用戶來講,他們也可以使用更高級(jí)的操作,如求冪和求余等。針對(duì)這樣的需求,該條目提出了一種非常巧妙的設(shè)計(jì)方案,即利用枚舉可以實(shí)現(xiàn)接口這一事實(shí),我們將API的參數(shù)定義為該接口,而不是具體的枚舉類型,見如下代碼:
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。下面為以上代碼的應(yīng)用示例:
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 }
????? 注意,參數(shù)Class<T> opSet將推演出類型參數(shù)的實(shí)際類型,即上例中的ExtendedOperation。與此同時(shí),test函數(shù)的參數(shù)類型限定確保了類型參數(shù)既是枚舉類型又是Operation的實(shí)現(xiàn)類,這正是遍歷元素和執(zhí)行每個(gè)元素相關(guān)聯(lián)的操作所必須的。
?
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

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