Java中的枚举类型(Enum)详解

885 阅读3分钟

枚举类型初探

枚举类型,其语法总让人觉着怪怪的,如下:

public enum Season {
    SPRING, SUMMER, AUTUMN, WINTER
}

简单的一行,就定义了包含四个值的枚举类型,缺总让人觉着语法有点怪异。而在使用时:

public void test() {
    System.out.println("spring name:" + Season.SPRING.name());
    System.out.println("spring ordinal:" + Season.SPRING.ordinal());
    System.out.println("autumn name:" + Season.AUTUMN.name());
    System.out.println("autumn ordinal:" + Season.AUTUMN.ordinal());
}

就可以得到输出:

spring name:SPRING
spring ordinal:0
autumn name:AUTUMN
autumn ordinal:2

那我们简单的一行定义中,到底发生了什么?枚举类型在编译时是怎样实现的?它还有着怎样的特点?

我们将会分几节对Java中的枚举类型进行介绍。

枚举产生之前

如果不使用枚举,我们要对“春夏秋冬”这四个值分别赋予一个数字,则常见的操作为:

public class Season {
    public static final int SPRING = 0;
    public static final int SUMMER = 1;
    public static final int AUTUMN = 2;
    public static final int WINTER = 3;
}

上述方法定义十分繁琐,而且容易出错。例如我们定义的int数字出现重复,编译器也不会给出任何的警示。同时,这样的操作是实在太频繁了,最终Java 5中增加了枚举类型。

而是用枚举类型后,一切就变成了如下所示的简单几行:

public enum Season {
    SPRING, SUMMER, AUTUMN, WINTER
}

而且,Java自动给按照枚举值出现的顺序,从0开始分配了编号。通过name()可以获得枚举值的名称,通过ordinal()可以获得枚举值的编号。

枚举实现原理

那我们定义枚举类型后,到底发生了什么呢?我们对枚举的实现原理进行探究。

首先,我们在实现Season枚举类时,并没有定义name()和ordinal()方法。我们从这里入手,点击该方法后,发现进入了一个抽象类:

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {
        }

并且,我们发现编译器不允许我们自行实现该抽象类从而构造一个新的类。但是,既然我们的Season枚举类可以调用其中的方法,因此Season枚举类应该是继承了该抽象类。为了验证这一猜想,我们让Season类继承一个其他的类,发现果然不可以,因为Java是不允许多继承的。

具体,我们对Season类进行反编译,即:java -p Season.class,得到反编译后的结果:

public final class com.test.constant.Season extends
    java.lang.Enum<com.test.constant.Season> {
  public static final com.test.constant.Season SPRING;
  public static final com.test.constant.Season SUMMER;
  public static final com.test.constant.Season AUTUMN;
  public static final com.test.constant.Season WINTER;
  private static final com.test.constant.Season[] $VALUES;
  public static com.test.constant.Season[] values();
  public static com.test.constant.Season valueOf(java.lang.String);
  private com.test.constant.Season();
  static {};
}

我们看到,对与枚举类,有很多值的注意的点:

  • 枚举类在经过编译后确实是生成了一个扩展了java.lang.Enum的类
  • 枚举类是final的,因此我们无法再继承它了
  • 我们定义的每个枚举值都是该类中的一个成员,且成员的类型仍然是Season类型
  • 枚举类中被默认增加了许多静态方法,例如values()等

为了进一步了解每个方法中的操作,我们使用java -p -c Season.class每个方法中的字节码:

public final class com.test.constant.Season extends java
.lang.Enum<com.test.constant.Season> {
  public static final com.test.constant.Season SPRING;
  public static final com.test.constant.Season SUMMER;
  public static final com.test.constant.Season AUTUMN;
  public static final com.test.constant.Season WINTER;

  private static final com.test.constant.Season[] $VALUES;

  public static com.test.constant.Season[] values();
    Code:
       0: getstatic     #1                  // Field $VALUES:[Lcom/test/constant/Season;
       3: invokevirtual #2                  // Method "[Lcom/test/constant/Season;".clone:()Ljava/lang/Object;
       6: checkcast     #3                  // class "[Lcom/test/constant/Season;"
       9: areturn

  public static com.test.constant.Season valueOf(java.lang.String);
    Code:
       0: ldc           #4                  // class com/test/constant/Season
       2: aload_0
       3: invokestatic  #5                  // Method java/lang/Enum.valueOf:
       (Ljava/lang/Class;Ljava/lang/String;)Ljavalang/Enum;
       6: checkcast     #4                  // class com/test/constant/Season
       9: areturn

  // 省略一些方法的字节码
}

根据字节码,我们还原其操作代码,大致如下:

注意:以下代码为笔者自己翻译,如果有好的字节码查看工具一定要告诉我啊!自己转太麻烦了。

public final class com.test.constant.Season extends java
.lang.Enum<com.test.constant.Season> {
  public static final com.test.constant.Season SPRING;
  public static final com.test.constant.Season SUMMER;
  public static final com.test.constant.Season AUTUMN;
  public static final com.test.constant.Season WINTER;

  private static final com.test.constant.Season[] $VALUES;

  public static com.test.constant.Season[] values(){
        return (Season[])$VALUES.clone();
  }

  public static com.test.constant.Season valueOf(String s){
        return (Season)Enum.valueOf(java/lang/String, s);
    }

  private com.test.constant.Season(String s, int i){
    super(s,i);
  }

  static {
    SPRING = new Season("SPRING", 0);
    SUMMER = new Season("SUMMER", 1);
    AUTUMN = new Season("AUTUMN", 2);
    WINTER = new Season("WINTER", 3);
        $VALUES = (new Season[] {
            SPRING, SUMMER, AUTUMN, WINTER
        });
    }
}

通过这里我们可以看到,在类的static操作中,编译器帮助我们生成每个枚举值的对象。

总结

我们在总结一下,我们使用enum定义的枚举类型,会在编译之后转化为一个继承了java .lang.Enum的类,而我们定义的每个枚举值都会在类的初始化阶段被实例化为我们所定义的枚举类的一个对象。同时,编译器还帮我们在类中增加了两个方法,分别是:values()和valueOf()。

至此,我们对Java的枚举对象有了彻底的认识。

但是,我们依旧有着很多的疑惑:

  • 编译器插入的方法和正常的方法有无不同
  • 枚举类型还有哪些使用上的注意点,例如相关方法和操作
  • 枚举类型的实现上还有哪些特殊之处

这些,我们在接下来的文章中继续分析。

最后,我是架构师易哥,欢迎关注我,我会偶尔出没分享软件架构和编程相关的干货知识。