`
TonyLian
  • 浏览: 396724 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

【第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

 

分享到:
评论

相关推荐

    C++Primer(第5版 )中文版(美)李普曼等著.part2.rar

     第Ⅲ部分 类设计者的工具 437  第13章 拷贝控制 439  13.1 拷贝、赋值与销毁 440  13.1.1 拷贝构造函数 440  13.1.2 拷贝赋值运算符 443  13.1.3 析构函数 444  13.1.4 三/五法则 447  13.1.5 使用=default...

    C++ Primer中文版(第5版)李普曼 等著 pdf 1/3

     第Ⅲ部分 类设计者的工具 437  第13章 拷贝控制 439  13.1 拷贝、赋值与销毁 440  13.1.1 拷贝构造函数 440  13.1.2 拷贝赋值运算符 443  13.1.3 析构函数 444  13.1.4 三/五法则 447  13.1.5 使用=default...

    asp.net面试题

    第二种:覆盖方法 public new XXXX(){} 第三种:new 约束指定泛型类声明中的任何类型参数都必须有公共的无参数构造函数。 2.如何把一个array复制到arrayList里 foreach( object o in array )arrayList.Add(o); 3....

    c语言程序设计标准教程

    在这个位域定义中,a占第一字节的4位,后4位填0表示不使用,b从第二字节开始,占用4位,c占用4位。 2. 由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进位。 3. 位域...

    C语言编程要点

    1.3 什么时候用一条switch语句比用多条if语句更好? 9 1.4 switch语句必须包含default分支吗? 10 1.5 switch语句的最后一个分支可以不要break语句吗? 11 1.6 除了在for语句中之外,在哪些情况下还要使用逗号...

    亮剑.NET深入体验与实战精要2

    因pdf的容量过大分4个压缩包打包,还有一个源码另外下载。 《.NET深入体验与实战精要》作者身为从事.NET一线开发的资深开发专家,常年耕耘...15.5.14 使用视图代替跨库操作 572 15.5.15 尽量避免大事务操作 572 15.5.16...

    亮剑.NET深入体验与实战精要3

    因pdf的容量过大分4个压缩包打包,还有一个源码另外下载。 《.NET深入体验与实战精要》作者身为从事.NET一线开发的资深开发专家,常年耕耘...15.5.14 使用视图代替跨库操作 572 15.5.15 尽量避免大事务操作 572 15.5.16...

    你必须知道的495个C语言问题

    2.22 有没有一种自动方法来跟踪联合的哪个域在使用? 枚举 2.23 枚举和一组预处理的#define有什么不同? 2.24 枚举可移植吗? 2.25 有什么显示枚举值符号的容易方法吗? 位域 2.26 一些结构声明中的这些...

    C 语言编程常见问题解答.chm

    3 什么时候用一条switch语句比用多条if语句更好? 1. 4 switch语句必须包含default分支吗? 1. 5 switch语句的最后—个分支可以不要break语句吗? 1. 6 除了在for语句中之外,在哪些情况下还要使用逗号运算? 1. 7...

    C语言FAQ 常见问题列表

    o 4.10 如果我不使用表达式的值, 我应该用 ++i 或 i++ 来自增一个变量吗? o 4.11 为什么如下的代码 int a = 100, b = 100; long int c = a * b; 不能工作? o 4.12 我需要根据条件把一个复杂的表达式赋值给两个...

    《你必须知道的495个C语言问题》

    2.22 有没有一种自动方法来跟踪联合的哪个域在使用? 30 枚举 31 2.23 枚举和一组预处理的#define有什么不同? 31 2.24 枚举可移植吗? 31 2.25 有什么显示枚举值符号的容易方法吗? 31 位域 31 2.26 ...

    你必须知道的495个C语言问题(PDF)

    3.10 如果我不使用表达式的值, 我应该用++i 或i++ 来自增一个变量 吗? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 3.11 为什么如下的代码int a = 100, b = 100; long int c = a * b;...

    51单片机C语言编程基础及实例

    #include #define LED P1^1 //用符号 LED 代替 P1_1 用符号 //用符号 KEY_ON 代替 P1_6 用符号 //用符号 KEY_OFF 代替 P1_7 用符号 //单片机复位后的执行入口,void 表示空,无输入参数,无返回值 #define KEY_ON ...

Global site tag (gtag.js) - Google Analytics