【第21条】用类来代替enum结构

要先说明一下:本书写作于2001年,正值作者参与建设JDK1.4的时期。后来到了JDK1.5,Java又将抛弃了多年的enum枚举重拾了起来。所以本条是在没有enum的时候写的。

 

    JDK1.4及以前版本省略了enum。其实enum也是一种struct,我们当然是用class来代替之,但为什么又要单独作为一条来讲呢?是因为用类来替代枚举的时候,比较容易(或者说事实中绝大多数人都已经)犯一些错误。

 

    比如我们要一个有三种颜色的Color,有人会定义为

public class Color {
    public static final String RED = "RED";
    public static final String GREEN = "GREEN";
    public static final String BLUE = "BLUE";
}

 

 这是我们非常常见的常量定义方法,通过 Color.RED 来使用。但是,你有没有想过,如果一个“不太合格”的程序员很有可能会写出这样的代码:

if (dotA.color.equals("RED")) {
    ......
}

 

这就将硬编码写到了程序中,而且如果不做Source Review是很难发现的。

 

这种写法还有一个“变种”,用int而非String:

 

public class Color {
    public static final int RED = 0;
    public static final int GREEN = 1;
    public static final int BLUE = 2;
}

 

当然,那个人可以继续写出 if (dotA.color == 0) { 这样的语句,Source Review 发现的概率更低了。

但是使用int型一个小“变种”,回来带一些好处:

public class Color {
    public static final int RED = 1;
    public static final int GREEN = 2;
    public static final int BLUE = 4;
}

 

每个值都是用2的整数幂,这样可以在需要保存多于一个状态的和的时候,将所有项目相加(或者位或)得到“保存值”。使用的时候,在通过此值与相应项目的位与后的Boolean值来判断是否包含之:

// 保存时
int value = Color.RED | Color.BLUE;   // 或 Color.RED + Color.BLUE
saveValue(value);   // 保存,可能是写入数据库

// 使用时
int value = getValue();   // 从数据库中取出
if (value & Color.RED) {
    system.out.println("包含红色");
} else if (value & Color.GREEN) {
    system.out.println("包含绿色");
} else if (value & Color.BLUE) {
    system.out.println("包含蓝色");
} 

// 结果会是:
// 包含红色
// 包含蓝色

 

当项目中这样的常量组越来越多,constants包中类会越来越多,甚至都想 import xxxx.constants.*; 了。于是有人开始把这些所有的常量组,放入一个Constants类中

public class Constants{
    // 颜色
   public static final int COLOR_RED = 1;
    public static final int COLOR_GREEN = 2;
    public static final int COLOR_BLUE = 4;

    // 形状
   public static final int SHAPE_CIRCLE = 1;
    public static final int SHAPE_RECTANGLE = 2;
    public static final int SHAPE_TRIANGLE = 4;
    ......
}
   

 现在import一个类就好了,但是,那个人又来了,这次他吸取了之前的教训,没有硬编码了,但是他写了:

if (dotA.color == Constants.SHAPE_CIRCLE) {  // A点的颜色是圆形吗?  &^!#&^*`~  倒!
   .......

这样的语句是不会被编译器挑出来的,那么你能做的,除了祈祷就是痛哭了!在之前的模式下,没有写成 if (dotA.color == Shape.CIRCLE) 就已经不错了。

 

    面对这个问题,书中给出了一个“尚未被人知晓”的方法——类型安全枚举模式。注意,它只是一种模式,再次强调JDK1.5之前并没有枚举,它实质上还是一个类。

 

public class Color {
    private final String name;

    private Color(String name){  // 使用者无法创建这个类的实例
        this.name = name;
    }

    public String toString() {
        return this.name;
    }

    public static final Color RED = new Color("red");
    public static final Color GREEN = new Color("green");
    public static final Color BLUE = new Color("blue");
}

 

私有的构造函数,保证了使用者无法创建这个类的实例,除了通过公有的静态final域导出的Color对象外,永远也不会再有其他实例存在。

 

    所谓“类型安全”正式它提供了编译时的类型安全性。任何传入的非null的对象引用,一定表示了三种颜色中的一种。这样的模式下,即防止了类型错误,有防止了硬编码问题。

if (dotA.color.equals(Color.RED)) {  // 前提:dotA的实现类中,color的类型既不是int也不是String,而是Color
    ......
}

 

    我们看到了两大好处。再看看JDK1.5提供的enum关键字(它对应一个类Enum),从使用方法和特征来说,类型安全枚举模式和JDK1.5的enum枚举是非常相似的。那么在没有枚举的年代,作者尚且建议我们使用类型安全枚举模式,如今有了enum,我们就更没有理由拒绝它了。

 

    但是,类型安全枚举模式和enum枚举就没有弱点吗?当然不是,首当其冲的就是刚才提到的,当常量组非常多的时候,使用N多的类型安全枚举模式或enum枚举,可能是一件让人头痛的事情。也许你会因为这一点而一票否决了它。

 

    再有就是如果对应枚举中项目的值(如前例中的dotA.color)将要保存到数据库或外部文件时,势必要用一个int或String来代表之,为此可能要在类型安全枚举类中增加一个private value,然后toString方法返回这个value的String形式;或者在enum中定义抽象方法(类似toString)。但这也可能导致toString方法的滥用,而失去“类型安全”的保护。

 

    最后,当类型安全枚举模式的类(没有研究enum枚举可否)实现了序列化后,如果期待不同版本程序序列化出来的字符流可以相互反序列化,那么后续版本就只能够在原来的最后面追加元素。(当然这一点对于int型和String型常量类也是一样的,而且还可能更糟)

 

    本来还想好好研究一下enum,但是,刚刚得知稍后要进行的公司羽毛球比赛被抽到的下下签,没有心情了,以后再说吧。今天的笔记就写到这儿了。

 

 

 

【Effective Java 学习笔记】系列连载专题请见:
http://tonylian.iteye.com/categories/64208

 

你可能感兴趣的:(【《Effective,Java》学习笔记】)