枚举的用法、枚举与单例、Enum类

为什么要使用枚举

有限元素

以项目中的ProjectCode为例。projectCode只有有限的几个合法值,无论是代码中还是数据库中,我们都必须禁止出现非法值(如SPED、UNKOWN、DouNiWan等)。否则的话,轻则代码运行过程中抛出异常,重则影响大批业务数据。

先考虑不使用枚举的情况。不使用枚举的的话,我们的ProjectCode就只能定义成String:

public class Order{
    /**
    * 项目代码;只有有限的几个合法值
    */
    private String projectCode;
}

如果使用String来定义,为了保证取值合法,我们必须在每一个输入项目代码的位置进行校验。例如,用户输入、第三方接口调用参数或者返回值,等等。但是,只要有一个地方的校验不到位,系统中就可能出现非法数据、进而可能导致业务问题。

如果使用枚举,我们就可以这样定义和使用ProjectCode(先不考虑枚举中是否需要其它字段或方法):

public enum ProjectCode{
    P_1,
    APOLLO,
    CHANG_E,
    MANHATTAN,
    TWO_BOMBS_ONE_SATELLITE,
    INSURANCE;
}

public class Order{
    private ProjectCode projectCode;
}

使用枚举之后,Order#projectCode这个字段自然而然的就只有两种取值情况:合法值,或者是null。假如用户尝试输入一个其它的值,无论是SPED还是ABC,系统都会在转换参数时抛出异常(或者将其处理为null),从而彻底杜绝了非法数据。

快速失败

这是一种“快速失败(fail-fast)”的设计策略,即尽可能早的发现问题、并将问题抛出来,而不是“带病运行”、直到无可挽回时才发现问题。对枚举设计来说,就是尽早地、强制性地检查输入数据是否合法。如果不合法,立即抛出异常。

这种策略还有其它方面的应用。例如ArrayList中的modCount。如果没有这个字段,我们就可以一边遍历列表、一边向其中增/删元素。但是这种操作是非常危险的,甚至有可能会陷入无限循环或者内存溢出的风险中,例如这种:

List<Integer> list = xxxx;
for(int i : list){
    System.out.println(i);
    list.add(i);
}

为了避免这种风险,ArrayList(还有其它集合类)采取了这样的“快速失败”的策略,在第一时间将问题抛出来,交给开发来处理。

问题:但是,为什么用for(int i=0;i<list.size();i++){list.add(i);}这种方式又不抛出异常呢?

我们的系统中,这种策略的最简单应用就是把校验逻辑或者可能的异常返回逻辑放到处理逻辑的前面。这样做一方面符合快速失败的理念,另一方面,也可以减少一些系统消耗。例如:

public void doSomething(Input input){
    // 调用第三方,提交数据
    thirdpartFacade.submit(input);
    // 插入一条操作历史
    insertLog(input);
    // 更新本地数据库
    boolean updateSuccess = update(input);
    if(!updateSuccess){
        // 这个时候才发现有问题已经太晚了。
        throw new RuntimeExcetion("更新失败");
    } 
}

这个例子中,在调用过第三方、操作过本地数据库之后才发现有问题并抛出异常,不仅会导致前面各种操作白白消耗掉性能,而且可能引发分布式事务问题。这种情况下,我们应当尽可能把会发生问题并结束流程的操作放到前面去。

问题:还有其它场景适用这种“快速失败”的设计吗?

枚举的用法

设计时的考虑:枚举与字典表、常量

上面说的限制合法值的需求,我们也可以通过字典表或者常量来实现。这几种方案各有什么利弊呢?或者说为什么一定要使用枚举呢?

先说字典表。字典表的问题很明显:它需要用大量的代码来检查某个值是否是合法值;并且,它并不能严格的满足限定合法值的需求。所以,只要我们有强烈的“限定合法值”的需求,就不应当使用字典表。但是,它也有枚举或常量所无法取代的优点:我们在字典表中增加一条数据后,完全不需要重启系统,新的数据就能生效。而增加了枚举或者常量以后,则必须重启系统,即使新增的代码不会影响任何处理流程。

常量可以说是三个选项中最差的一个。它既没有字典表那样的灵活,又没有枚举那样强大的Java语言级别的支持。所以,一般建议只在private情况下定义常量。

枚举得到了Java的支持,因此,在代码量和合法值限定方面,它都是最优的。但是,与字典表相比,枚举的灵活性比较差:每增加一个枚举都需要重新启动服务。

总的来说,如果只是在局部使用,那么定义一个常量会比较简单可行;如果需要在整个系统、甚至跨系统使用,那么就应该考虑字典表或者枚举。后二者的决策有两个方面:对合法值限定的需求是否强烈,以及增加一个值之后对系统流程是否用影响。

代码用法

在代码中声明枚举就非常简单了:

public enum Type{
    TYPE_1,
    TYPE_2,
    ;
}

代码很少,但是这个枚举类实际上包含了很多东西:一个私有的无参数构造方法;两个属性name和ordinal,以及对应的访问方法;两个静态方法values()以及valueOf();此外还有重写过的toString()/equals()/hashCode()/clone()/compareTo()/finalize()等方法。这些都是Java编译时自动向enum中添加进去的。

需要注意的是其中的ordinal属性:它与枚举的声明顺序有关。以上面的代码为例,TYPE_1的ordinal为0,是因为它在整个Type类中最先被声明出来。如果我们把它放到TYPE_2的后面,那么它的ordinal就是1了。因为ordinal会随代码写法而改变,所以建议业务逻辑不要依赖于它;同时也建议增加枚举时追加在最后面,不要在中间插入。

声明枚举后,要使用枚举也非常简单,大概就类似这样:

public class SomeClass{
    private Type type;
}

像这样声明了枚举字段以后,就可以拿它来像普通字段或者类一样使用了。

需要注意的是,声明、使用了枚举,就尽可能在枚举类的基础上进行操作,而不要仍然用name()等方法来把它转为字符串使用:

SomeClass some = new SomeClass();

// 尽量这样使用
if(Type.TYPE_1 == some.getType()){
    // 略
}

// 尽量不要这样使用
if("TYPE_2".equals(some.getType().name())){
    // 略
}

枚举之间是否相等,可以直接用“==”来比较。实际上enum的equals()方法也是直接使用==来比较的。

除了上面示例的基础的声明之外,枚举也可以像普通的类一样定义字段、实现接口。但是enum不能再继承其它类。

但是,枚举中的字段一般建议声明为private final;其构造方法也应该是private的(enum默认就是private的,编译器会有检查)。

public enum Type implements HasCode{
    Type_1("T1"),
    Type_2("T2"),
    ;
    private final String code;
    Type(String c){
        this.code = c;
    }
    @Override
   public String getCode(){
       return this.code;
   }    
}

枚举与字符串转换

在WEB系统中,可以认为所有的用户输入其实只有三种类型:String、Number、Boolean,如果算上文件(字节流)也就四种。这四种类型里都没有枚举类,所以我们必然需要有某种方式来把用户输入转换为枚举类。一般情况下都是字符串转枚举。

Enum类提供了一个静态方法valueOf(Class c, String name),每一个enum也会默认有一个静态方法valueOf(String name)。StringMVC、Json等框架或工具一般都是基于这两个方法来做转换的。大概其就是这样:

Type type = Enum.valueOf(Type.class, "TYPE_1");
Type type_2 = Type.valueOf("Type_2");

除了Java自带的这两个方法,Apache的工具包中提供了一个EnumUtils类,有类似的方法: Type type = EnumUtils.valueOf(Type.class, "TYPE_1");

当然,也可以用我们常用的“遍历大法”:

for(Type type : Type.values()){
    if(type.name().equals(target)){
        return type;
    }
}
return null;

问题:这三种方法各有什么优点和缺点?

枚举转字符串就很简单了,直接toString()或者用name()都可以。实际上toString()内部调用的就是name()方法。

对数据库来说,除了把枚举转换为字符串来存储之外,还有一种可能更有效率的方式:用整数来存储枚举。在这种情况下,用来映射枚举和整数的就是ordinal字段:枚举转整数时,用ordinal()方法即可;整数转枚举时,用整数作为数组下标,从Enum.values()中直接读取一个枚举就可以了。

不过,除非能确定枚举取值万年不变,一般不会把枚举转换为整数。

问题:MyBatis是怎样处理数据库中的字符串与代码中的枚举类之间的转换的?SpringMVC呢?

枚举与单例

单例

先来复习一下单例的两种写法。

public class Singleton implements Cloneable, Serializable {

    private static final Singleton SINGLETON = new Singleton();

    private volatile static Singleton SINGLETON_0;

    private Singleton() {
    }

    public static Singleton getInstanceEagly() {
        return SINGLETON;
    }

    public static Singleton getInstanceLazy() {
        if (SINGLETON_0 == null) {
            synchronized (Singleton.class) {
                if (SINGLETON_0 == null) {
                    SINGLETON_0 = new Singleton();
                }
            }
        }
        return SINGLETON_0;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

这个类里面示例了两种单例的写法。一般来说,这两种写法都可以保证只生成一个实例,除了一些特殊情况。

例如反射。下面的代码就可以绕开Singleton类的单例,获取一个全新的实例:

Class<Singleton> claz = Singleton.class;

Constructor<Singleton> constructor = claz.getDeclaredConstructor(null);
constructor.setAccessible(true);
Singleton singleton = constructor.newInstance(null);

System.out.println(singleton == Singleton.getInstanceEagly());

System.out.println(singleton == Singleton.getInstanceLazy());

问题:除了反射之外,还有哪些方法可能生成一个新的Singleton实例?Singleton类定义中有提示。

除了自己写单例之外,把实例委托给Spring等容器进行管理也是一个办法。这里按下不表。

枚举

用枚举来实现单例,首要的是一个枚举类中只能声明一个实例。否则就不是单例了。例如这样:

public enum SingletonEnum {
    SINGLETON
}

相比前面的Singleton类,写法非常简洁。本质上,枚举等价于饿汉模式的单例:在类加载的同时进行初始化。

除了代码简洁之外,枚举形式的单例还能避免出现新的实例:无论是用反射、clone还是序列化。

问题:为什么?
问题:真的没有任何办法来生成一个新的枚举实例吗?

Enum类

前面说过,虽然我们没有在enum中声明,每个enum都会有name和ordinal属性和对应的方法;此外,enum默认都实现了equals()/clone()/compareTo()等方法,这些属性和方法是从哪儿来的呢?

大部分字段和方法都来自于java.lang.Enum类。Java在编译enum时,会默认令它继承自Enum类。name/ordinal等方法都是从Enum类中继承而来的。

所以,上面那个例子中的简单枚举Type,实际上等价于:

public enum Type extends Enum{
    public static final Type TYPE_1 = new Type("TYPE_1",0),
    TYPE_2 = new Type("TYPE_2",1);
    
    private final String name;
    private final int ordinal;
    public final String name() {
        return name;
    }
    public final int ordinal() {
        return ordinal;
    }
    // 这个方法的调用会有些不一样
    private Type(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }
    public String toString() {
        return name;
    }
    public final boolean equals(Object other) {
        return this==other;
    }
    public final int hashCode() {
        return super.hashCode();
    }
    protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }
    public final int compareTo(Type o) {
        Type other = o;
        Type self = this;
        if (self.getClass() != other.getClass() && // optimization
            self.getDeclaringClass() != other.getDeclaringClass())
            throw new ClassCastException();
        return self.ordinal - other.ordinal;
    }
    public final Class<E> getDeclaringClass() {
        Class<?> clazz = getClass();
        Class<?> zuper = clazz.getSuperclass();
        return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
    }
    public static Type valueOf(String name) {
        return Enum.valueOf(Type.class,name);
    }
    protected final void finalize() { }
    private void readObject(ObjectInputStream in) throws IOException,
        ClassNotFoundException {
        throw new InvalidObjectException("can't deserialize enum");
    }
    private void readObjectNoData() throws ObjectStreamException {
        throw new InvalidObjectException("can't deserialize enum");
    }
}

其中大部分方法都是final的,目的就是为了统一枚举类的行为,并禁止各枚举自己改变行为。例如,Enum类统一了clone()方法和readObject()/readObjectNoData()方法(都是丢出异常),以此禁止了通过clone或者序列化/反序列化的方法来生成新的枚举实例。

利用Java继承机制,定义好一个类的核心功能、本质属性,而把其它的细枝末节交给子类自己处理,这是面向对象语言和设计很强大的一点。利用好这一点我们可以创建系统和业务模型、复用代码、提高编码效率、提高项目质量。

除了继承机制之外,面向对象语言和设计中还有很多非常出色的功能点。此外还有诸如“快速失败”这样的通用设计思想。希望大家在编程时能够把这些功能学起来、用起来,既能提高项目质量、提高开发水平,又能提高工作效率、减少无谓的加班和返工。