java枚举类型的实现原理

来源:互联网 发布:cd设计软件 编辑:程序博客网 时间:2024/05/22 03:24

Java从JDK1.5开始支持枚举,也就是说,Java一开始是不支持枚举的,就像泛型一样,都是JDK1.5才加入的新特性。通常一个特性如果在一开始没有提供,在语言发展后期才添加,会遇到一个问题,就是向后兼容性的问题。像Java在1.5中引入的很多特性,为了向后兼容,编译器会帮我们写的源代码做很多事情,比如泛型为什么会擦除类型,为什么会生成桥接方法,foreach迭代,自动装箱/拆箱等,这有个术语叫“语法糖”,而编译器的特殊处理叫“解语法糖”。那么像枚举也是在JDK1.5中才引入的,又是怎么实现的呢?

Java在1.5中添加了java.lang.Enum抽象类,它是所有枚举类型基类。提供了一些基础属性和基础方法。同时,当对把枚举用作Set和Map也提供了支持,即java.util.EnumSet和java.util.EnumMap。

如何定义枚举类型

比如表示加减乘除操作,我们可以定义如下枚举:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. package com.mikan;  
  2.   
  3. /** 
  4.  * @author Mikan 
  5.  * @date 2015-08-29 12:06 
  6.  */  
  7. public enum Operator {  
  8.   
  9.     ADD,  
  10.     SUBTRACT,  
  11.     MULTIPLY,  
  12.     DIVIDE  
  13.   
  14. }  
上面的枚举定义了四个枚举常量,同时,在枚举中还可以定义普通方法、抽象方法,如下所示:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. package com.mikan;  
  2.   
  3. /** 
  4.  * @author Mikan 
  5.  * @date 2015-08-29 12:06 
  6.  */  
  7. public enum Operator {  
  8.   
  9.     ADD {  
  10.         @Override  
  11.         public int calculate(int a, int b) {  
  12.             return a + b;  
  13.         }  
  14.     },  
  15.     SUBTRACT {  
  16.         @Override  
  17.         public int calculate(int a, int b) {  
  18.             return a - b;  
  19.         }  
  20.     },  
  21.     MULTIPLY {  
  22.         @Override  
  23.         public int calculate(int a, int b) {  
  24.             return a * b;  
  25.         }  
  26.     },  
  27.     DIVIDE {  
  28.         @Override  
  29.         public int calculate(int a, int b) {  
  30.             if (b == 0) {  
  31.                 throw new IllegalArgumentException("divisor must not be 0");  
  32.             }  
  33.             return a / b;  
  34.         }  
  35.     };  
  36.   
  37.     public abstract int calculate(int a, int b);  
  38.   
  39. }  
从上面可以看到,我们基本可以像定义类一样来定义枚举。我们还可以定义属性、构造方法等:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. package com.mikan;  
  2.   
  3. /** 
  4.  * @author Mikan 
  5.  * @date 2015-08-29 12:06 
  6.  */  
  7. public enum Operator {  
  8.   
  9.     ADD ("+") {  
  10.         @Override  
  11.         public int calculate(int a, int b) {  
  12.             return a + b;  
  13.         }  
  14.     },  
  15.     SUBTRACT ("-") {  
  16.         @Override  
  17.         public int calculate(int a, int b) {  
  18.             return a - b;  
  19.         }  
  20.     },  
  21.     MULTIPLY  ("*") {  
  22.         @Override  
  23.         public int calculate(int a, int b) {  
  24.             return a * b;  
  25.         }  
  26.     },  
  27.     DIVIDE ("/") {  
  28.         @Override  
  29.         public int calculate(int a, int b) {  
  30.             if (b == 0) {  
  31.                 throw new IllegalArgumentException("divisor must not be 0");  
  32.             }  
  33.             return a / b;  
  34.         }  
  35.     };  
  36.   
  37.     Operator (String operator) {  
  38.         this.operator = operator;  
  39.     }  
  40.   
  41.     private String operator;  
  42.   
  43.     public abstract int calculate(int a, int b);  
  44.   
  45.     public String getOperator() {  
  46.         return operator;  
  47.     }  
  48.   
  49. }  

实现原理分析

既然可以像使用普通的类一样使用枚举,编译器究竟为我们做了些什么事呢?要想知道这其中的秘密,最有效的途径就是查看生成的字节码。下面就来看看上面定义的枚举生成的字节码是怎么样的。

首先来看看反编译的基本信息:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. localhost:mikan mikan$ javap Operator.class  
  2. Compiled from "Operator.java"  
  3. public abstract class com.mikan.Operator extends java.lang.Enum<com.mikan.Operator> {  
  4.   public static final com.mikan.Operator ADD;  
  5.   public static final com.mikan.Operator SUBTRACT;  
  6.   public static final com.mikan.Operator MULTIPLY;  
  7.   public static final com.mikan.Operator DIVIDE;  
  8.   public static com.mikan.Operator[] values();  
  9.   public static com.mikan.Operator valueOf(java.lang.String);  
  10.   public abstract int calculate(intint);  
  11.   public java.lang.String getOperator();  
  12.   com.mikan.Operator(java.lang.String, int, java.lang.String, com.mikan.Operator$1);  
  13.   static {};  
  14. }  
可以看到,一个枚举在经过编译器编译过后,变成了一个抽象类,它继承了java.lang.Enum;而枚举中定义的枚举常量,变成了相应的public static final属性,而且其类型就抽象类的类型,名字就是枚举常量的名字,同时我们可以在Operator.class的相同路径下看到四个内部类的.class文件com/mikan/Operator$1.class、com/mikan/Operator$2.class、com/mikan/Operator$3.class、com/mikan/Operator$4.class,也就是说这四个命名字段分别使用了内部类来实现的;同时添加了两个方法values()和valueOf(String);我们定义的构造方法本来只有一个参数,但却变成了三个参数;同时还生成了一个静态代码块。这些具体的内容接下来仔细看看。

