`

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

 

分享到:
评论

相关推荐

    Effective Java第三版1

    《Effective Java》是Java编程领域的一本经典著作,由Joshua Bloch撰写,该书的第三版继续提供了关于如何编写高效、优雅、可维护的Java代码的指导。以下是基于给出的目录和部分内容提取的一些关键知识点: ### 第一...

    《Effective Java》读书分享.pptx

    "Effective Java 读书分享" 《Effective Java》读书分享.pptx 是一本 Java 编程语言指南,旨在帮助开发者编写高质量、可维护的 Java 代码。该书包含 90 个条目,每个条目讨论一条规则,涵盖了 Java 编程语言的...

    effective java 读书笔记

    《Effective Java》是Java开发领域的经典著作,作者...以上仅是《Effective Java》的部分内容,书中还有更多关于枚举、泛型、集合、多线程等方面的重要指导,都是Java开发者提升代码质量、遵循良好编程习惯的宝贵资源。

    Effective Java.zip

    《Effective Java》是一本经典Java编程指南,作者是Joshua Bloch,这本书深入探讨了如何编写高质量、高效、可维护的Java代码。以下是对压缩包中各章节主要知识点的详细阐述: 1. **第2章 创建和销毁对象** - 单例...

    java 枚举(enum) 详解(学习资料)

    《Effective Java》和《Java 与模式》等书籍推荐使用枚举实现单例,因为它们提供了一种天然的防篡改机制,保证了在任何情况下都只有一个实例。 5. **枚举的优势**: - **安全性**:枚举对象是不可变的,防止了意外...

    effective-java 配套代码

    《Effective Java》是Java开发领域的一本经典著作,由Joshua Bloch撰写,书中提出了一系列编程最佳实践和设计模式,帮助开发者写出更高效、更可靠、更易于维护的Java代码。配套代码`effective-java-examples-master`...

    2021年EFFECTIVEJAVA读书笔记.docx

    Effective Java 读书笔记 - 枚举与注解 本文总结了Effective Java 中关于枚举与注解的知识点,涵盖了枚举类型的优点、使用指南、避免使用 int 常量、使用 EnumSet 和 EnumMap 等。 枚举类型的优点 枚举类型提供了...

    effecctivejava 第三版中文

    《Effective Java》是Java编程领域的一本经典著作,由Joshua Bloch撰写,现在已经更新到第三版。这本书深入探讨了如何编写高效、可维护且设计良好的Java代码,是每一个Java开发者提升技能的重要参考资料。以下是对该...

    effectiveJava的笔记

    《Effective Java》是Java开发领域的经典著作,由Joshua Bloch编写,旨在提供一系列实用的编程准则和最佳实践。这本书的第三版包含了大量更新,涵盖了Java语言和平台的新发展,如Java 8和Java 9的新特性。以下是对...

    Effective-Java-2nd-Edition-(May-2008).zip_effective java

    《Effective Java》是Java编程领域的一本经典著作,由Joshua Bloch撰写,第二版发布于2008年。这本书旨在提供实用的编程指导,帮助开发者写出更高效、更可维护的Java代码。以下是对书中核心知识点的详细解读: 1. *...

    Effective-Java:Effective Java中文版第二版示例代码

    《Effective Java》是Java开发领域的经典著作,由Joshua Bloch撰写,中文版第二版更是深受广大Java开发者喜爱。这本书提供了许多实用的编程实践和经验教训,帮助开发者编写出更高效、可维护的Java代码。这里我们将...

    Effective Java 第三版

    《Effective Java 第三版》是由Joshua Bloch所著的一本关于Java编程的书籍,旨在向Java开发者传授编写高效、健壮、可靠的Java代码的最佳实践。书中分为多个章节,每一章节都详细介绍了Java语言中的一个特定主题,并...

    Effective.Java_Java8_并发_java_effectivejava_

    目录:一、创建和销毁对象 (1 ~ 7)二、对于所有对象都通用的方法 (8 ~ 12)三、类和接口 (13 ~ 22)四、泛型 (23 ~ 29)五、枚举和注解 (30 ~ 37)六、方法 (38 ~ 44)七、通用程序设计 (45 ~ 56)八、异常 ...

    1_Effective_Java_2nd_Edition_proglib_java_Joshua_

    2. **枚举类型**:Bloch强调了枚举类型在Java中的重要性,它比传统的常量类更安全,更易于使用,且能提供更多的功能,如枚举方法和枚举实例的集合操作。 3. **泛型**:第二版《Effective Java》详尽讨论了Java泛型...

    Effective-Java读书笔记

    《Effective Java》是Java编程领域的一本经典著作,由Joshua Bloch撰写,它提供了许多实用的编程指导原则,帮助开发者写出更高效、更可维护的代码。这本书分为多个条目,每个条目都深入探讨了一个特定的Java编程实践...

    EffectiveJava:有效的 Java 示例

    《EffectiveJava》是Java开发领域的经典著作,由Joshua Bloch撰写,提供了许多关于如何编写高效、可维护和设计良好的Java代码的实用建议。这本书的第2版在原有的基础上进行了更新,以适应Java语言的新发展。现在,...

Global site tag (gtag.js) - Google Analytics