为什么要用enum?

最近发现公司的代码中存在大量的使用int枚举的方式,也看到了一些使用enum进行枚举的方式。对此很好奇,进行了研究,估写此文总结,来说一说为什么要使用enum。

假设现在有两种订单类型:预订订单和非预订订单。
需求一: 方法submitOrder根据不同订单类型进行不同的处理。
需求二: 给一个对象: OrderResult设置订单类型。

下面分别用两种枚举方式来实现。
int枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class IntEnumExample{
private static final PRE_ORDER = 1;
private static final NOT_PRE_ORDER = 2;

public void submitOrder(int orderType, OrderResult orderResult){
orderResult.setType(orderType);
if(orderType == PRE_ORDER){
do something to process preOrder
}else if(orderType == NOT_PRE_ORDER){
do something to process other order
}
}

public static void main(String [] args){
IntEnumExample example = new IntEnumExample();
//passing wrong type to the method, however, no compile error and runtime exception here, the bug is hard to be discerned.
example.submitOrder(3, orderResult);
}

}

从上面的例子可以看到,使用int枚举有几个缺点:

  1. int枚举不做类型检查,可以给上面的submitOrder方法传入任意int值
  2. 代码可读性低,如果没有文档或者源码,不知道给submitOrder传递的值的意义。
    不仅如此,就像我之前修改项目中sonar扫描代码发现的问题那样,很多人在使用int枚举时,并没有像上例中将int值定义成static final。如果忘记定义变量为final则int枚举的值就可以被修改,如果忘记定义变量为static,就可能出现使用这个int值时,该int值还没有被初始化好。
    总之,会引起bug。
    下面来看看使用enum如何做同样的事儿。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    class IntEnumExample{
    private enum ORDER_TYPE {
    NOT_PRE_ORDER(1),PRE_ORDER(2);
    private final int value;
    private ORDER_TYPE(int value){
    this.value = value;
    }
    }

    public void submitOrder(ORDER_TYPE orderType, OrderResult orderResult){
    orderResult.setType(orderType);
    if(orderType == ORDER_TYPE.PRE_ORDER){
    do something to process preOrder
    }else if(orderType == ORDER_TYPE.NOT_PRE_ORDER){
    do something to process other order
    }
    }

    public static void main(String [] args){
    IntEnumExample example = new IntEnumExample();
    //compiler will complain error here, if argument is not the type in the enum.
    example.submitOrder(ORDER_TYPE.PRE_ORDER, orderResult);
    }

    }

通过上述代码可以看到,enum很优雅的解决了上一个例子中的问题。

  1. 编译器将对enum进行类型检查,类型不符合的编译器会直接报错。
  2. 相比与int枚举型直接传int数值的方式,enum传递enum类型对象的方式,代码可读性更高,传递的枚举类型一目了然。
  3. enum类型中的对象本身就是static final的。

重要提示:
还有一点值得一提的是,如果有时想给每一个枚举类型赋予一个int值,要使用上例中enum定义的方式。

1
2
3
4
5
6
7
private enum ORDER_TYPE {
NOT_PRE_ORDER(1),PRE_ORDER(2);
private final int value;
private ORDER_TYPE(int value){
this.value = value;
}
}

enum本质也是一个类,所以方法ORDER_TYPE(int value)是这个枚举类型的构造函数,故每个枚举类型在初始化的时候需要给构造函数传递响应的值,如: PRE_ORDER(2)
这种情况下,想得到枚举类型对应的int数值时可以通过ORDER_TYPE.PRE_ORDER.value获取。
单独提下enum的这种用法是因为,有些人可能会直接使用enum中的ordinal()方法直接实现enum类型与int类型的关联。ordinal()方法返回的是enum类型在被定义时的序数,如ORDER_TYPE.PRE_ORDER.value.ordinal()返回值为0。所以获取枚举类型对应的int数值貌似也可以通过ORDER_TYPE.PRE_ORDER.value.ordinal()+1实现。
不要使用ordinal()方法!不要使用ordinal()方法!不要使用ordinal()方法!重要的事情说三遍,为什么?
序数是很不可靠的东西,序数是可以改变的,假设有一天ORDER_TYPE的定义变了,需要增加几种类型,或者不小心换了NOT_PRE_ORDERPRE_ORDER定义时的顺序,如:

1
2
3
private enum ORDER_TYPE {
PRE_ORDER,NOT_PRE_ORDER;
}

这时就会造成很严重的bug,而且不好发现,编译时,运行时都不会有报错。
所以,不要依赖ordianl()方法。

关于enum的其他事情
《Effective Java》中提到了一种利用enum实现单例模式的方式,这种方式实现单例代码最简介,《Effective Java》极力推荐。实现方式如下:

1
2
3
public enum Singleton {
INSTANCE;
}

我很早就知道这种方法,但是我一直不知道其原理。要知道,实现单例模式的时候要注意几个问题:

  1. 防止多线程多次实例化单例
  2. 防止反序列化多次实例化单例
  3. 防止反射攻击
    一般情况下上述三个问题的解决办法为:
  4. 使用synchronize空值实例创建
  5. 重写readResolve()方法
  6. 将构造方法设置为private

通过这次对enum的研究,我一下知道为什么可以用enum实现单例

  1. enum是一个类,enum类型的实例是static final的,也就是只能被初始化一次,不可能被多线程实例化多次
  2. enum本身已经帮助我们实现了Serializable接口。
  3. enum的构造器是private的,不能被访问。
    所以enum是实现单例的最好方式。

关于enum今天先说到这里吧。