看下面详细的反编译信息:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. localhost:mikan mikan$ javap -c -v Operator.class  
  2. Classfile /Users/mikan/Documents/workspace/project/algorithm/target/classes/com/mikan/Operator.class  
  3.   Last modified 2015-8-29; size 1720 bytes  
  4.   MD5 checksum 478439554cb827fec3c36cf51c8d36da  
  5.   Compiled from "Operator.java"  
  6. public abstract class com.mikan.Operator extends java.lang.Enum<com.mikan.Operator>  
  7.   Signature: #67                          // Ljava/lang/Enum<Lcom/mikan/Operator;>;  
  8.   SourceFile: "Operator.java"  
  9.   InnerClasses:  
  10.        static #24//class com/mikan/Operator$4  
  11.        static #19//class com/mikan/Operator$3  
  12.        static #14//class com/mikan/Operator$2  
  13.        static #9//class com/mikan/Operator$1  
  14.   minor version: 0  
  15.   major version: 51  
  16.   flags: ACC_PUBLIC, ACC_SUPER, ACC_ABSTRACT, ACC_ENUM  
  17. Constant pool:  
  18.   // 省略常量池信息  
  19. {  
  20.   public static final com.mikan.Operator ADD;  
  21.     flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM  
  22.   public static final com.mikan.Operator SUBTRACT;  
  23.     flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM  
  24.   public static final com.mikan.Operator MULTIPLY;  
  25.     flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM  
  26.   public static final com.mikan.Operator DIVIDE;  
  27.     flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM  
  28.   public static com.mikan.Operator[] values();  
  29.     flags: ACC_PUBLIC, ACC_STATIC  
  30.     Code:  
  31.       stack=1, locals=0, args_size=0  
  32.          0: getstatic     #2                  // Field $VALUES:[Lcom/mikan/Operator;  
  33.          3: invokevirtual #3                  // Method "[Lcom/mikan/Operator;".clone:()Ljava/lang/Object;  
  34.          6: checkcast     #4                  // class "[Lcom/mikan/Operator;"  
  35.          9: areturn  
  36.       LineNumberTable:  
  37.         line 70  
  38.   
  39.   public static com.mikan.Operator valueOf(java.lang.String);  
  40.     flags: ACC_PUBLIC, ACC_STATIC  
  41.     Code:  
  42.       stack=2, locals=1, args_size=1  
  43.          0: ldc_w         #5                  // class com/mikan/Operator  
  44.          3: aload_0  
  45.          4: invokestatic  #6                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;  
  46.          7: checkcast     #5                  // class com/mikan/Operator  
  47.         10: areturn  
  48.       LineNumberTable:  
  49.         line 70  
  50.       LocalVariableTable:  
  51.         Start  Length  Slot  Name   Signature  
  52.                0      11     0  name   Ljava/lang/String;  
  53.   
  54.   public abstract int calculate(intint);  
  55.     flags: ACC_PUBLIC, ACC_ABSTRACT  
  56.   
  57.   public java.lang.String getOperator();  
  58.     flags: ACC_PUBLIC  
  59.     Code:  
  60.       stack=1, locals=1, args_size=1  
  61.          0: aload_0  
  62.          1: getfield      #8                  // Field operator:Ljava/lang/String;  
  63.          4: areturn  
  64.       LineNumberTable:  
  65.         line 460  
  66.       LocalVariableTable:  
  67.         Start  Length  Slot  Name   Signature  
  68.                0       5     0  this   Lcom/mikan/Operator;  
  69.   
  70.   com.mikan.Operator(java.lang.String, int, java.lang.String, com.mikan.Operator$1);  
  71.     flags: ACC_SYNTHETIC  
  72.     Code:  
  73.       stack=4, locals=5, args_size=5  
  74.          0: aload_0  
  75.          1: aload_1  
  76.          2: iload_2  
  77.          3: aload_3  
  78.          4: invokespecial #1                  // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V  
  79.          7return  
  80.       LineNumberTable:  
  81.         line 70  
  82.       LocalVariableTable:  
  83.         Start  Length  Slot  Name   Signature  
  84.                0       8     0  this   Lcom/mikan/Operator;  
  85.                0       8     1    x0   Ljava/lang/String;  
  86.                0       8     2    x1   I  
  87.                0       8     3    x2   Ljava/lang/String;  
  88.                0       8     4    x3   Lcom/mikan/Operator$1;  
  89.   
  90.   static {};  
  91.     flags: ACC_STATIC  
  92.     Code:  
  93.       stack=5, locals=0, args_size=0  
  94.          0new           #9                  // class com/mikan/Operator$1  
  95.          3: dup  
  96.          4: ldc           #10                 // String ADD  
  97.          6: iconst_0  
  98.          7: ldc           #11                 // String +  
  99.          9: invokespecial #12                 // Method com/mikan/Operator$1."<init>":(Ljava/lang/String;ILjava/lang/String;)V  
  100.         12: putstatic     #13                 // Field ADD:Lcom/mikan/Operator;  
  101.         15new           #14                 // class com/mikan/Operator$2  
  102.         18: dup  
  103.         19: ldc           #15                 // String SUBTRACT  
  104.         21: iconst_1  
  105.         22: ldc           #16                 // String -  
  106.         24: invokespecial #17                 // Method com/mikan/Operator$2."<init>":(Ljava/lang/String;ILjava/lang/String;)V  
  107.         27: putstatic     #18                 // Field SUBTRACT:Lcom/mikan/Operator;  
  108.         30new           #19                 // class com/mikan/Operator$3  
  109.         33: dup  
  110.         34: ldc           #20                 // String MULTIPLY  
  111.         36: iconst_2  
  112.         37: ldc           #21                 // String *  
  113.         39: invokespecial #22                 // Method com/mikan/Operator$3."<init>":(Ljava/lang/String;ILjava/lang/String;)V  
  114.         42: putstatic     #23                 // Field MULTIPLY:Lcom/mikan/Operator;  
  115.         45new           #24                 // class com/mikan/Operator$4  
  116.         48: dup  
  117.         49: ldc           #25                 // String DIVIDE  
  118.         51: iconst_3  
  119.         52: ldc           #26                 // String /  
  120.         54: invokespecial #27                 // Method com/mikan/Operator$4."<init>":(Ljava/lang/String;ILjava/lang/String;)V  
  121.         57: putstatic     #28                 // Field DIVIDE:Lcom/mikan/Operator;  
  122.         60: iconst_4  
  123.         61: anewarray     #5                  // class com/mikan/Operator  
  124.         64: dup  
  125.         65: iconst_0  
  126.         66: getstatic     #13                 // Field ADD:Lcom/mikan/Operator;  
  127.         69: aastore  
  128.         70: dup  
  129.         71: iconst_1  
  130.         72: getstatic     #18                 // Field SUBTRACT:Lcom/mikan/Operator;  
  131.         75: aastore  
  132.         76: dup  
  133.         77: iconst_2  
  134.         78: getstatic     #23                 // Field MULTIPLY:Lcom/mikan/Operator;  
  135.         81: aastore  
  136.         82: dup  
  137.         83: iconst_3  
  138.         84: getstatic     #28                 // Field DIVIDE:Lcom/mikan/Operator;  
  139.         87: aastore  
  140.         88: putstatic     #2                  // Field $VALUES:[Lcom/mikan/Operator;  
  141.         91return  
  142.       LineNumberTable:  
  143.         line 90  
  144.         line 1515  
  145.         line 2130  
  146.         line 2745  
  147.         line 760  
  148. }  
  149. localhost:mikan mikan$  
