澳门新葡亰娱乐官网Effective Java (枚举)

在Java SE5之前,我们要使用枚举类型时,通常会使用static final
定义一组int常量来标识,代码如下

三十、用enum代替int常量:

澳门新葡亰娱乐官网,编写高质量代码:改善Java程序的151个建议(第6章:枚举和注解___建议88~92),java151

public static final int MAN = 0;
public static final int WOMAN = 1;

     
枚举类型是指由一组固定的常量组成合法值的类型,该特征是在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;
     

建议88:用枚举实现工厂方法模式更简洁

  工厂方法模式(Factory Method Pattern)是”
创建对象的接口,让子类决定实例化哪一个类,并使一个类的实例化延迟到其它子类”。工厂方法模式在我们的开发中经常会用到。下面以汽车制造为例,看看一般的工厂方法模式是如何实现的,代码如下:

 1 //抽象产品
 2 interface Car{
 3     
 4 }
 5 //具体产品类
 6 class FordCar implements Car{
 7     
 8 }
 9 //具体产品类
10 class BuickCar implements Car{
11     
12 }
13 //工厂类
14 class CarFactory{
15     //生产汽车
16     public static Car createCar(Class<? extends Car> c){
17         try {
18             return c.newInstance();
19         } catch (InstantiationException | IllegalAccessException e) {
20             e.printStackTrace();
21         }
22         return null;
23     }
24 }

  这是最原始的工厂方法模式,有两个产品:福特汽车和别克汽车,然后通过工厂方法模式来生产。有了工厂方法模式,我们就不用关心一辆车具体是怎么生成的了,只要告诉工厂”
给我生产一辆福特汽车 “就可以了,下面是产出一辆福特汽车时客户端的代码: 

    public static void main(String[] args) {
        //生产车辆
        Car car = CarFactory.createCar(FordCar.class);
    }

  这就是我们经常使用的工厂方法模式,但经常使用并不代表就是最优秀、最简洁的。此处再介绍一种通过枚举实现工厂方法模式的方案,谁优谁劣你自行评价。枚举实现工厂方法模式有两种方法:

(1)、枚举非静态方法实现工厂方法模式

  我们知道每个枚举项都是该枚举的实例对象,那是不是定义一个方法可以生成每个枚举项对应产品来实现此模式呢?代码如下:

 1 enum CarFactory {
 2     // 定义生产类能生产汽车的类型
 3     FordCar, BuickCar;
 4     // 生产汽车
 5     public Car create() {
 6         switch (this) {
 7         case FordCar:
 8             return new FordCar();
 9         case BuickCar:
10             return new BuickCar();
11         default:
12             throw new AssertionError("无效参数");
13         }
14     }
15 
16 }

  create是一个非静态方法,也就是只有通过FordCar、BuickCar枚举项才能访问。采用这种方式实现工厂方法模式时,客户端要生产一辆汽车就很简单了,代码如下: 

public static void main(String[] args) {
        // 生产车辆
        Car car = CarFactory.BuickCar.create();
    }

(2)、通过抽象方法生成产品

  枚举类型虽然不能继承,但是可以用abstract修饰其方法,此时就表示该枚举是一个抽象枚举,需要每个枚举项自行实现该方法,也就是说枚举项的类型是该枚举的一个子类,我们俩看代码:

 1 enum CarFactory {
 2     // 定义生产类能生产汽车的类型
 3     FordCar{
 4         public Car create(){
 5             return new FordCar();
 6         }
 7     },
 8     BuickCar{
 9         public Car create(){
10             return new BuickCar();
11         }
12     };
13     //抽象生产方法
14     public abstract Car create();
15 }

  首先定义一个抽象制造方法create,然后每个枚举项自行实现,这种方式编译后会产生CarFactory的匿名子类,因为每个枚举项都要实现create抽象方法。客户端调用与上一个方案相同,不再赘述。

  大家可能会问,为什么要使用枚举类型的工厂方法模式呢?那是因为使用枚举类型的工厂方法模式有以下三个优点:

  • 避免错误调用的发生:一般工厂方法模式中的生产方法(也就是createCar方法),可以接收三种类型的参数:类型参数(如我们的例子)、String参数(生产方法中判断String参数是需要生产什么产品)、int参数(根据int值判断需要生产什么类型的的产品),这三种参数都是宽泛的数据类型,很容易发生错误(比如边界问题、null值问题),而且出现这类错误编译器还不会报警,例如:

    public static void main(String[] args) {
        // 生产车辆
        Car car = CarFactory.createCar(Car.class);
    }

  Car是一个接口,完全合乎createCar的要求,所以它在编译时不会报任何错误,但一运行就会报出InstantiationException异常,而使用枚举类型的工厂方法模式就不存在该问题了,不需要传递任何参数,只需要选择好生产什么类型的产品即可。

  • 性能好,使用简洁:枚举类型的计算时以int类型的计算为基础的,这是最基本的操作,性能当然会快,至于使用便捷,注意看客户端的调用,代码的字面意思就是”
    汽车工厂,我要一辆别克汽车,赶快生产”。
  • 降低类间耦合:不管生产方法接收的是Class、String还是int的参数,都会成为客户端类的负担,这些类并不是客户端需要的,而是因为工厂方法的限制必须输入的,例如Class参数,对客户端main方法来说,他需要传递一个FordCar.class参数才能生产一辆福特汽车,除了在create方法中传递参数外,业务类不需要改Car的实现类。这严重违背了迪米特原则(Law
    of Demeter 
    简称LoD),也就是最少知识原则:一个对象应该对其它对象有最少的了解。

  而枚举类型的工厂方法就没有这种问题了,它只需要依赖工厂类就可以生产一辆符合接口的汽车,完全可以无视具体汽车类的存在。

相信很多小伙伴,在实际开发中也是那么干的,既然这样已经能实现枚举的功能,为什么还要引入枚举呢?我们接着往下看当我们需要是同这组“int枚举”是代码如下

这样的写法是比较脆弱的。首先是没有提供相应的类型安全性,如两个逻辑上不相关的常量值之间可以进行比较或运算(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的实现类,这正是遍历元素和执行每个元素相关联的操作所必须的。

建议89:枚举项的数量限制在64个以内

  为了更好地使用枚举,Java提供了两个枚举集合:EnumSet和EnumMap,这两个集合使用的方法都比较简单,EnumSet表示其元素必须是某一枚举的枚举项,EnumMap表示Key值必须是某一枚举的枚举项,由于枚举类型的实例数量固定并且有限,相对来说EnumSet和EnumMap的效率会比其它Set和Map要高。

     
虽然EnumSet很好用,但是它有一个隐藏的特点,我们逐步分析。在项目中一般会把枚举用作常量定义,可能会定义非常多的枚举项,然后通过EnumSet访问、遍历,但它对不同的枚举数量有不同的处理方式。为了进行对比,我们定义两个枚举,一个数量等于64,一个是65(大于64即可,为什么是64而不是128,512呢,一会解释),代码如下: 

 1 //普通枚举项,数量等于64
 2 enum Const{
 3     A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,
 4     AA,BB,CC,DD,EE,FF,GG,HH,II,JJ,KK,LL,MM,NN,OO,PP,QQ,RR,SS,TT,UU,VV,WW,XX,YY,ZZ,
 5     AAA,BBB,CCC,DDD,EEE,FFF,GGG,HHH,III,JJJ,KKK,LLL
 6 }
 7 //大枚举,数量超过64
 8 enum LargeConst{
 9     A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,
10     AA,BB,CC,DD,EE,FF,GG,HH,II,JJ,KK,LL,MM,NN,OO,PP,QQ,RR,SS,TT,UU,VV,WW,XX,YY,ZZ,
11     AAAA,BBBB,CCCC,DDDD,EEEE,FFFF,GGGG,HHHH,IIII,JJJJ,KKKK,LLLL,MMMM
12 }

  Const的枚举项数量是64,LagrgeConst的枚举项数量是65,接下来我们希望把这两个枚举转换为EnumSet,然后判断一下它们的class类型是否相同,代码如下: 

 1 public class Client89 {
 2     public static void main(String[] args) {
 3         EnumSet<Const> cs = EnumSet.allOf(Const.class);
 4         EnumSet<LargeConst> lcs = EnumSet.allOf(LargeConst.class);
 5         //打印出枚举数量
 6         System.out.println("Const的枚举数量:"+cs.size());
 7         System.out.println("LargeConst的枚举数量:"+lcs.size());
 8         //输出两个EnumSet的class
 9         System.out.println(cs.getClass());
10         System.out.println(lcs.getClass());
11     }
12 }

  程序很简单,现在的问题是:cs和lcs的class类型是否相同?应该相同吧,都是EnumSet类的工厂方法allOf生成的EnumSet类,而且JDK
API也没有提示EnumSet有子类。我们来看看输出结果:

  澳门新葡亰娱乐官网 1

  很遗憾,两者不相等。就差一个元素,两者就不相等了?确实如此,这也是我们重点关注枚举项数量的原因。先来看看Java是如何处理的,首先跟踪allOf方法,其源码如下:
 

1  public static <E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType) {
2         //生成一个空EnumSet
3         EnumSet<E> result = noneOf(elementType);
4         //加入所有的枚举项
5         result.addAll();
6         return result;
7     }

  allOf通过noneOf方法首先生成了一个EnumSet对象,然后把所有的枚举都加进去,问题可能就出在EnumSet的生成上了,我们来看看noneOf的源码:  

 1   public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
 2         //获得所有的枚举项
 3         Enum[] universe = getUniverse(elementType);
 4         if (universe == null)
 5             throw new ClassCastException(elementType + " not an enum");
 6         //枚举数量小于等于64
 7         if (universe.length <= 64)
 8             return new RegularEnumSet<>(elementType, universe);
 9         else 
10             //枚举数量大于64
11             return new JumboEnumSet<>(elementType, universe);
12     }

  看到这里,恍然大悟,Java原来是如此处理的:当枚举项数量小于等于64时,创建一个RegularEnumSet实例对象,大于64时则创建一个JumboEnumSet实例对象。

  为什么要如此处理呢?这还要看看这两个类之间的差异,首先看RegularEnumSet类,源码如下:

 1 class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {
 2     private static final long serialVersionUID = 3411599620347842686L;
 3     /**
 4      * Bit vector representation of this set.  The 2^k bit indicates the
 5      * presence of universe[k] in this set.
 6      */
 7     //记录所有的枚举号,注意是long型
 8     private long elements = 0L;
 9    //构造函数
10     RegularEnumSet(Class<E>elementType, Enum[] universe) {
11         super(elementType, universe);
12     }
13 
14    //加入所有元素
15     void addAll() {
16         if (universe.length != 0)
17             elements = -1L >>> -universe.length;
18     }
19     
20    //其它代码略
21 }

  我们知道枚举项的排序值ordinal
是从0、1、2……依次递增的,没有重号,没有跳号,RegularEnumSet就是利用这一点把每个枚举项的ordinal映射到一个long类型的每个位置上的,注意看addAll方法的elements元素,它使用了无符号右移操作,并且操作数是负值,位移也是负值,这表示是负数(符号位是1)的”无符号左移”:符号位为0,并补充低位,简单的说,Java把一个不多于64个枚举项映射到了一个long类型变量上。这才是EnumSet处理的重点,其他的size方法、contains方法等都是根据elements方法等都是根据elements计算出来的。想想看,一个long类型的数字包含了所有的枚举项,其效率和性能能肯定是非常优秀的。

  我们知道long类型是64位的,所以RegularEnumSet类型也就只能负责枚举项的数量不大于64的枚举(这也是我们以64来举例,而不以128,512举例的原因),大于64则由JumboEnumSet处理,我们看它是怎么处理的: 

 1 class JumboEnumSet<E extends Enum<E>> extends EnumSet<E> {
 2     private static final long serialVersionUID = 334349849919042784L;
 3 
 4     /**
 5      * Bit vector representation of this set.  The ith bit of the jth
 6      * element of this array represents the  presence of universe[64*j +i]
 7      * in this set.
 8      */
 9    //映射所有的枚举项
10     private long elements[];
11 
12     // Redundant - maintained for performance
13     private int size = 0;
14 
15     JumboEnumSet(Class<E>elementType, Enum[] universe) {
16         super(elementType, universe);
17         //默认长度是枚举项数量除以64再加1
18         elements = new long[(universe.length + 63) >>> 6];
19     }
20 
21       void addAll() {
22         //elements中每个元素表示64个枚举项
23         for (int i = 0; i < elements.length; i++)
24             elements[i] = -1;
25         elements[elements.length - 1] >>>= -universe.length;
26         size = universe.length;
27     }
28 }

  JumboEnumSet类把枚举项按照64个元素一组拆分成了多组,每组都映射到一个long类型的数字上,然后该数组再放置到elements数组中,简单来说JumboEnumSet类的原理与RegularEnumSet相似,只是JumboEnumSet使用了long数组容纳更多的枚举项。不过,这样的程序看着会不会觉得郁闷呢?其实这是因为我们在开发中很少使用位移操作。大家可以这样理解:RegularEnumSet是把每个枚举项映射到一个long类型数字的每个位上,JumboEnumSet是先按照64个一组进行拆分,然后每个组再映射到一个long类型数字的每个位上。

  从以上的分析可知,EnumSet提供的两个实现都是基本的数字类型操作,其性能肯定比其他的Set类型要好的多,特别是Enum的数量少于64的时候,那简直就是飞一般的速度。

  注意:枚举项数量不要超过64,否则建议拆分。

public void showSex(int sex){
     switch(sex){
        case MAN:
           System.out.println("this is a boy");   
        break;    
        case WOMAN:
           System.out.println("this is a Girl");   
        break; 
      } 
}

建议90:小心注解继承

  Java从1.5版本开始引入注解(Annotation),其目的是在不影响代码语义的情况下增强代码的可读性,并且不改变代码的执行逻辑,对于注解始终有两派争论,正方认为注解有益于数据与代码的耦合,”在有代码的周边集合数据”;反方认为注解把代码和数据混淆在一起,增加了代码的易变性,消弱了程序的健壮性和稳定性。这些争论暂且搁置,我们要说的是一个我们不常用的元注解(Meta-Annotation):@Inheruted,它表示一个注解是否可以自动继承,我们开看它如何使用。

  思考一个例子,比如描述鸟类,它有颜色、体型、习性等属性,我们以颜色为例,定义一个注解来修饰一下,代码如下:

 1 import java.lang.annotation.ElementType;
 2 import java.lang.annotation.Inherited;
 3 import java.lang.annotation.Retention;
 4 import java.lang.annotation.RetentionPolicy;
 5 import java.lang.annotation.Target;
 6 
 7 @Retention(RetentionPolicy.RUNTIME)
 8 @Target(ElementType.TYPE)
 9 @Inherited
10 public @interface Desc {
11     enum Color {
12         White, Grayish, Yellow
13     }
14 
15     // 默认颜色是白色的
16     Color c() default Color.White;
17 }

  该注解Desc前增加了三个注解:Retention表示的是该注解的保留级别,Target表示的是注解可以标注在什么地方,@Inherited表示该注解会被自动继承。注解定义完毕,我们把它标注在类上,代码如下: 

 1 @Desc(c = Color.White)
 2 abstract class Bird {
 3     public abstract Color getColor();
 4 }
 5 
 6 // 麻雀
 7 class Sparrow extends Bird {
 8     private Color color;
 9 
10     // 默认是浅灰色
11     public Sparrow() {
12         color = Color.Grayish;
13     }
14 
15     // 构造函数定义鸟的颜色
16     public Sparrow(Color _color) {
17         color = _color;
18     }
19 
20     @Override
21     public Color getColor() {
22         return color;
23     }
24 }
25 
26 // 鸟巢,工厂方法模式
27 enum BirdNest {
28     Sparrow;
29     // 鸟类繁殖
30     public Bird reproduce() {
31         Desc bd = Sparrow.class.getAnnotation(Desc.class);
32         return bd == null ? new Sparrow() : new Sparrow(bd.c());
33     }
34 }

  上面程序声明了一个Bird抽象类,并且标注了Desc注解,描述鸟类的颜色是白色,然后编写一个麻雀Sparrow类,它有两个构造函数,一个是默认的构造函数,也就是我们经常看到的麻雀是浅灰色的,另外一个构造函数是自定义麻雀的颜色,之后又定义了一个鸟巢(工厂方法模式),它是专门负责鸟类繁殖的,它的生产方法reproduce会根据实现类注解信息生成不同颜色的麻雀。我们编写一个客户端调用,代码如下:   

1 public static void main(String[] args) {
2         Bird bird = BirdNest.Sparrow.reproduce();
3         Color color = bird.getColor();
4         System.out.println("Bird's color is :" + color);
5     }

  现在问题是这段客户端程序会打印出什么来?因为采用了工厂方法模式,它最主要的问题就是bird变量到底采用了那个构造函数来生成,是无参构造函数还是有参构造?如果我们单独看子类Sparrow,它没有被添加任何注释,那工厂方法中的bd变量就应该是null了,应该调用的是无参构造。是不是如此呢?我们来看运行结果:“Bird‘s 
Color  is White ”;

  白色?这是我们添加到父类Bird上的颜色,为什么?这是因为我们在注解上加了@Inherited注解,它表示的意思是我们只要把注解@Desc加到父类Bird上,它的所有子类都会从父类继承@Desc注解,不需要显示声明,这与Java的继承有点不同,若Sparrow类继承了Bird却不用显示声明,只要@Desc注解释可自动继承的即可。

  采用@Inherited元注解有利有弊,利的地方是一个注解只要标注到父类,所有的子类都会自动具有父类相同的注解,整齐,统一而且便于管理,弊的地方是单单阅读子类代码,我们无从知道为何逻辑会被改变,因为子类没有显示标注该注解。总体上来说,使用@Inherited元注解弊大于利,特别是一个类的继承层次较深时,如果注解较多,则很难判断出那个注解对子类产生了逻辑劫持。

看起来这些貌似也没什么问题,但是我们知道,一个项目基本都是基于团队开发,或许只有你自己知道int类型的1代表gril,0代表Boy。其他同事看到这个函数根本不知道其中的含义,这样的代码很明显阅读性很差,从而会造成沟通成本很高。我们接着往下看,现在你为你的func写了很nice的注释,傻子都能看得懂(0
boy,1
gril)。但是项目组不可避免的总会出现那么一两个傻子,非要给你传个3进来,而且这样的错误编译器不会报任何错误,运行时会造成什么bug,这个只有乔老爷知道了。所以这样的代码是极不安全的。使用枚举就能很好的避免上面的问题,接下来我们就来理一理枚举的用法。

建议91:枚举和注解结合使用威力更大

  我们知道注解的写法和接口很类似,都采用了关键字interface,而且都不能有实现代码,常量定义默认都是public
static final 
类型的等,它们的主要不同点是:注解要在interface前加上@字符,而且不能继承,不能实现,这经常会给我们的开发带来些障碍。  

  我们来分析一下ACL(Access  Control  
List,访问控制列表)设计案例,看看如何避免这些障碍,ACL有三个重要元素:

  • 资源,有哪些信息是要被控制起来的。
  • 权限级别,不同的访问者规划在不同的级别中。
  • 控制器(也叫鉴权人),控制不同的级别访问不同的资源。

  鉴权人是整个ACL的设计核心,我们从最主要的鉴权人开始,代码如下:   

interface Identifier{
    //无权访问时的礼貌语
    String REFUSE_WORD  =  "您无权访问";
    //鉴权
    public  boolean identify();
}

  这是一个鉴权人接口,定义了一个常量和一个鉴权方法。接下来应该实现该鉴权方法,但问题是我们的权限级别和鉴权方法之间是紧耦合,若分拆成两个类显得有点啰嗦,怎么办?我们可以直接顶一个枚举来实现,代码如下:

 1 enum CommonIdentifier implements Identifier {
 2     // 权限级别
 3     Reader, Author, Admin;
 4 
 5     @Override
 6     public boolean identify() {
 7         return false;
 8     }
 9 
10 }

  定义了一个通用鉴权者,使用的是枚举类型,并且实现了鉴权者接口。现在就剩下资源定义了,这很容易定义,资源就是我们写的类、方法等,之后再通过配置来决定哪些类、方法允许什么级别的访问,这里的问题是:怎么把资源和权限级别关联起来呢?使用XML配置文件?是个方法,但对我们的示例程序来说显得太繁重了,如果使用注解会更简洁些,不过这需要我们首先定义出权限级别的注解,代码如下:

1 @Retention(RetentionPolicy.RUNTIME)
2 @Target(ElementType.TYPE)
3 @interface Access{
4     //什么级别可以访问,默认是管理员
5     CommonIdentifier level () default CommonIdentifier.Admin;
6 }

  该注解释标注在类上面的,并且会保留到运行期。我们定义一个资源类,代码如下: 

@Access(level=CommonIdentifier.Author)
class Foo{

}

  Foo类只能是作者级别的人访问。场景都定义完毕了,那我们看看如何模拟ACL实现,代码如下:

 1 public static void main(String[] args) {
 2         // 初始化商业逻辑
 3         Foo b = new Foo();
 4         // 获取注解
 5         Access access = b.getClass().getAnnotation(Access.class);
 6         // 没有Access注解或者鉴权失败
 7         if (null == access || !access.level().identify()) {
 8             // 没有Access注解或者鉴权失败
 9             System.out.println(access.level().REFUSE_WORD);
10         }
11     }

  看看这段代码,简单,易读,而且如果我们是通过ClassLoader类来解释该注解的,那会使我们的开发更简洁,所有的开发人员只要增加注解即可解决访问控制问题。注意看加粗代码,access是一个注解类型,我们想使用Identifier接口的identity鉴权方法和REFUSE_WORD常量,但注解释不能集成的,那怎么办?此处,可通过枚举类型CommonIdentifier从中间做一个委派动作(Delegate),委派?你可以然identity返回一个对象,或者在Identifier上直接定义一个常量对象,那就是“赤裸裸”
的委派了。

枚举用于存储数量有限的一组固定的数据集。使用场景:上面说到的性别的表示,一年四级春夏秋冬的表示,一周七天的表示,颜色的表示等等。

建议92:注意@Override不同版本的区别

  @Override注解用于方法的覆写上,它是在编译器有效,也就是Java编译器在编译时会根据注解检查方法是否真的是覆写,如果不是就报错,拒绝编译。该注解可以很大程度地解决我们的误写问题,比如子类和父类的方法名少写一个字符,或者是数字0和字母O为区分出来等,这基本是每个程序员都曾将犯过的错误。在代码中加上@Override注解基本上可以杜绝出现此类问题,但是@Override有个版本问题,我们来看如下代码:

 1 interface Foo {
 2     public void doSomething();
 3 }
 4 
 5 class FooImpl implements Foo{
 6     @Override
 7     public void doSomething() {
 8         
 9     }
10 }

 这是一个简单的@Override示例,接口中定义了一个doSomething方法,实现类FooImpl实现此方法,并且在方法前加上了@Override注解。这段代码在Java1.6版本上编译没问题,虽然doSomething方法只是实现了接口的定义,严格来说并不是覆写,但@Override出现在这里可减少代码中出现的错误。

  可如果在Java1.5版本上编译此段代码可能会出现错误:

      The  method doSomeThing()  of type FooImpl must override  a
superclass  method 

  注意,这是个错误,不能继续编译,原因是Java1.5版本的@Override是严格遵守覆写的定义:子类方法与父类方法必须具有相同的方法名、输出参数、输出参数(允许子类缩小)、访问权限(允许子类扩大),父类必须是一个类,不能是接口,否则不能算是覆写。而这在Java1.6就开放了很多,实现接口的方法也可以加上@Override注解了,可以避免粗心大意导致方法名称与接口不一致的情况发生。

  在多环境部署应用时,需呀考虑@Override在不同版本下代表的意义,如果是Java1.6版本的程序移植到1.5版本环境中,就需要删除实现接口方法上的@Override注解。

建议88:用枚举实现工厂方法模式更简洁 工厂方法模式(Fact…

枚举的简单用法

 // 普通枚举
 public enum ColorEnum {
        white, red, green, blue;
 }

    /**
     * 
     * 枚举中有一个自带的静态方法values(),返回enum实例的数据并且该数组中的元素顺序和声明时的顺序一样 
     * 枚举也可以像普通的类一样可以添加属性和方法,可以为它添加静态和非静态的属性或方法
     */
    public enum SeasonEnum {
        //注:枚举写在最前面,否则编译出错
        spring, summer, autumn, winter;

        private final static String position = "test";

        public static SeasonEnum getSeason() {
            if ("test".equals(position))
                return spring;
            else
                return winter;
        }
    }
     /**
     * 带构造器的枚举
     * 必须带有一个参构造器和一个属性跟方法,否则编译出错
     * 
     */
    public enum Mode {
        PULLDOWN("下拉"), PULLUP("上拉");
        private final String value;

        /**
         * 构造器默认也只能是private, 从而保证构造函数只能在内部使用
         * 
         */
        private Mode(String value) {
            this.value = value;
        }

        public String getValue() {
            return value;
        }
    }

枚举中还有一个ordinal()方法返回一个int值,这是每个enum实例在声明时的次序,从0开始。枚举类还实现了Compareable接口,所以他具有compareTo()方法。同时还实现了Serializable接口,还自动为你提供了equals()和hashCode()方法。除了不能继承一个枚举类之外,我们基本上可以把枚举类当成一个常规的Java类,可以往其中添加新的方法,包括抽象方法甚至main方法。

现在我们用枚举来实现上面的那个性别函数

public enum EnumSex {
        MAN, WOMAN

    }

    public static void showSex(EnumSex EnumSex) {
        switch (EnumSex) {
        case MAN:
            System.out.println("this is a boy");
            break;
        case WOMAN:
            System.out.println("this is a girl");
            break;
        }
    }

    public static void main(String[] args) {
        showSex(EnumSex.MAN);
        // showSex(EnumSex.Renyao) 编译报错

    }

这样既有很好的阅读性又避免的安全性问题

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图