`

Effective Java (枚举)

 
阅读更多

 

三十、用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的实现类,这正是遍历元素和执行每个元素相关联的操作所必须的。

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics