Effective Java-枚举和注解

枚举和注解是Java1.5版本中新增的特性,本章讨论使用它们时的最佳实践。
本章内容导图:

Effective Java-枚举和注解_第1张图片

1.用enum代替int常量

枚举类型是指由一组固定的常量组成合法值的类型,如一年中的季节、太阳系中的行星、一副牌中的花色等。在编程语言没有引入枚举之前,表示枚举类型的常用模式是声明一组具名的int常量,每个类型成员一个常量:

public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;

这种方法称作int枚举模式,它存在着诸多不足:
1.类型安全性问题
可能会传递错误的值
2.没有自己的命名空间
一般只能通过前缀的形式区分
3.采用int枚举模式的程序十分脆弱
int枚举是编译时常量,被编译到使用它们的客户端中,如果枚举常量值发生了变化,客户端必须重新编译才行。
4.无法提供便利的方法打印信息
int枚举的打印信息只是数字

String枚举模式是int枚举模式的变体,虽然它可以提供可打印的字符串,但存在性能及书写时的安全性问题。

Java1.5开始,提供了枚举类型,它不仅可以避免int枚举模式和String枚举模式的缺点,还可以提供许多额外的好处:

public enum Apple {
    FUJI,
    PIPPIN,
    GRANNY_SMITH
}

枚举的好处有:
1.提供编译时的类型安全
如果声明一个参数的类型为枚举类型Apple,就可以保证,被传递到该参数上的任何非null的对象引用一定属于三个有效的Apple之一。试图传递类型错误的值时,会导致编译错误。
2.每个枚举类型都有自己的命名空间
枚举类是独立的类型,有自己的命名空间,可以增加或者重新排列枚举类型中的常量。
3.可提供便利的打印信息
通过toString(),可以将枚举转换成可打印的字符串。
4.允许添加任意的方法和域,并实现任意的接口
枚举是一种类型,可以拥有自己的方法和域,并实现接口。
枚举的缺点:
1.装载和初始化枚举时会有空间和时间的成本

在枚举中添加域和方法的动机:
1.想将数据与它的常量关联起来
2.添加方法增强枚举类型功能

如果一个枚举具有普适性,就应该成为一个顶层类;如果它只是被用在一个特定的顶层类中,就应该成为该顶层类的一个成员类。

在枚举类中添加方法时,这些方法是枚举常量共有的,但有时每个常量都会关联本质上完全不同的行为,可以使用特定于常量的方法实现来完成。它的实现过程如下:
1.在枚举类型中声明一个抽象的方法
2.在特定常量的类主体中,用具体的方法实现抽象方法

enum Operation {
    PLUS {
        @Override
        double apply(double x, double y) {
            return x + y;
        }
    },
    
    MINUS {
        @Override
        double apply(double x, double y) {
            return x - y;
        }
    },
    
    TIMES {
        @Override
        double apply(double x, double y) {
            return x * y;
        }
    },
    
    DIVIDE {
        @Override
        double apply(double x, double y) {
            return x / y;
        }
    };

    abstract double apply(double x, double y);
}

使用枚举的时机:
每当需要一组固定常量的时候。
1.包括“天然的枚举类型”,如行星、一周的天数、一年中的季节等;
2.包括在编译时就知道其所有可能值的其他集合,如操作代码、命令行标记、菜单的选项等。
枚举类型中的常量集并不一定要始终保持不变,专门设计枚举特性也是考虑到枚举类型二进制兼容演变的需求。

与int常量相比,枚举类型的优势很多。枚举更加易读,也更加安全,功能更加强大。
许多枚举都不需要显式的构造器或者成员,但如有需求,你可以提供与常量相关联的属性和方法。还可以使用特定于常量的方法将多种行为与单个方法关联。
如果多个枚举常量同时共享相同的行为,可考虑策略枚举

2.用实例域代替序数

所有的枚举都有一个ordinal方法,它返回每个枚举常量在类型中的数组位序。
依赖ordinal()返回的枚举常量序数会使得代码极难维护。因为枚举常量可能会进行重新排序,也可能会添加新的枚举常量。

永远不要根据枚举序数去得到与它关联的值,而是要将它保存在一个实例域中

//不当的使用方式
public enum Ensemble {
    SOLO, DUET, TRIO, QUARTET, QUINTET,
    SEXTET, SEPTET, OCTET, NONET, DECTET;
    
    //依赖ordinal()返回与枚举常量关联的值
    public int numberOfMusicians() {
        return ordinal() + 1;
    }
}
//推荐的使用方式
public enum Ensemble {
    SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
    SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
    NONET(9), DECTET(10), TRIPLE_QUARTET(12);
    
    private final int numberOfMusicians;
    
    Ensemble(int size) {
        this.numberOfMusicians = size;
    }
    
    public int numberOfMusicians() {
        return numberOfMusicians;
    }
}

Enum规范中对ordinal()的描述为:大多数程序员都不需要这个方法,它被设计成用于像EnumSet、EnumMap这种基于枚举的通用数据结构的。除非你在编写这种数据结构,否则最好完全避免使用ordinal方法。

3.用EnumSet代替位域

如果一个枚举类型的元素主要用在集合中,可能会使用int枚举模式:

public class Text {
    public static final int STYLE_BOLD = 1 << 0;          //1
    public static final int STYLE_ITALIC = 1 << 1;        //2
    public static final int STYLE_UNDERLINE = 1 << 2;     //4
    public static final int STYLE_STRIKETHROUGH = 1 << 3; //8
    
    public void applyStyles(int styles) {
        ...
    }
}

这种表示法让你用or位运算符将几个常量合并到一个集合中,这个集合称作位域
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
位域表示法也允许利用位操作,执行像交集、并集这样的集合操作。但位域具有int枚举常量所有的缺点,甚至更多。位域以数字形式打印时,翻译位域比翻译int枚举常量要困难的多,遍历位域表示的所有元素也相当不容易。

Set是一种集合,只能向其中添加不重复的对象,enum也要求其成员都是唯一的,看起来也具有集合的行为,但不能从enum中删除/添加元素。Java1.5引入了EnumSet替代传统的基于int枚举类型的位域集合,它表示从单个枚举类型中提取多个枚举值的集合
EnumSet是与enum类型一起使用的专用Set类型,EnumSet中的所有元素都必须来自同一个enum。
使用EnumSet代替位域后的代码更加简短、更加清楚、更加安全:

public class Text {
    public enum Style {
        BOLD, ITALIC, UNDERLINE, STRIKETHROUGH
    }
    
    public void applyStyles(Set