枚举

枚举

本文基于这篇文章, 你可以通过点击前往阅读。

枚举类型是Java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是因为它既是一种类class类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。

在阅读Effective Java的时候,也包括我们在阅读一些文档的时候,我们会发现大家都极力推荐我们使用枚举类型而不是常量。

public class DayDemo {
    public static final int MONDAY =1;
    public static final int TUESDAY=2;
    public static final int WEDNESDAY=3;
    public static final int THURSDAY=4;
    public static final int FRIDAY=5;
    public static final int SATURDAY=6;
    public static final int SUNDAY=7;
}

上述的常量定义常量的方式称为int枚举模式,这样的定义方式并没有什么错,但它存在许多不足,如在类型安全和使用方便性上并没有多少好处,如果存在定义int值相同的变量,混淆的几率还是很大的,编译器也不会提出任何警告,因此这种方式在枚举出现后并不提倡,现在我们利用枚举类型来重新定义上述的常量,同时也感受一把枚举定义的方式,如下定义周一到周日的常量

public enum Weekday {

    MONDAY(1), TUESDAY(2), WEDNESDAY(3),
    THURSDAY(4), FRIDAY(5), SATURDAY(6), SUNDAY(7);

    private int day;
    Weekday(int day) {
        this.day = day;
    }

    public int getDay() {
        return this.day;
    }
}

枚举实现原理

我们大概了解了枚举类型的定义与简单使用后,现在有必要来了解一下枚举类型的基本实现原理。

我们来分析一下Enum的编译后的class文件,这样观察出来的结果比较真实。首先我们先来看看使用Java内置的class反编译器锁编译出来的结果: javap -p Weekday.class

Compiled from "Weekday.java"
public final class com.demo.Weekday extends java.lang.Enum<com.demo.Weekday> {
  public static final com.demo.Weekday MONDAY;
  public static final com.demo.Weekday TUESDAY;
  public static final com.demo.Weekday WEDNESDAY;
  public static final com.demo.Weekday THURSDAY;
  public static final com.demo.Weekday FRIDAY;
  public static final com.demo.Weekday SATURDAY;
  public static final com.demo.Weekday SUNDAY;
  private int value;
  private static final com.demo.Weekday[] $VALUES;
  public static com.demo.Weekday[] values();
  public static com.demo.Weekday valueOf(java.lang.String);
  private com.demo.Weekday(int);
  static {};
}

实际上在使用关键字enum创建枚举类型并编译后,编译器会为我们生成一个相关的类,这个类继承了Java API中的java.lang.Enum类,也就是说通过关键字enum创建枚举类型在编译后事实上也是一个类类型而且该类继承自java.lang.Enum类。

其实我们真实要研究的不是这个java类,相反,而是JavaClass文件, 使用不同的反编译工具可能得出的是不同的结果,当然,最好的是直接查看class文件。 其实通过javap的命令,不仅限于上面使用的方法,可以很全面的查看这个class文件的所有方法,但是这个解析结果不适合全面查看,我们接下来使用不同的工具来做更多的事情。

这个编译结果是使用好几个反编译文件之后的结果, 具体使用的软件是JAD.exe完成的结果。

package com.demo;
public final class Weekday extends Enum{
    public static Weekday[] values(){
        return (Weekday[])$VALUES.clone();
    }
    public static Weekday valueOf(String name){
        return (Weekday)Enum.valueOf(com/demo/Weekday, name);
    }

    private Weekday(String s, int i, int value){
        super(s, i);
        this.value = value;
    }

    public static final Weekday MONDAY;
    public static final Weekday TUESDAY;
    public static final Weekday WEDNESDAY;
    public static final Weekday THURSDAY;
    public static final Weekday FRIDAY;
    public static final Weekday SATURDAY;
    public static final Weekday SUNDAY;
    private int value;
    private static final Weekday $VALUES[];

    static {
        MONDAY = new Weekday("MONDAY", 0, 1);
        TUESDAY = new Weekday("TUESDAY", 1, 2);
        WEDNESDAY = new Weekday("WEDNESDAY", 2, 3);
        THURSDAY = new Weekday("THURSDAY", 3, 4);
        FRIDAY = new Weekday("FRIDAY", 4, 5);
        SATURDAY = new Weekday("SATURDAY", 5, 6);
        SUNDAY = new Weekday("SUNDAY", 6, 7);
        $VALUES = (new Weekday[] {
            MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
        });
    }
}

从反编译的代码可以看出编译器确实帮助我们生成了一个Weekday类(注意该类是final类型的,将无法被继承)而且该类继承自java.lang.Enum类,该类是一个抽象类(稍后我们会分析该类中的主要方法),除此之外,编译器还帮助我们生成了7个Weekday类型的实例对象分别对应枚举中定义的7个日期,这也充分说明了我们前面使用关键字enum定义的Weekday类型中的每种日期枚举常量也是实实在在的Weekday实例对象,只不过代表的内容不一样而已。注意编译器还为我们生成了两个静态方法,分别是values()valueOf(),稍后会分析它们的用法,到此我们也就明白了,使用关键字enum定义的枚举类型,在编译期后,也将转换成为一个实实在在的类,而在该类中,会存在每个在枚举类型中定义好变量的对应实例对象,如上述的MONDAY枚举类型对应public static final Day MONDAY;同时编译器会为该类创建两个方法,分别是values()valueOf()。到此相信我们对枚举的实现原理也比较清晰,下面我们深入了解一下java.lang.Enum类以及values()valueOf()的用途。

Enum抽象类常见方法

ordinal()

该方法获取的是枚举变量在枚举类中声明的顺序,下标从0开始,如日期中的MONDAY在第一个位置,那么MONDAY的ordinal值就是0,如果MONDAY的声明位置发生变化,那么ordinal方法获取到的值也随之变化,注意在大多数情况下我们都不应该首先使用该方法,毕竟它总是变幻莫测的。

public final int ordinal() {
    return ordinal;
}

那我们为什么这么说呢,是因为在每一个enum里面的对象被创建的时候, 可以很明显的发现,对象的真正创建过程,创建的static域并不是真正的按照我们设定的: 我们可以很明显的发现,这个实际创建过程被JVM修改了,他们的修改过程不是我们可以干预的. 而且,一旦我们改变了顺序,对编译器没有任何改变,但是却对实际业务代码造成了影响,所以,如果可以的话,我们尽可能不用这个方法。

//我们希望的事情:
MONDAY(1)
//我们实际看到的结果:这里面添加了名字,和顺序号
new Weekday("MONDAY", 0, 1);

compareTo(E o)

方法则是比较枚举的大小,注意其内部实现是根据每个枚举的ordinal值大小进行比较的。

public final int compareTo(E o) {
    Enum<?> other = (Enum<?>)o;
    Enum<E> self = this;
    if (self.getClass() != other.getClass() && // optimization
        self.getDeclaringClass() != other.getDeclaringClass())
        throw new ClassCastException();
    return self.ordinal - other.ordinal;
}

正如我们刚才所说,这个ordinal不安全,所以,也就可以说明compareTo方法不安全,所以同样,如果我们判断,可以通过equals方法或者==方法来看。 同样通过Enum源码,可以知道,Enum实现了Comparable接口,这也是可以使用compareTo比较的原因,当然Enum构造函数也是存在的,该函数只能由编译器调用,毕竟我们只能使用enum关键字定义枚举,其他事情就放心交给编译器吧。

name()

name()方法与toString()几乎是等同的,都是输出变量的字符串形式.

public final String name() {
    return name;
}

跟ordinal类似。

valueOf(Class<T> enumType, String name)

根据枚举类的Class对象和枚举名称获取枚举常量,注意该方法是静态的。

public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) {
    T result = enumType.enumConstantDirectory().get(name);
    if (result != null)
        return result;
    if (name == null)
        throw new NullPointerException("Name is null");
    throw new IllegalArgumentException(
        "No enum constant " + enumType.getCanonicalName() + "." + name);
}

这个方法比较有意思,一般用来获取枚举常量,可是如果不能转换,就会直接抛异常, 这对我们转换的过程中,需要注意catch异常。

实际上通过调用enumType(Class对象的引用)的enumConstantDirectory方法获取到的是一个Map集合,在该集合中存放了以枚举name为key和以枚举实例变量为value的Key&Value数据,因此通过name的值就可以获取到枚举实例,看看enumConstantDirectory方法源码:

Map<String, T> enumConstantDirectory() {
        if (enumConstantDirectory == null) {
            //getEnumConstantsShared最终通过反射调用枚举类的values方法
            T[] universe = getEnumConstantsShared();
            if (universe == null)
                throw new IllegalArgumentException(
                    getName() + " is not an enum type");
            Map<String, T> m = new HashMap<>(2 * universe.length);
            //map存放了当前enum类的所有枚举实例变量,以name为key值
            for (T constant : universe)
                m.put(((Enum<?>)constant).name(), constant);
            enumConstantDirectory = m;
        }
        return enumConstantDirectory;
    }
    private volatile transient Map<String, T> enumConstantDirectory = null;

