理解Java枚舉類型


 (參考資料:深入理解java enum

1、原理:對編譯后的class文件javap反編譯可以看出,定義的枚舉類繼承自java.lang.Enum抽象類且通過public static final定義了幾個常量作為枚舉常量。示例:

 1 //定義枚舉類型
 2 enum Day {
 3     MONDAY, TUESDAY, WEDNESDAY,
 4     THURSDAY, FRIDAY, SATURDAY, SUNDAY
 5 }
 6 
 7 //對應的完整內容
 8 //反編譯Day.class
 9 final class Day extends Enum
10 {
11     //編譯器為我們添加的靜態的values()方法
12     public static Day[] values()
13     {
14         return (Day[])$VALUES.clone();
15     }
16     //編譯器為我們添加的靜態的valueOf()方法,注意間接調用了Enum也類的valueOf方法
17     public static Day valueOf(String s)
18     {
19         return (Day)Enum.valueOf(com/zejian/enumdemo/Day, s);
20     }
21     //私有構造函數
22     private Day(String s, int i)
23     {
24         super(s, i);
25     }
26      //前面定義的7種枚舉實例
27     public static final Day MONDAY;
28     public static final Day TUESDAY;
29     public static final Day WEDNESDAY;
30     public static final Day THURSDAY;
31     public static final Day FRIDAY;
32     public static final Day SATURDAY;
33     public static final Day SUNDAY;
34     private static final Day $VALUES[];
35 
36     static 
37     {    
38         //實例化枚舉實例
39         MONDAY = new Day("MONDAY", 0);
40         TUESDAY = new Day("TUESDAY", 1);
41         WEDNESDAY = new Day("WEDNESDAY", 2);
42         THURSDAY = new Day("THURSDAY", 3);
43         FRIDAY = new Day("FRIDAY", 4);
44         SATURDAY = new Day("SATURDAY", 5);
45         SUNDAY = new Day("SUNDAY", 6);
46         $VALUES = (new Day[] {
47             MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
48         });
49     }
50 }
枚舉類反編譯后的源碼

java.lang.Enum抽象類定義了一些方法:

返回類型 方法名稱 方法說明
int compareTo(E o) 比較此枚舉與指定對象的順序
boolean equals(Object other) 當指定對象等於此枚舉常量時,返回 true。
Class<?> getDeclaringClass() 返回與此枚舉常量的枚舉類型相對應的 Class 對象
String name() 返回此枚舉常量的名稱,在其枚舉聲明中對其進行聲明
int ordinal() 返回枚舉常量的序數(它在枚舉聲明中的位置,其中初始常量序數為零)
String toString() 返回枚舉常量的名稱,它包含在聲明中
static<T extends Enum<T>> T static valueOf(Class<T> enumType, String name) 返回帶指定名稱的指定枚舉類型的枚舉常量。

 主要源碼:

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

    private final String name; //枚舉字符串名稱

    public final String name() {
        return name;
    }

    private final int ordinal;//枚舉順序值

    public final int ordinal() {
        return ordinal;
    }

    //枚舉的構造方法,只能由編譯器調用
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

    public String toString() {
        return name;
    }

    public final boolean equals(Object other) {
        return this==other;
    }

    //比較的是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值比較大小
    }

    @SuppressWarnings("unchecked")
    public final Class<E> getDeclaringClass() {
        //獲取class對象引用,getClass()是Object的方法
        Class<?> clazz = getClass();
        //獲取父類Class對象引用
        Class<?> zuper = clazz.getSuperclass();
        return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
    }


    public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                String name) {
        //enumType.enumConstantDirectory()獲取到的是一個map集合,key值就是name值,value則是枚舉變量值   
        //enumConstantDirectory是class對象內部的方法,根據class對象獲取一個map集合的值       
        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);
    }

    //.....省略其他沒用的方法
}
java.lang.Enum

