介绍Java枚举类的基本使用,并从字节码层面出发,深入剖析Java对于JDK5加入的枚举类的实现原理 定义一个简单的枚举类,其中包含若干枚举常量,示例如下:
public enum Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY,THURSDAY, FRIDAY, SATURDAY }Java 的 switch 语句参数支持使用枚举类
// day是Day类型变量switch (day) { case MONDAY: System.out.println("要开组会了好难受"); break; case THURSDAY: System.out.println("肯德基疯狂星期四"); break; case SATURDAY: case SUNDAY: System.out.println("不浪对不起周末"); break; default: System.out.println("摸摸鱼"); break;}其实这个用 static final 也可以,但是用枚举类的好处在于:
static final 的话,传入的变量就需要进行参数检查,而枚举类不用,因为肯定在枚举的范围中,或为 nullstatic final 不支持属性扩展,每个变量名对应一个值,而每一个枚举值可以拥有自己的多个属性(字段)枚举类可以添加属性及相应的构造器,以及方法,不过枚举类中的构造器默认也必须是 private 的。示例如下:
public enum Day { MONDAY(1, "星期一"), TUESDAY(2, "星期二"), // ...... SUNDAY(7, "星期日"); private int index; private String name; Day(int index, String name) { this.index = index; this.name = name; } // 针对index、name的getter方法 // ......}一般来说,不会为枚举类添加 setter 方法,因为这样会枚举一般只用来做常量,setter 会破坏它的常量特性
枚举类中的每个枚举常量都可以实现枚举主类定义的(abstract)方法,也可以拥有各自的内部方法,如下:
public enum Day { MONDAY(1, "星期一"){ @Override public Day getNext() { return TUESDAY; } }, TUESDAY(2, "星期二"){ @Override public Day getNext() { return WEDNESDAY; } }, // ...... SUNDAY(7, "星期日"){ @Override public Day getNext() { return MONDAY; } }; private int index; private String name; Day(int index, String name) { this.index = index; this.name = name; } // 在主类中定义抽象方法 public abstract Day getNext(); // 针对index、name的getter方法 // ......}虽然也可以在每个枚举常量中自定义任何方法,但是如果没有在主类中声明,就不能访问到,这个暂且按下不表,原因在后面会解释
所有枚举类都默认继承了 Enum 类,可以直接使用 Enum 提供的实用方法。由于 Java 只支持单继承,所以枚举类不能再继承别的父类,只能实现接口。一些使用的 Enum 类的方法,都贴在了文末_
枚举类的枚举常量全局唯一,不存在并发安全性问题,且不会被反射、序列化方式恶意创建新的枚举常量对象,很适合用来实现单例模式。这里可以参加博主的另一篇文章:单例模式的各种实现方式(Java)
最后再补充一点,博主发现某书和很多博客都说:在比较两个枚举类型的值时 , 永远不需要调用 equals 方法, 而直接使用 == 就可以了。但是我看了下 Enum 类中给到的 equals 源码(贴在下面),实际上用的也是 ==,我自己手动测试了也没问题。但不知道为什么,大家的博客上都这么写的,难道真就是人云亦云吗-_-||
public final boolean equals(Object other) { return this==other;}研究一个问题,最好是从现象出发去看本质,先知道有哪些现象,再看看它们的本质原因是什么。对于枚举类来说,它和普通类的不同之处就是现象:
Enum 类,不可以再继承其他类,且枚举类不可以被继承Enum 类中并没有 values() 和 valueOf(String) 方法,但是枚举类也可以调用abstract 方法,每个枚举常量都可以分别对其提供实现,不过它们也可以自定义其他方法。但只有在枚举主类定义过的方法,才能通过枚举常量调用到,否则不能先编写一段普通的枚举类作为示例,代码如下:
public enum Color { red("红", 0) { @Override public void print() { System.out.println(getName() + ":" + getIndex()); } }, green("绿", 1) { @Override public void print() { System.out.println(getName() + " " + getIndex()); } }; private String name; private int index; Color() { } Color(String name, int index) { this.name = name; this.index = index; } // 枚举主类中定义的抽象方法 public abstract void print(); // name、index的getterr方法}先对 Color.class 进行反编译(javap 不加参数 -v):
Compiled from "Color.java"public abstract class com.duyuzhou.enumTest.Color extends java.lang.Enum<com.duyuzhou.enumTest.Color> { public static final com.duyuzhou.enumTest.Color red; public static final com.duyuzhou.enumTest.Color green; public static com.duyuzhou.enumTest.Color[] values(); public static com.duyuzhou.enumTest.Color valueOf(java.lang.String); public java.lang.String getName(); public int getIndex(); public abstract void print(); public static void main(java.lang.String[]); com.duyuzhou.enumTest.Color(java.lang.String, int, java.lang.String, int, com.duyuzhou.enumTest.Color$1); static {};}仔细分析,就可以得出以下结论:
Enum 类,以及 Enum 类的各种方法。编译器为枚举类添加了 final 关键字,使得枚举类不能被其他类继承static 方法:values() 和 valueOf(String)abstract 类,因此枚举类可以定义抽象方法private,因此外界不可以使用枚举类创建对象static final 属性,那么在类加载的初始化阶段就会将所有枚举常量创建好,而且只会创建一次。final 也能保证枚举常量一旦被创建好后,对于所有线程都是可见的,不会存在线程安全问题如果反编译加上 -v 参数,可以看到 Color 有两个静态内部类,分别是 Color$1 和 Color$2,对其中一个进行反编译(不加 -v):
Compiled from "Color.java"final class com.duyuzhou.enumTest.Color$1 extends com.duyuzhou.enumTest.Color { com.duyuzhou.enumTest.Color$1(java.lang.String, int, java.lang.String, int); public void print();}其实每个静态内部类都对应了一个枚举常量,这些静态内部类都继承了枚举主类,所以枚举常量中可以实现主类中定义的 abstract 方法。而且,在枚举主类中,每个枚举常量都变成了一个枚举主类类型的字段,因此外界不可能调用一个枚举常量中私自定义但枚举主类中没定义的方法
此外,由于每个枚举常量都是不同枚举子类的一个对象,所以它们各自继承了父类定义的字段,且观察枚举常量的反编译结果会发现,编译器为每个枚举子类都添加了一个构造函数,所以枚举主类中定义的字段是在各个枚举常量中分开赋值的
至此,上面的所有“现象”,都通过反编译查看字节码的方式得到了解答,本质上是编译器帮我们做好了幕后工作,所以才有了这些代码中看不到却实际存在的规则。不过,还遗留了一个小问题——在 Color.class 中,为什么编译器会为 Color 的构造器额外添加两个方法参数:String 和 int 型呢?
来看看 Color 的构造器 com.duyuzhou.enumTest.Color(java.lang.String, int, java.lang.String, int, com.duyuzhou.enumTest.Color$1) 反编译出的字节码:
0 aload_01 aload_12 iload_23 aload_34 invokespecial #2 <enumtest/Color.<init> : (Ljava/lang/String;ILjava/lang/String;)V>7 return这里会调用 Color.<init> 方法,该方法的字节码需要借助 Jclasslib 工具查看,如下:
0 aload_01 aload_12 iload_23 invokespecial #9 <java/lang/Enum.<init> : (Ljava/lang/String;I)V>6 return看到这里应该就能懂了,这里实际上会将额外添加的两个方法参数传递给父类 Enum 的构造器,那么看一下 Enum 中接收一个 String 和一个 int 型的构造器是怎样的:
protected Enum(String name, int ordinal) { this.name = name; this.ordinal = ordinal;}如果读者想进一步刨根问底,可以研究一下传递给 name 和 ordinal 的值是什么。这里先给出答案:因为调用构造函数创建的枚举常量,是由 static final 修饰的,所以调用的时机发生在类加载的初始化阶段,这时编译器会按顺序收集所有 static 赋值语句和 static 块,生成一个 <clinit> 方法,然后去执行这个方法。所以,name 和 ordinal 参数可以在该方法中分析字节码找到。name 实际上就是枚举常量名,ordinal 就是枚举常量在枚举类中声明的位置,从0开始计数。记录这两个参数是为了方便 Enum 中 toString、ordinal、compareTo 等方法的调用
能够看到这里,想必也就明白了枚举类这个 JDK5 才加入的新特性,就是一颗“语法糖”罢了。为了保持向后兼容性,Java 编译器做了很多幕后工作。根据这样的思路,我们也可以探究一下其他 Java 语法糖是如何实现的,比如 forEach 方法、自动装箱/拆箱、泛型为什么会擦除类型等
最后总结一下比较实用的 Enum 类提供的方法,和编译器为每个枚举类自动添加的两个方法(values() 和 valueOf(String))
Enum 类提供的方法:
String name():等同于 toString()int ordinal():返回枚举常量在枚举类中声明的位置,从0开始计数String toString():返回枚举常量名int compareTo(E other):比较两个枚举常量声明的位置谁更靠前,其实靠的就是比较 ordinal 的大小static Enum valueOf(Class clz, String name):根据枚举类和枚举常量名,返回特定的枚举常量编译器为每个枚举类自动添加的方法:
Enum[] values():返回枚举常量数组,实际上编译器在 <clinit> 中创建各个枚举常量时,也会创建一个字段 $VALUES,其中就保存了这个数组Enum valueOf(String name):根据枚举常量名,返回该枚举类中特定的枚举常量,内部调用的就是 Enum 的 valueOf 方法