到这里我们也就可以看出枚举序列化确实不会重新创建新实例,jvm保证了每个枚举实例变量的唯一性。再来看看反射到底能不能创建枚举,下面试图通过反射获取构造器并创建枚举:

public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
  //获取枚举类的构造函数(前面的源码已分析过)
   Constructor<SingletonEnum> constructor=SingletonEnum.class.getDeclaredConstructor(String.class,int.class);
   constructor.setAccessible(true);
   //创建枚举
   SingletonEnum singleton=constructor.newInstance("otherInstance",9);
  }
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at zejian.SingletonEnum.main(SingletonEnum.java:38)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

显然告诉我们不能使用反射创建枚举类,这是为什么呢?不妨看看newInstance方法源码:

public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        //这里判断Modifier.ENUM是不是枚举修饰符,如果是就抛异常
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

源码很了然,确实无法使用反射创建枚举实例,也就是说明了创建枚举实例只有编译器能够做到而已。显然枚举单例模式确实是很不错的选择,因此我们推荐使用它。

构造方法

protected Enum(String name, int ordinal) {
    this.name = name;
    this.ordinal = ordinal;
}

这个构造方法比较有意思,我们结合我们当前的方法来说明:

private Weekday(String s, int i, int value){
    super(s, i);
    this.value = value;
}

我们可以很简单的发现这么几个问题,Enum的构造器通过JVM 编译我们编写的代码,之后会对构造器改变,之后指向的就是Enum的构造器。

枚举类内部JVM生成的方法

我们看完继承自继承自java.lang.Enum类的主要方法之后,现在我们可以再回过头来看看我们自己的Enum类的方法,但是原来的java文件肯定是不能发现什么东西的,因为会被JVM编译器经过编译之后,转换成JVM运行状态。还是老样子,我们分析class文件。

values()

从结果可知道,values()方法的作用就是获取枚举类中的所有变量,并作为数组返回。 这里我们还必须注意到,由于values()方法是由编译器插入到枚举类中的static方法,所以如果我们将枚举实例向上转型为Enum,那么values()方法将无法被调用,因为Enum类中并没有values()方法,valueOf()方法也是同样的道理,注意是一个参数的。

public static Weekday[] values(){
    return (Weekday[])$VALUES.clone();
}

valueOf(String name)

因此从前面的分析中,但valueOf()方法还是有出现的,只不过编译器生成的valueOf()方法需传递一个name参数,而Enum自带的静态方法valueOf()则需要传递两个方法,从前面反编译后的代码可以看出,编译器生成的valueOf方法最终还是调用了Enum类的valueOf方法,下面通过代码来演示这两个方法的作用:

public static Weekday valueOf(String name){
    return (Weekday)Enum.valueOf(com/demo/Weekday, name);
}

枚举的进阶用法

在前面的分析中,我们都是基于简单枚举类型的定义,也就是在定义枚举时只定义了枚举实例类型,实际上使用关键字enum定义的枚举类,除了不能使用继承(因为编译器会自动为我们继承Enum抽象类而Java只支持单继承,因此枚举类是无法手动实现继承的),可以把enum类当成常规类,也就是说我们可以向enum类中添加方法和变量,甚至是mian方法,下面就来感受一把。

关于覆盖enum类方法

既然enum类跟常规类的定义没什么区别(实际上enum还是有些约束的),那么覆盖父类的方法也不会是什么难说,可惜的是父类Enum中的定义的方法只有toString方法没有使用final修饰,因此只能覆盖toString方法.

enum类中定义抽象方法

与常规抽象类一样,enum类允许我们为其定义抽象方法,然后使每个枚举实例都实现该方法,以便产生不同的行为方式,注意abstract关键字对于枚举类来说并不是必须的如下:

enum COLOR {
    RED("red"){
        @Override
        public String getColorCode(){return "123456";}
    },
    BLUE("blue"){
        @Override
        public String getColorCode(){return "123456";}
    },
    BLACK("black"){
        @Override
        public String getColorCode(){return "123456";}
    },
    WHITE("white"){
        @Override
        public String getColorCode(){return "123456";}
    };

    private final String color;
    public String getColor() {
        return color;
    }
    public abstract String getColorCode();

    COLOR(String color) {
        this.color = color;
    }
}

通过这种方式就可以轻而易举地定义每个枚举实例的不同行为方式。

enum类与接口

由于Java单继承的原因,enum类并不能再继承其它类,但并不妨碍它实现接口,因此enum类同样是可以实现多接口的。

一下摘自Thinking in Java

public interface Food {
  enum Appetizer implements Food {
    SALAD, SOUP, SPRING_ROLLS;
  }
  enum MainCourse implements Food {
    LASAGNE, BURRITO, PAD_THAI,
    LENTILS, HUMMOUS, VINDALOO;
  }
  enum Dessert implements Food {
    TIRAMISU, GELATO, BLACK_FOREST_CAKE,
    FRUIT, CREME_CARAMEL;
  }
  enum Coffee implements Food {
    BLACK_COFFEE, DECAF_COFFEE, ESPRESSO,
    LATTE, CAPPUCCINO, TEA, HERB_TEA;
  }
}

public class TypeOfFood {
  public static void main(String[] args) {
    Food food = Appetizer.SALAD;
    food = MainCourse.LASAGNE;
    food = Dessert.GELATO;
    food = Coffee.CAPPUCCINO;
  }
}

通过这种方式可以很方便组织上述的情景,同时确保每种具体类型的食物也属于Food,现在我们利用一个枚举嵌套枚举的方式,把前面定义的菜谱存放到一个Meal菜单中,通过这种方式就可以统一管理菜单的数据了。

public enum Meal{
  APPETIZER(Food.Appetizer.class),
  MAINCOURSE(Food.MainCourse.class),
  DESSERT(Food.Dessert.class),
  COFFEE(Food.Coffee.class);
  private Food[] values;
  private Meal(Class<? extends Food> kind) {
    //通过class对象获取枚举实例
    values = kind.getEnumConstants();
  }
  public interface Food {
    enum Appetizer implements Food {
      SALAD, SOUP, SPRING_ROLLS;
    }
    enum MainCourse implements Food {
      LASAGNE, BURRITO, PAD_THAI,
      LENTILS, HUMMOUS, VINDALOO;
    }
    enum Dessert implements Food {
      TIRAMISU, GELATO, BLACK_FOREST_CAKE,
      FRUIT, CREME_CARAMEL;
    }
    enum Coffee implements Food {
      BLACK_COFFEE, DECAF_COFFEE, ESPRESSO,
      LATTE, CAPPUCCINO, TEA, HERB_TEA;
    }
  }
}

枚举与单例模式

就目前我们知道的所有的不是枚举的单例模式,其实都不是很好, 他们都有这样或者那样的问题,虽然双重锁检查在一定程度上解决了问题,可是他依旧无法解决一个核心问题,反序列化。 是的,我们可以通过异常来解决这个问题:

public class Singleton implements java.io.Serializable {     
   public static Singleton INSTANCE = new Singleton();    
   private static volatile  boolean  flag = true;

   protected Singleton() {     
     if(flag){
        flag = false;   
        }else{
            throw new RuntimeException("The instance  already exists !");
    }
   }  

   //反序列时直接返回当前INSTANCE
   private Object readResolve() {     
            return INSTANCE;     
      }    
}   

如上所述,问题确实也得到了解决,但问题是我们为此付出了不少努力,即添加了不少代码,还应该注意到如果单例类维持了其他对象的状态时还需要使他们成为transient的对象,这种就更复杂了,那有没有更简单更高效的呢?

public enum  SingletonEnum {
    INSTANCE;
    private String name;
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
}

代码相当简洁,我们也可以像常规类一样编写enum类,为其添加变量和方法,访问方式也更简单,使用SingletonEnum.INSTANCE进行访问,这样也就避免调用getInstance方法。

更重要的是使用枚举单例的写法,我们完全不用考虑序列化和反射的问题。 枚举序列化是由jvm保证的,每一个枚举类型和定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定:在序列化时Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。 同时,编译器是不允许任何对这种序列化机制的定制的并禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,从而保证了枚举实例的唯一性,这里我们不妨再次看看Enum类的valueOf方法:

我们可以着重看一下这段代码:

public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        //这里判断Modifier.ENUM是不是枚举修饰符,如果是就抛异常
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

源码很了然,确实无法使用反射创建枚举实例,也就是说明了创建枚举实例只有编译器能够做到而已。显然枚举单例模式确实是很不错的选择,因此我们推荐使用它。