2、可以把枚舉類型當成常規類,即我們可以向枚舉類中添加方法和變量。但是枚舉常量定義必須在方法定義前面,否則編譯報錯。示例:

 1 public enum Day2 {
 2     MONDAY("星期一"),
 3     TUESDAY("星期二"),
 4     WEDNESDAY("星期三"),
 5     THURSDAY("星期四"),
 6     FRIDAY("星期五"),
 7     SATURDAY("星期六"),
 8     SUNDAY("星期日");//記住要用分號結束
 9 
10     private String desc;//中文描述
11 
12     /**
13      * 私有構造,防止被外部調用
14      * @param desc
15      */
16     private Day2(String desc){
17         this.desc=desc;
18     }
19 
20     /**
21      * 定義方法,返回描述,跟常規類的定義沒區別
22      * @return
23      */
24     public String getDesc(){
25         return desc;
26     }
27 
28     public static void main(String[] args){
29         for (Day2 day:Day2.values()) {
30             System.out.println("name:"+day.name()+
31                     ",desc:"+day.getDesc());
32         }
33     }
34 
35     /**
36      輸出結果:
37      name:MONDAY,desc:星期一
38      name:TUESDAY,desc:星期二
39      name:WEDNESDAY,desc:星期三
40      name:THURSDAY,desc:星期四
41      name:FRIDAY,desc:星期五
42      name:SATURDAY,desc:星期六
43      name:SUNDAY,desc:星期日
44      */
45 }
枚舉類型自定義方法
public enum EnumDemo3 {

    FIRST{
        @Override
        public String getInfo() {
            return "FIRST TIME";
        }
    },
    SECOND{
        @Override
        public String getInfo() {
            return "SECOND TIME";
        }
    }

    ;

    /**
     * 定義抽象方法
     * @return
     */
    public abstract String getInfo();

    //測試
    public static void main(String[] args){
        System.out.println("F:"+EnumDemo3.FIRST.getInfo());
        System.out.println("S:"+EnumDemo3.SECOND.getInfo());
        /**
         輸出結果:
         F:FIRST TIME
         S:SECOND TIME
         */
    }
}
枚舉類型中定義抽象方法

3、定義的枚舉類型無法被繼承(看反編譯后的源碼可知類被final修飾了)也無法繼承其他類(因其已默認繼承了Enum類,而Java只允許單繼承),但可以實現接口。一個很好的示例:

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;
    }
  }
} 
枚舉類實現接口

 4、枚舉與單例:使用枚舉單例的寫法,我們完全不用考慮序列化和反射的問題。枚舉序列化是由jvm保證的,每一個枚舉類型和定義的枚舉變量在JVM中都是唯一的,在枚舉類型的序列化和反序列化上,Java做了特殊的規定:在序列化時Java僅僅是將枚舉對象的name屬性輸出到結果中,反序列化的時候則是通過java.lang.Enum的valueOf方法來根據名字查找枚舉對象。同時,編譯器是不允許任何對這種序列化機制的定制的並禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,從而保證了枚舉實例的唯一性(也說明了只有Java中只有編譯器能創建枚舉實例)。

如何確保反序列化時不會破壞單例:根據valueOf(name)得到反序列化后對象,valueOf根據枚舉常量名獲取對應枚舉常量

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);
  }




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;
valueOf

如何確保反射不會破壞單例:反射源碼里對於枚舉類型反射直接拋異常所以反射生成不了枚舉類型實例

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;
    }
View Code

在單例中,枚舉也不是萬能的。在android開發中,內存優化是個大塊頭,而使用枚舉時占用的內存常常是靜態變量的兩倍還多,因此android官方在內存優化方面給出的建議是盡量避免在android中使用enum。

5、EnumMap與EnumSet:見上述參考資料。

前者與HashMap類似,只不過key是Enum類型且不能為null。

后者則采用位向量實現,對於枚舉值個數少於64的用一個long來標記(RegularEnumSet)否則用long[ ]來標記(JumboEnumSet)。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM