Java Enum 详解

   Enum(“枚举”)全称为 enumeration, 是 JDK 1.5  中引入的新特性,在Java开发者眼中枚举已经是再普通不过的一种类型了,可以说任何Java程序员都编写过自己的枚举类,一般这些枚举类用来表示同一类属性,如: 

Java代码   收藏代码
  1. /** 
  2.  * @Description 操作系统类型 
  3.  * @author ming.li 
  4.  * @Date 2018年4月16日 下午2:33:14 
  5.  */  
  6. public enum OSType {  
  7.           Linux, MacOSX, Windows, iOS, Android, DOS;  
  8. }  

        以上是一个表示操作系统类型的枚举,在使用时我们只需要选择其中某种类型即可,如:OSType.Windows,这样既简单又方便。

        枚举的取值仅限于已定义的所有类型,以OSType为例,也就是说我们只能选择OSType中定义的这些操作系统类型,即使出现新的类型我们也无法使用,除非更新OSType。这种特性一方面可以理解为局限性,另一方面也可理解为限制性,限制使用者的目标选择。

        虽然枚举我们经常会用到,但是,往往我们越是熟悉,越是觉得普通,平时根本不注意的事物,就越是不简单。所以今天我们就一步步的去了解这个经常使用却并不熟悉的“枚举”。

 

        1.枚举的父类

        定义一个新枚举我们只需要声明“enum”关键字即可,虽说在开发过程中枚举是一种类型,但最终“枚举”会被编译成一个class,通过javap 反编译命令就可以证明这一点,在此之前我们来看一下javap 命令的使用说明:

Java代码   收藏代码
  1. javap -help  
  2. 用法: javap <options> <classes>  
  3. 其中, 可能的选项包括:  
  4.   -help  --help  -?        输出此用法消息  
  5.   -version                 版本信息  
  6.   -v  -verbose             输出附加信息  
  7.   -l                       输出行号和本地变量表  
  8.   -public                  仅显示公共类和成员  
  9.   -protected               显示受保护的/公共类和成员  
  10.   -package                 显示程序包/受保护的/公共类和成员 (默认)  
  11.   -p  -private             显示所有类和成员  
  12.   -c                       对代码进行反汇编  
  13.   -s                       输出内部类型签名  
  14.   -sysinfo                 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列)  
  15.   -constants               显示最终常量  
  16.   -classpath <path>        指定查找用户类文件的位置  
  17.   -cp <path>               指定查找用户类文件的位置  
  18.   -bootclasspath <path>    覆盖引导类文件的位置  

        于是,我们使用javap反编译上面的OSType枚举,得到以下结果:

Java代码   收藏代码
  1. javap OSType.class  
  2. Compiled from "OSType.java"  
  3. public final class OSType extends java.lang.Enum<OSType> {  
  4.   public static final OSType Linux;  
  5.   public static final OSType MacOSX;  
  6.   public static final OSType Windows;  
  7.   public static final OSType iOS;  
  8.   public static final OSType Android;  
  9.   public static final OSType DOS;  
  10.   public static OSType[] values();  
  11.   public static OSType valueOf(java.lang.String);  
  12.   static {};  
  13. }  

        通过控制台输出的反编译结果可以看到,OSType 枚举被编译成了一个public final class OSType  并继承自java.lang.Enum<OSType>,指定其泛型为OSType 。

        所以,可以得知我们使用枚举其实跟使用一个常量类并没有什么不同,但为什么还要创造这个类型呢?带着疑问我们进一步学习和分析。

        虽然编译后Enum 类是OSType 枚举的父类,但在编写代码过程中我们却无法使用extends 关键字令枚举去继承任何类,否则编译器直接会给出错误提示。

        反编译结果中类OSType继承了java.lang.Enum这个类,那么我们就先打开java.lang.Enum 的源代码来看一看,以下就是java.lang.Enum 的源代码(JDK1.8版本,代码中我已经将说明加上,有利于大家理解):

Java代码   收藏代码
  1. package java.lang;  
  2.   
  3. import java.io.Serializable;  
  4. import java.io.IOException;  
  5. import java.io.InvalidObjectException;  
  6. import java.io.ObjectInputStream;  
  7. import java.io.ObjectStreamException;  
  8.   
  9. /** 
  10.  * 这是所有 Java 语言枚举类型的公共基本类 
  11.  * @param <E> 枚举类型的子类 
  12.  * @since   1.5 
  13.  */  
  14. public abstract class Enum<E extends Enum<E>>  
  15.         implements Comparable<E>, Serializable {  
  16.     /** 
  17.      * 枚举名称 
  18.      */  
  19.     private final String name;  
  20.   
  21.     /** 
  22.      * 返回此枚举常量的名称,在其枚举声明中对其进行声明。 与此方法相比,大多数程序员应该优先考虑使用 toString() 方法,因为 toString 方法返回更加用户友好的名称。该方法主要设计用于特殊情形,其正确性取决于获取正确的名称,其名称不会随版本的改变而改变。 
  23.      */  
  24.     public final String name() {  
  25.         return name;  
  26.     }  
  27.   
  28.     /** 
  29.      * 枚举顺序,初始值为0 
  30.      */  
  31.     private final int ordinal;  
  32.   
  33.     /** 
  34.      * 返回枚举常量的序号(它在枚举声明中的位置,其中初始常量序数为零)。 大多数程序员不会使用此方法。它被设计用于复杂的基于枚举的数据结构,比如 EnumSet 和 EnumMap。 
  35.      */  
  36.     public final int ordinal() {  
  37.         return ordinal;  
  38.     }  
  39.   
  40.     /** 
  41.      * 单独的构造方法。程序员无法调用此构造方法。 
  42.      * 该构造方法用于由响应枚举类型声明的编译器发出的代码。 
  43.      * 换句话说就是该构造方法是给编译器使用的 
  44.      */  
  45.     protected Enum(String name, int ordinal) {  
  46.         this.name = name;  
  47.         this.ordinal = ordinal;  
  48.     }  
  49.   
  50.     /** 
  51.      * toString()方法重写,返回该枚举名称  
  52.      */  
  53.     public String toString() {  
  54.         return name;  
  55.     }  
  56.   
  57.     /** 
  58.      * equals()方法重写 
  59.      */  
  60.     public final boolean equals(Object other) {  
  61.         return this==other;  
  62.     }  
  63.   
  64.     /** 
  65.      * hashCode()方法重写 
  66.      */  
  67.     public final int hashCode() {  
  68.         return super.hashCode();  
  69.     }  
  70.   
  71.     /** 
  72.      * 直接抛出 CloneNotSupportedException。 
  73.      * 这样这可保证枚举永远不会被复制,这对于保留其“单元素”状态是必需的。 
  74.      */  
  75.     protected final Object clone() throws CloneNotSupportedException {  
  76.         throw new CloneNotSupportedException();  
  77.     }  
  78.   
  79.     /** 
  80.      * 比较此枚举与指定对象的顺序。在该对象小于、等于或大于指定对象时,分别返回负整数、零或正整数。 枚举常量只能与相同枚举类型的其他枚举常量进行比较。该方法实现的自然顺序就是声明常量的顺序。 
  81.      */  
  82.     public final int compareTo(E o) {  
  83.         Enum<?> other = (Enum<?>)o;  
  84.         Enum<E> self = this;  
  85.         if (self.getClass() != other.getClass() && // optimization  
  86.             self.getDeclaringClass() != other.getDeclaringClass())  
  87.             throw new ClassCastException();  
  88.         return self.ordinal - other.ordinal;  
  89.     }  
  90.   
  91.     /** 
  92.      * 返回与此枚举常量的枚举类型相对应的 Class 对象。当且仅当 e1.getDeclaringClass() == e2.getDeclaringClass() 时,两个枚举常量 e1 和 e2 的枚举类型才相同。(由该方法返回的值不同于由 Object.getClass() 方法返回的值,Object.getClass() 方法用于带有特定常量的类主体的枚举常量。) 
  93.      */  
  94.     @SuppressWarnings("unchecked")  
  95.     public final Class<E> getDeclaringClass() {  
  96.         Class<?> clazz = getClass();  
  97.         Class<?> zuper = clazz.getSuperclass();  
  98.         return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;  
  99.     }  
  100.   
  101.     /** 
  102.      * 返回带指定名称的指定枚举类型的枚举常量。名称必须与在此类型中声明枚举常量所用的标识符完全匹配。(不允许使用额外的空白字符。) 
  103.      */  
  104.     public static <T extends Enum<T>> T valueOf(Class<T> enumType,  
  105.                                                 String name) {  
  106.         T result = enumType.enumConstantDirectory().get(name);  
  107.         if (result != null)  
  108.             return result;  
  109.         if (name == null)  
  110.             throw new NullPointerException("Name is null");  
  111.         throw new IllegalArgumentException(  
  112.             "No enum constant " + enumType.getCanonicalName() + "." + name);  
  113.     }  
  114.   
  115.     /** 
  116.      * 枚举类不能存在finalize()方法 
  117.      */  
  118.     protected final void finalize() { }  
  119.   
  120.     /** 
  121.      * 阻止默认反序列化 
  122.      */  
  123.     private void readObject(ObjectInputStream in) throws IOException,  
  124.         ClassNotFoundException {  
  125.         throw new InvalidObjectException("can't deserialize enum");  
  126.     }  
  127.     /** 
  128.      * 阻止默认反序列化 
  129.      */  
  130.     private void readObjectNoData() throws ObjectStreamException {  
  131.         throw new InvalidObjectException("can't deserialize enum");  
  132.     }  
  133. }  

 

        Enum 类中有两个成员变量:name ordinal,分别用于表示该枚举的名称和排序位置。Enum 中还有一些常用的方法,作用及原理都比较简单,我这里就不详细讲解了。

 

        2.枚举中隐含的方法

        编译后的自定义枚举类是java.lang.Enum 这个抽象类的子类,并且添加了valueOf(java.lang.String)values()的方法。然而这两个方法却并不是源代码中已经定义的,甚至你根本都找不到它是在哪里实现的。

        既然根本找不到实现,那么我们就自己去重写一下,于是在OSType枚举类中试着重写valueOf(String name)这个方法:

Java代码   收藏代码
  1. //The enum OSType already defines the method valueOf(String) implicitly  
  2. public OSType valueOf(String name) {  
  3.   
  4. }  

        重写valueOf(String name)方法后却惊奇的发现,编译器给出了错误提示,意思是说:该枚举(OSType)已经隐式定义了valueOf(String)方法,我们无法进行重写,否则编译器会给出错误提示。

        原来在枚举中已经定义了几种方法,如:values()valueOf(String),当重新定义它们时,编译器会主动给出错误提示:

Java代码   收藏代码
  1. /** 
  2. * Returns an array containing the constants of this enum  
  3. * type, in the order they're declared.  This method may be 
  4. * used to iterate over the constants as follows: 
  5. * 
  6. *    for(E c : E.values()) 
  7. *        System.out.println(c); 
  8. * 
  9. * @return an array containing the constants of this enum  
  10. * type, in the order they're declared 
  11. */  
  12. public static E[] values();  
  13.   
  14. /** 
  15. * Returns the enum constant of this type with the specified 
  16. * name. 
  17. * The string must match exactly an identifier used to declare 
  18. * an enum constant in this type.  (Extraneous whitespace  
  19. * characters are not permitted.) 
  20.  
  21. * @return the enum constant with the specified name 
  22. * @throws IllegalArgumentException if this enum type has no 
  23. * constant with the specified name 
  24. */  
  25. public static E valueOf(String name);  

        更详细的内容可以参看:https://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.9.2

 

        3.枚举的构造方法/函数

        java.lang.Enum类中有一个唯一的构造函数:

Java代码   收藏代码
  1. /** 
  2. * 唯一构造函数。程序员不能调用这个构造函数。 
  3. */  
  4. protected Enum(String name, int ordinal) {  
  5.     this.name = name;  
  6.     this.ordinal = ordinal;  
  7. }  

        虽然Enum 类是枚举的实际父类,但这也只是在编译后,在编译前我们是无法使用Enum 的任何方法和构造参数的。

        但是我们可以为自己定义的枚举创建适合的构造参数,如:

 

Java代码   收藏代码
  1. public enum OSType {  
  2.                     Linux, MacOSX, Windows("windows"), iOS("ios"3), Android(4), DOS();  
  3.   
  4.     private int    order;  
  5.     private String value;  
  6.   
  7.     OSType(){  
  8.     }  
  9.   
  10.     OSType(String value){  
  11.         this.value = value;  
  12.     }  
  13.   
  14.     OSType(int order){  
  15.         this.order = order;  
  16.     }  
  17.   
  18.     OSType(String value, int order){  
  19.         this.order = order;  
  20.         this.value = value;  
  21.     }  
  22.   
  23.   
  24.     public String getValue() {  
  25.         return value;  
  26.     }  
  27.   
  28.     public void setValue(String value) {  
  29.         this.value = value;  
  30.     }  
  31.   
  32.     public int getOrder() {  
  33.         return order;  
  34.     }  
  35.   
  36.     public void setOrder(int order) {  
  37.         this.order = order;  
  38.     }  
  39. }  

 

        自定义构造函数的好处就在于,我们可以为枚举提供更丰富的“值/意义”。但是枚举属性的定义必须符合构造函数,这是为什么呢?其实原理很简单,在使用枚举前,枚举通过invokestatic 字节码关键字已经把所有枚举属性进行了初始化,初始化当然需要调用构造函数,所以枚举属性的定义就需要符合构造函数的定义规则。

        通过javap -c 命令,输出更多内容:

Java代码   收藏代码
  1. javap -c OSType.class  
  2. Compiled from "OSType.java"  
  3. public final class OSType extends java.lang.Enum<OSType> {  
  4.   public static final OSType Linux;  
  5.   
  6.   public static final OSType MacOSX;  
  7.   
  8.   public static final OSType Windows;  
  9.   
  10.   public static final OSType iOS;  
  11.   
  12.   public static final OSType Android;  
  13.   
  14.   public static final OSType DOS;  
  15.   
  16.   public static OSType[] values();  
  17.     Code:  
  18.        0: getstatic     #1                  // Field $VALUES:[LOSType;  
  19.        3: invokevirtual #2                  // Method "[LOSType;".clone:()Ljava/  
  20. lang/Object;  
  21.        6: checkcast     #3                  // class "[LOSType;"  
  22.        9: areturn  
  23.   
  24.   public static OSType valueOf(java.lang.String);  
  25.     Code:  
  26.        0: ldc           #4                  // class OSType  
  27.        2: aload_0  
  28.        3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Lj  
  29. ava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;  
  30.        6: checkcast     #4                  // class OSType  
  31.        9: areturn  
  32.   
  33. ...  
  34.   
  35.   static {};  
  36.     Code:  
  37.        0new           #4                  // class OSType  
  38.        3: dup  
  39.        4: ldc           #9                  // String Linux  
  40.        6: iconst_0  
  41.        7: invokespecial #10                 // Method "<init>":(Ljava/lang/Strin  
  42. g;I)V  
  43.       10: putstatic     #11                 // Field Linux:LOSType;  
  44.       13new           #4                  // class OSType  
  45.       16: dup  
  46.       17: ldc           #12                 // String MacOSX  
  47.       19: iconst_1  
  48.       20: invokespecial #10                 // Method "<init>":(Ljava/lang/Strin  
  49. g;I)V  
  50.       23: putstatic     #13                 // Field MacOSX:LOSType;  
  51.       26new           #4                  // class OSType  
  52.       29: dup  
  53.       30: ldc           #14                 // String Windows  
  54.       32: iconst_2  
  55.       33: ldc           #15                 // String windows  
  56.       35: invokespecial #16                 // Method "<init>":(Ljava/lang/Strin  
  57. g;ILjava/lang/String;)V  
  58.       38: putstatic     #17                 // Field Windows:LOSType;  
  59.       41new           #4                  // class OSType  
  60.       44: dup  
  61.       45: ldc           #18                 // String iOS  
  62.       47: iconst_3  
  63.       48: ldc           #19                 // String ios  
  64.       50: iconst_3  
  65.       51: invokespecial #20                 // Method "<init>":(Ljava/lang/Strin  
  66. g;ILjava/lang/String;I)V  
  67.       54: putstatic     #21                 // Field iOS:LOSType;  
  68.       57new           #4                  // class OSType  
  69.       60: dup  
  70.       61: ldc           #22                 // String Android  
  71.       63: iconst_4  
  72.       64: iconst_4  
  73.       65: invokespecial #23                 // Method "<init>":(Ljava/lang/Strin  
  74. g;II)V  
  75.       68: putstatic     #24                 // Field Android:LOSType;  
  76.       71new           #4                  // class OSType  
  77.       74: dup  
  78.       75: ldc           #25                 // String DOS  
  79.       77: iconst_5  
  80.       78: invokespecial #10                 // Method "<init>":(Ljava/lang/Strin  
  81. g;I)V  
  82.       81: putstatic     #26                 // Field DOS:LOSType;  
  83.       84: bipush        6  
  84.       86: anewarray     #4                  // class OSType  
  85.       89: dup  
  86.       90: iconst_0  
  87.       91: getstatic     #11                 // Field Linux:LOSType;  
  88.       94: aastore  
  89.       95: dup  
  90.       96: iconst_1  
  91.       97: getstatic     #13                 // Field MacOSX:LOSType;  
  92.      100: aastore  
  93.      101: dup  
  94.      102: iconst_2  
  95.      103: getstatic     #17                 // Field Windows:LOSType;  
  96.      106: aastore  
  97.      107: dup  
  98.      108: iconst_3  
  99.      109: getstatic     #21                 // Field iOS:LOSType;  
  100.      112: aastore  
  101.      113: dup  
  102.      114: iconst_4  
  103.      115: getstatic     #24                 // Field Android:LOSType;  
  104.      118: aastore  
  105.      119: dup  
  106.      120: iconst_5  
  107.      121: getstatic     #26                 // Field DOS:LOSType;  
  108.      124: aastore  
  109.      125: putstatic     #1                  // Field $VALUES:[LOSType;  
  110.      128return  
  111. }  

        其中invokespecial 关键字的作用就是调用指定构造函数进行初始化,跟Java代码中的new Object() 形式类似。

 

        4.枚举中声明方法

        在定义的枚举类中我们可以声明方法(为了方便及节省篇幅,我把代码整理了一下),如:

Java代码   收藏代码
  1. /** 
  2.  * @Description 操作系统类型 
  3.  * @Notes 未填写备注 
  4.  * @author ming.li 
  5.  * @Date 2018年4月16日 下午2:33:14 
  6.  * @version 1.0 
  7.  * @since JDK 1.8 
  8.  */  
  9.   
  10. public enum OSType {  
  11.                     Linux("linux"), Windows("windows");  
  12.     // 声明的方法  
  13.     public String getOSTypeName() {  
  14.         return value;  
  15.     }  
  16.   
  17.     private String value;  
  18.     // 声明的方法  
  19.     public static void main(String args[]) {  
  20.         System.out.println(OSType.Linux.getOSTypeName());  
  21.         System.out.println(OSType.Windows.getValue());  
  22.   
  23.     }  
  24.   
  25.     OSType(){  
  26.     }  
  27.   
  28.     OSType(String value){  
  29.         this.value = value;  
  30.     }  
  31.     // 声明的方法  
  32.     public String getValue() {  
  33.         return value;  
  34.     }  
  35.     // 声明的方法  
  36.     public void setValue(String value) {  
  37.         this.value = value;  
  38.     }  
  39. }  
  40. //打印结果:  
  41. linux  
  42. windows  

        从打印结果可以看到我们调用枚举中定义的public 方法成功了。需要注意的是,如果将方法定义成private我们将无法在外部对其进行调用。

        在枚举中我们亦可以定义抽象的方法体,但是定义了抽象方法后,必须在枚举成员体中实现它,因为每一个枚举成员你都可以理解成一个命名不同的类实例如:

Java代码   收藏代码
  1. /** 
  2.  * @Description 操作系统类型 
  3.  * @Notes 未填写备注 
  4.  * @author ming.li 
  5.  * @Date 2018年4月16日 下午2:33:14 
  6.  * @version 1.0 
  7.  * @since JDK 1.8 
  8.  */  
  9. public enum OSType {  
  10.                     Linux("linux") {  
  11.   
  12.                         @Override  
  13.                         String getProducer() {  
  14.                             return "Red Hat";  
  15.                         }  
  16.                     },  
  17.                     Windows("windows") {  
  18.   
  19.                         @Override  
  20.                         String getProducer() {  
  21.                             return "Microsoft";  
  22.                         }  
  23.                     };  
  24.   
  25.     // 抽象方法:获取生产厂商  
  26.     abstract String getProducer();  
  27.   
  28.     private String value;  
  29.   
  30.     OSType(String value){  
  31.         this.value = value;  
  32.     }  
  33.   
  34.     public String getValue() {  
  35.         return value;  
  36.     }  
  37.   
  38.     public void setValue(String value) {  
  39.         this.value = value;  
  40.     }  
  41. }  

        通过javac编译后产生了一个OSType.class文件和两个内部类OSType$1.class、OSType$2.class文件,通过javap命令反编译后:

Java代码   收藏代码
  1. javap  OSType  
  2. Compiled from "OSType.java"  
  3. public abstract class OSType extends java.lang.Enum<OSType> {  
  4.   public static final OSType Linux;  
  5.   public static final OSType Windows;  
  6.   public static OSType[] values();  
  7.   public static OSType valueOf(java.lang.String);  
  8.   abstract java.lang.String getProducer();  
  9.   public java.lang.String getValue();  
  10.   public void setValue(java.lang.String);  
  11.   OSType(java.lang.String, int, java.lang.String, OSType$1);  
  12.   static {};  
  13. }  
  14.   
  15. javap  OSType$1  
  16. Compiled from "OSType.java"  
  17. final class OSType$1 extends OSType {  
  18.   OSType$1(java.lang.String, int, java.lang.String);  
  19.   java.lang.String getProducer();  
  20. }  
  21.   
  22. javap  OSType$2  
  23. Compiled from "OSType.java"  
  24. final class OSType$2 extends OSType {  
  25.   OSType$2(java.lang.String, int, java.lang.String);  
  26.   java.lang.String getProducer();  
  27. }  

        我们发现OSType 被编译成了abstract 类,两个子类则被编译成了final 类。这样就可以很容易的理解为什么在枚举中定义一个抽象方法必须要在枚举成员中实现了。

        当然你也可以在枚举属性中直接定义方法,然而这种方法无法被外部调用,所以也就没什么意义。

 

        5.枚举与switch

        我敢说很多人之所以使用枚举就是因为枚举可以被switch 无缝支持,可以像switch基本数据类型一样高效,然而新版本的JDK中已经支持switch(String) 这种方式,那么枚举还具备它天生的优势吗?

        首先,定义一个常量类,用于表示不同的操作系统类型:

Java代码   收藏代码
  1. public class OSTypeConstatns {  
  2.   
  3.     public static String LINUX   = "linux";  
  4.     public static String WINDOWS = "windows";  
  5. }  

        然后,分别调用OSType 枚举与OSTypeConstatns 常量类进行switch 操作:

Java代码   收藏代码
  1. OSType ost = OSType.Windows;  
  2. switch (ost) {  
  3.     case Linux:  
  4.         break;  
  5.     case Windows:  
  6.         break;  
  7.     default:  
  8.         break;  
  9. }  
  10. String ostc = OSTypeConstatns.WINDOWS;  
  11. switch (ostc) {  
  12.     case "windows":  
  13.         break;  
  14.     case "linux":  
  15.         break;  
  16.     default:  
  17.         break;  
  18. }  

        两种方式都可以达到相同的目的:根据指定的值,进行指定的操作。然而事实却并非如此,两者的效率却相差很大,通过javap 反编译得到字节码对比:

Java代码   收藏代码
  1. Code:  
  2. stack=2, locals=4, args_size=1  
  3.  0: getstatic     #2                  // Field OSTypeConstatns.WINDOWS:L  
  4. java/lang/String;  
  5.  3: astore_1  
  6.  4: aload_1  
  7.  5: astore_2  
  8.  6: iconst_m1  
  9.  7: istore_3  
  10.  8: aload_2  
  11.  9: invokevirtual #3                  // Method java/lang/String.hashCod  
  12. e:()I  
  13. //使用Stirng类型进行switch  
  14. 12: lookupswitch  { // 2  
  15.        10297778054  
  16.       134949337940  
  17.      default65  
  18.     }  
  19. 40: aload_2  
  20. 41: ldc           #4                  // String windows  
  21. 43: invokevirtual #5                  // Method java/lang/String.equals:  
  22. (Ljava/lang/Object;)Z  
  23. 46: ifeq          65  
  24. 49: iconst_0  
  25. 50: istore_3  
  26. 51goto          65  
  27. 54: aload_2  
  28. 55: ldc           #6                  // String linux  
  29. 57: invokevirtual #5                  // Method java/lang/String.equals:  
  30. (Ljava/lang/Object;)Z  
  31. 60: ifeq          65  
  32. 63: iconst_1  
  33. 64: istore_3  
  34. 65: iload_3  
  35. //使用枚举类型进行switch  
  36. 66: lookupswitch  { // 2  
  37.            092  
  38.            195  
  39.      default98  
  40.     }  
  41. 92goto          98  
  42. 95goto          98  
  43. 98return  
  44. LineNumberTable:  
  45. line 220  
  46. line 234  
  47. line 2592  
  48. line 2795  
  49. line 3198  
  50. StackMapTable: number_of_entries = 6  
  51. frame_type = 254 /* append */  
  52.   offset_delta = 40  
  53.   locals = [ class java/lang/String, class java/lang/String, int ]  
  54. frame_type = 13 /* same */  
  55. frame_type = 10 /* same */  
  56. frame_type = 26 /* same */  
  57. frame_type = 2 /* same */  
  58. frame_type = 249 /* chop */  
  59.   offset_delta = 2  

        可以明显的看到枚举中lookupswitch 操作判断的是枚举的ordinal(顺序号),而用String 时判断的是字符串的hashCode,在此之前还需要计算hashCode,所以明显枚举的效率更高。

        执行完case 里的方法后,枚举直接return,而String完成后还进行一些其他操作,况且String equals 操作肯定不如直接比较int 型来的直接,最终switch使用枚举是一个比较好的选择,就如同使用int型一样高效。

 

        6.嵌套枚举

        所谓嵌套枚举就是在一个普通class类中定义枚举,这样的好处就是便于管理,如:

 

Java代码   收藏代码
  1. /** 
  2.  * @Description 枚举常量 
  3.  * @Notes 未填写备注 
  4.  * @author ming.li 
  5.  * @Date 2018年4月23日 下午4:06:48 
  6.  * @version 1.0 
  7.  * @since JDK 1.8 
  8.  */  
  9. public class EnumConstants {  
  10.   
  11.     enum PCType {  
  12.                  PC, MAC, PAD;  
  13.     }  
  14.   
  15.     static enum OSType {  
  16.                         LINUX, WINDOWS;  
  17.     }  
  18.   
  19. }  

        需要注意的是嵌套类型的枚举隐式静态(static)的,但也可以显示声明为static,这意味着不可能定义一个局部枚举,或者在一个内部类中定义一个枚举。

 

        7.EnumMap

        在java.util包下有一个EnumMap 类,顾名思义,这是一个与枚举类型键一起使用的专用 Map 实现。枚举映射中所有键都必须来自单个枚举类型,该枚举类型在创建映射时显式或隐式地指定。枚举映射在内部表示为数组。此表示形式非常紧凑且高效。

        枚举映射根据其键的自然顺序 来维护(该顺序是声明枚举常量的顺序)。在 collection 视图(keySet()entrySet() 和 values())所返回的迭代器中反映了这一点。

        由 collection 视图返回的迭代器是弱一致 的:它们不会抛出 ConcurrentModificationException,也不一定显示在迭代进行时发生的任何映射修改的效果。

        不允许使用 null 键。试图插入 null 键将抛出 NullPointerException。但是,试图测试是否出现 null 键或移除 null 键将不会抛出异常。允许使用 null 值。

        像大多数 collection 一样,EnumMap 是不同步的。如果多个线程同时访问一个枚举映射,并且至少有一个线程修改该映射,则此枚举映射在外部应该是同步的。这一般通过对自然封装该枚举映射的某个对象进行同步来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap(java.util.Map ) 方法来“包装”该枚举。最好在创建时完成这一操作,以防止意外的非同步访问:

Java代码   收藏代码
  1. Map<EnumKey, V> m = Collections.synchronizedMap(new EnumMap<EnumKey, V>(...));  

        注意事项:所有基本操作都在固定时间内执行。虽然并不保证,但它们很可能比其 HashMap 副本更快。

        EnumMap 原理并不复杂,其实它就是把key换成了枚举值,内部用一个对象数组Object[] vals 来实现value 的存储,而且put get 时比较的是枚举值的ordinal(序号),所以效率比较高,唯一有限制的地方就是key 的取值必须是已定义的枚举范围内,以下是一些关键方法的源码:

Java代码   收藏代码
  1. /** 
  2. * key的Class类型 
  3. */  
  4. private final Class<K> keyType;  
  5.   
  6. /** 
  7. * 所有key 
  8. */  
  9. private transient K[] keyUniverse;  
  10.   
  11. /** 
  12. * 对象数组实现 
  13. */  
  14. private transient Object[] vals;  
  15.   
  16. /** 
  17. * put方法 
  18. **/  
  19. public V put(K key, V value) {  
  20.     typeCheck(key);  
  21.     //这里的index直接取的是枚举的序号值  
  22.     int index = key.ordinal();  
  23.     Object oldValue = vals[index];  
  24.     vals[index] = maskNull(value);  
  25.     if (oldValue == null)  
  26.         size++;  
  27.     return unmaskNull(oldValue);  
  28. }  
  29.   
  30. /** 
  31. * get方法 
  32. **/  
  33. public V get(Object key) {  
  34.     return (isValidKey(key) ?  
  35.     unmaskNull(vals[((Enum<?>)key).ordinal()]) : null);  
  36. }  

        如果key的取值范围固定且value不为空的情况下,我们就应该优先考虑使用EnumMap ,毕竟它的效率比HashMap 要稳定平均,不会出现抖动。EnumMap 特别适合适配器模式的使用。

        EnumMap的使用:

Java代码   收藏代码
  1. Map<OSType, Object> map = new EnumMap<OSType, Object>(OSType.class);  
  2. map.put(OSType.Windows, "Microsoft");  
  3. Set<OSType> keys = map.keySet();  
  4. for (OSType key : keys) {  
  5.     System.out.println(map.get(key));  
  6. }  
  7. //打印结果:  
  8. Microsoft  

        通过debug 我们可以看到,EnumMap 初始化时已经将OSType 的所有枚举类型获取并赋予keyUniverse :


 

        8.EnumSet

        与EnumMap 一样EnumSet 同样是Java Collections Framework 的成员,他们都在java.util包之下,EnumSet是使用枚举类型的专用 Set 实现。枚举 set 中所有键都必须来自单个枚举类型,该枚举类型在创建 set 时显式或隐式地指定。枚举 set 在内部表示为位向量。此表示形式非常紧凑且高效。此类的空间和时间性能应该很好,足以用作传统上基于 int 的“位标志”的替换形式,具有高品质、类型安全的优势。如果其参数也是一个枚举 set,则批量操作(如 containsAll 和 retainAll)也应运行得非常快。

        由 iterator 方法返回的迭代器按其自然顺序 遍历这些元素(该顺序是声明枚举常量的顺序)。返回的迭代器是弱一致的:它从不抛出 ConcurrentModificationException,也不一定显示在迭代进行时发生的任何 set 修改的效果。

        不允许使用 null 元素。试图插入 null 元素将抛出 NullPointerException。但是,试图测试是否出现 null 元素或移除 null 元素将不会抛出异常。

        像大多数 collection 实现一样,EnumSet 是不同步的。如果多个线程同时访问一个枚举 set,并且至少有一个线程修改该 set,则此枚举 set 在外部应该是同步的。这通常是通过对自然封装该枚举 set 的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用 Collections.synchronizedSet(java.util.Set ) 方法来“包装”该 set。最好在创建时完成这一操作,以防止意外的非同步访问:

Java代码   收藏代码
  1. Set<MyEnum> s = Collections.synchronizedSet(EnumSet.noneOf(MyEnum.class));  

        与EnumMap 一样,EnumSet 的所有基本操作都在固定时间内执行。依然不能保证,但很可能比 HashSet效率更高。如果其参数也是一个枚举 set ,则批量操作会在固定时间内执行。

        与EnumMap使用方法类似,这里我就不举例了。

 

        9.使用枚举时的一些注意或技巧

        1)枚举类型不能声明为抽象,这样做会导致编译时错误:

Java代码   收藏代码
  1. public abstract enum EnumName {  
  2. }  

        2)枚举同样不能被声明为final,这样做会导致编译时错误。如下做法会导致无法编译:

Java代码   收藏代码
  1. public final enum EnumName {  
  2. }  

 

        3)枚举可以不声明为public ,如:

Java代码   收藏代码
  1. enum EnumName {  
  2. }  

 

        4)可以添加普通方法、静态方法、抽象方法、构造方法,但抽象方法在枚举类型值中必须实现。

        5)枚举可以实现接口,例如:

Java代码   收藏代码
  1. public enum EnumName implements Serializable {  
  2. }  

        6)枚举不可以继承,原因我就不解释了,看到这里的人已经很清楚了。

        7)一般情况下降枚举值定义为大写的,当然并不是必须。

        8)枚举无法使用new 进行示例化。

        9)java.lang.Enum 中的final 克隆方法确保枚举常量永远不能被克隆,并且序列化机制的特殊处理确保了重复实例不会因为反序列化而创建。

        10)枚举可以被注解修饰。

        11)因为每个枚举只有一个实例,所以我们可以直接使用“==”来代替equals 比较两个枚举是否相同,而且这么做效率更高。

        12)枚举的构造函数无法被声明为public protected 的,这样做会导致编译时错误,但是我们可以将构造函数显示声明为private,因为枚举的构造函数隐式默认为private

        13)一个枚举永远不会被finalized

        14)枚举就是枚举,不要为枚举添加过多的业务判断,虽然你可以在枚举类中这么做,单一职责一定是最高效的。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值