下面分析一下字节码中的各部分,其中:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. InnerClasses:  
  2.      static #24//class com/mikan/Operator$4  
  3.      static #19//class com/mikan/Operator$3  
  4.      static #14//class com/mikan/Operator$2  
  5.      static #9//class com/mikan/Operator$1  
从中可以看到它有4个内部类,这四个内部类的详细信息后面会分析。

静态代码块:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1.   static {};  
  2.     flags: ACC_STATIC  
  3.     Code:  
  4.       stack=5, locals=0, args_size=0  
  5. // 创建一个Operator$1的内部类对象  
  6.          0new           #9                  // class com/mikan/Operator$1  
  7.          3: dup  
  8. // 接下来的三条指令分别是把三个参数推送到栈顶,然后调用Operator$1的编译器生成的<init>方法  
  9.          4: ldc           #10                 // String ADD  
  10.          6: iconst_0  
  11.          7: ldc           #11                 // String +  
  12. // 调用<init>方法  
  13.          9: invokespecial #12                 // Method com/mikan/Operator$1."<init>":(Ljava/lang/String;ILjava/lang/String;)V  
  14. // 设置ADD属性的值为新创建的对象  
  15.         12: putstatic     #13                 // Field ADD:Lcom/mikan/Operator;  
  16. // 接下来是分别初始化另外三个属性SUBTRACT、MULTIPLY、DIVIDE,这里就不再重复  
  17.         15new           #14                 // class com/mikan/Operator$2  
  18.         18: dup  
  19.         19: ldc           #15                 // String SUBTRACT  
  20.         21: iconst_1  
  21.         22: ldc           #16                 // String -  
  22.         24: invokespecial #17                 // Method com/mikan/Operator$2."<init>":(Ljava/lang/String;ILjava/lang/String;)V  
  23.         27: putstatic     #18                 // Field SUBTRACT:Lcom/mikan/Operator;  
  24.         30new           #19                 // class com/mikan/Operator$3  
  25.         33: dup  
  26.         34: ldc           #20                 // String MULTIPLY  
  27.         36: iconst_2  
  28.         37: ldc           #21                 // String *  
  29.         39: invokespecial #22                 // Method com/mikan/Operator$3."<init>":(Ljava/lang/String;ILjava/lang/String;)V  
  30.         42: putstatic     #23                 // Field MULTIPLY:Lcom/mikan/Operator;  
  31.         45new           #24                 // class com/mikan/Operator$4  
  32.         48: dup  
  33.         49: ldc           #25                 // String DIVIDE  
  34.         51: iconst_3  
  35.         52: ldc           #26                 // String /  
  36.         54: invokespecial #27                 // Method com/mikan/Operator$4."<init>":(Ljava/lang/String;ILjava/lang/String;)V  
  37.         57: putstatic     #28                 // Field DIVIDE:Lcom/mikan/Operator;  
  38. // 下面是new了一个长度为4的Operator类型的数组,并分别设置数组中各元素的值为上面的四个属性的值  
  39.         60: iconst_4  
  40.         61: anewarray     #5                  // class com/mikan/Operator  
  41.         64: dup  
  42.         65: iconst_0  
  43.         66: getstatic     #13                 // Field ADD:Lcom/mikan/Operator;  
  44.         69: aastore  
  45.         70: dup  
  46.         71: iconst_1  
  47.         72: getstatic     #18                 // Field SUBTRACT:Lcom/mikan/Operator;  
  48.         75: aastore  
  49.         76: dup  
  50.         77: iconst_2  
  51.         78: getstatic     #23                 // Field MULTIPLY:Lcom/mikan/Operator;  
  52.         81: aastore  
  53.         82: dup  
  54.         83: iconst_3  
  55.         84: getstatic     #28                 // Field DIVIDE:Lcom/mikan/Operator;  
  56.         87: aastore  
  57. //下面是设置属性$VALUES的值为刚创建的数组  
  58.         88: putstatic     #2                  // Field $VALUES:[Lcom/mikan/Operator;  
  59.         91return  
其实编译器生成的这个静态代码块做了如下工作:分别设置生成的四个公共静态常量字段的值,同时编译器还生成了一个静态字段$VALUES,保存的是枚举类型定义的所有枚举常量。相当于下面的代码:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. Operator ADD = new Operator1();  
  2. Operator SUBTRACT = new Operator1();  
  3. Operator MULTIPLY = new Operator1();  
  4. Operator DIVIDE = new Operator1();  
  5. Operator[] $VALUES = new Operator[4];  
  6. $VALUES[0] = ADD;  
  7. $VALUES[0] = SUBTRACT;  
  8. $VALUES[0] = MULTIPLY;  
  9. $VALUES[0] = DIVIDE;  
编译器添加的values方法:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public static com.mikan.Operator[] values();  
  2.   flags: ACC_PUBLIC, ACC_STATIC  
  3.   Code:  
  4.     stack=1, locals=0, args_size=0  
  5.        0: getstatic     #2                  // Field $VALUES:[Lcom/mikan/Operator;  
  6.        3: invokevirtual #3                  // Method "[Lcom/mikan/Operator;".clone:()Ljava/lang/Object;  
  7.        6: checkcast     #4                  // class "[Lcom/mikan/Operator;"  
  8.        9: areturn  
这个方法是一个公共的静态方法,所以我们可以直接调用该方法(Operator.values()),返回这个枚举值的数组,另外,这个方法的实现是,克隆在静态代码块中初始化的$VALUES字段的值,并把类型强转成Operator[]类型返回。它相当于下面的代码:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public static com.mikan.Operator[] values() {  
  2. return (Operator[])$VALUES.clone();  
  3. }  
编译器添加的valueOf方法:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public static com.mikan.Operator valueOf(java.lang.String);  
  2.   flags: ACC_PUBLIC, ACC_STATIC  
  3.   Code:  
  4.     stack=2, locals=1, args_size=1  
  5.        0: ldc_w         #5                  // class com/mikan/Operator  
  6.        3: aload_0  
  7.        4: invokestatic  #6                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;  
  8.        7: checkcast     #5                  // class com/mikan/Operator  
  9.       10: areturn  
这个方法是一个公共的静态方法,所以我们可以直接调用该方法(Operator.valueOf()),返回参数字符串表示的枚举常量,另外,这个方法的实现是,调用父类Enum的valueOf方法,并把类型强转成Operator。它相当于如下的代码:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public static com.mikan.Operator valueOf(String name) {  
  2. return (Operator)Enum.valueOf(Operator.class, name);  
  3. }  
生成的内部类

下面看看生成的内部类Operator$1:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. localhost:mikan mikan$ javap Operator\$1.class  
  2. Compiled from "Operator.java"  
  3. final class com.mikan.Operator$1 extends com.mikan.Operator {  
  4.   com.mikan.Operator$1(java.lang.String, int, java.lang.String);  
  5.   public int calculate(intint);  
  6. }  
  7. localhost:mikan mikan$  
可以看到,实现内部类是继承自Operator,即

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. ADD {  
  2.     @Override  
  3.     public int calculate(int a, int b) {  
  4.         return a + b;  
  5.     }  
  6. },  
这就是说,我们定义的每个枚举常量,最终都生成了一个像上面这样的内部类。

构造方法为什么增加了两个方法?

有一个问题,构造方法我们明明只定义了一个参数,为什么生成的构造方法是三个参数呢?

从Enum类中我们可以看到,为每个枚举都定义了两个属性,name和ordinal,name表示我们定义的枚举常量的名称,如ADD、SUBTRACT等,而ordinal是一个顺序号,根据定义的顺序分别赋予一个整形值,从0开始。在枚举常量初始化时,会自动为初始化这两个字段,设置相应的值,所以才在构造方法中添加了两个参数。即:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. com.mikan.Operator$1(String name, int ordinal, String operator);  
另外三个枚举常量生成的内部类基本上差不多,这里就不重复说明了。

我们可以从Enum类的代码中看到,定义的name和ordinal属性都是final的,而且大部分方法也都是final的,特别是clone、readObject、writeObject这三个方法,这三个方法和枚举通过静态代码块来进行初始化一起,它保证了枚举类型的不可变性,不能通过克隆,不能通过序列化和反序列化来复制枚举,这能保证一个枚举常量只是一个实例,即是单例的,所以在effective java中推荐使用枚举来实现单例。

总结

枚举本质上是通过普通的类来实现的,只是编译器为我们进行了处理。每个枚举类型都继承自java.lang.Enum,并自动添加了values和valueOf方法。而每个枚举常量是一个静态常量字段,使用内部类实现,该内部类继承了枚举类。所有枚举常量都通过静态代码块来进行初始化,即在类加载期间就初始化。另外通过把clone、readObject、writeObject这三个方法定义为final的,同时实现是抛出相应的异常。这样保证了每个枚举类型及枚举常量都是不可变的。可以利用枚举的这两个特性来实现线程安全的单例。

1 0
原创粉丝点击