天天看點

枚舉的底層原理是什麼?什麼是枚舉枚舉的使用枚舉本身的方法原理枚舉實作單例總結

文章已收錄Github精選,歡迎Star: https://github.com/yehongzhi/learningSummary

什麼是枚舉

枚舉是JDK1.5新增的一種資料類型,是一種特殊的類,常用于表示一組常量,比如一年四季,12個月份,星期一到星期天,服務傳回的錯誤碼,結算支付的方式等等。枚舉是使用enum關鍵字來定義。

枚舉的使用

在使用枚舉之前我們先探讨一個問題,為什麼要使用枚舉。

現在有個業務場景是結算支付,有支付寶和微信支付兩種方式,1表示支付寶,2表示微信支付,還需要根據編碼(1或2)擷取相應的英文名,如果不用枚舉,我們就要這樣寫。

public class PayTypeUtil {
    //支付寶
    private static final int ALI_PAY = 1;
    //微信支付
    private static final int WECHAT_PAY = 2;

    //根據編碼擷取支付方式的名稱
    public String getPayName(int code) {
        if (ALI_PAY == code) {
            return "Ali_Pay";
        }
        if (WECHAT_PAY == code) {
            return "Wechat_Pay";
        }
        return null;
    }
}           

如果這時,産品經理說要增加一個銀聯支付,就要加多if的判斷,就會造成有多少種支付方式,就有多少個

if

,非常難看。

如果使用枚舉,就變得很優雅,先看代碼:

public enum PayTypeEnum {
    /** 支付寶*/
    ALI_PAY(1, "ALI_PAY"),
    /** 微信支付*/
    WECHAT_PAY(2, "WECHAT_PAY");

    private int code;

    private String describe;

    PayTypeEnum(int code, String describe) {
        this.code = code;
        this.describe = describe;
    }
    //根據編碼擷取支付方式
    public PayTypeEnum find(int code) {
        for (PayTypeEnum payTypeEnum : values()) {
            if (payTypeEnum.getCode() == code) {
                return payTypeEnum;
            }
        }
        return null;
    }
    //getter、setter方法
}           

當我們需要擴充,隻需要定義多一個執行個體即可,其他代碼都不用動,比如加多一個銀聯支付。

/** 支付寶*/
ALI_PAY(1, "ALI_PAY"),
/** 微信支付*/
WECHAT_PAY(2, "WECHAT_PAY"),
//隻需要加多一行代碼即可完成擴充
/** 銀聯支付*/
UNION_PAY(3,"UNION_PAY");           

一般在實際項目中,最多的寫法就是這樣,主要是簡單明了,易于擴充。

第二種常見的用法是結合switch-case使用,比如我定義一個一年四季的枚舉。

public enum Season {
    //春
    SPRING,
    //夏
    SUMMER, 
    //秋
    AUTUMN, 
    //冬
    WINTER;
}           

然後結合switch使用。

public static void main(String[] args) throws Exception{
    doSomething(Season.SPRING);
}

private static void doSomething(Season season){
    switch (season){
        case SPRING:
            System.out.println("不知細葉誰裁出,二月春風似剪刀");
            break;
        case SUMMER:
            System.out.println("接天蓮葉無窮碧,映日荷花别樣紅");
            break;
        case AUTUMN:
            System.out.println("停車坐愛楓林晚,霜葉紅于二月花");
            break;
        case WINTER:
            System.out.println("梅花香自苦寒來,寶劍鋒從磨砺出");
            break;
        default:
            System.out.println("垂死病中驚坐起,笑問客從何處來");
    }
}           

可能很多人覺得直接用int,String類型配合switch使用就夠了,為什麼還要支援枚舉,這樣的設計是不是顯得很多餘,其實非也。

不妨反過來想,假如用1到4代表四季,接收的參數類型就是int,在沒有提示的情況下,我們僅僅隻知道數int類型是很難猜到需要傳入數字的範圍,字元串也是一樣,如果不用枚舉你是很難一眼看出需要傳入什麼參數,這才是最關鍵的。

如果使用枚舉,那麼問題就迎刃而解,當你調用doSomething()方法時,一看到枚舉就知道傳入的是哪幾個參數,因為已經在枚舉類裡面定義好了。這對于項目交接,還有代碼的可讀性都是非常有利的。

這種限制不單止限制了調用方,也限制了傳入的參數隻能是定義好的枚舉,不用擔心傳入的參數錯誤導緻的程式錯誤。

是以枚舉類使用得恰當,對于項目的可維護性是有很大提升的。

枚舉本身的方法

首先我們先以上面的支付類型枚舉PayTypeEnum為例子,看看有哪些自帶的方法。

valueOf()方法

這是一個靜态方法,傳入一個字元串(枚舉的名稱),擷取枚舉類。如果傳入的名稱不存在,則報錯。

public static void main(String[] args) throws Exception{
    System.out.println(PayTypeEnum.valueOf("ALI_PAY"));
    System.out.println(PayTypeEnum.valueOf("HUAWEI_PAY"));
}           
枚舉的底層原理是什麼?什麼是枚舉枚舉的使用枚舉本身的方法原理枚舉實作單例總結

values()方法

傳回包含枚舉類中所有枚舉資料的一個數組。

public static void main(String[] args) throws Exception {
    PayTypeEnum[] payTypeEnums = PayTypeEnum.values();
    for (PayTypeEnum payTypeEnum : payTypeEnums) {
        System.out.println("code: " + payTypeEnum.getCode() + ",describe: " + payTypeEnum.getDescribe());
    }
}           
枚舉的底層原理是什麼?什麼是枚舉枚舉的使用枚舉本身的方法原理枚舉實作單例總結

ordinal()方法

預設情況下,枚舉類會給定義的枚舉提供一個預設的次序,ordinal()方法就可以傳回枚舉的次序。

public static void main(String[] args) throws Exception {
    PayTypeEnum[] payTypeEnums = PayTypeEnum.values();
    for (PayTypeEnum payTypeEnum : payTypeEnums) {
        System.out.println("ordinal: " + payTypeEnum.ordinal() + ", Enum: " + payTypeEnum);
    }
}
/**
ordinal: 0, Enum: ALI_PAY
ordinal: 1, Enum: WECHAT_PAY
ordinal: 2, Enum: UNION_PAY
*/           

name()、toString()方法

傳回定義枚舉用的名稱。

public static void main(String[] args) throws Exception {
    for (Season season : Season.values()) {
        System.out.println(season.name());
    }
    for (Season season : Season.values()) {
        System.out.println(season.toString());
    }
}           

輸出結果都是一樣的:

SPRING
SUMMER
AUTUMN
WINTER           

為什麼?因為底層代碼是一樣,傳回的是name。

public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
    
    public final String name() {
        return name;
    }
    
    public String toString() {
        return name;
    }
}
               

差別在于toString()方法沒有被final修飾,可以重寫,name()方法不能重寫。

compareTo()方法

因為枚舉類實作了Comparable接口,是以必須重寫compareTo()方法,比較的是枚舉的次序,也就是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;
}           

因為實作Comparable接口,是以可以用來排序,比如這樣:

public static void main(String[] args) throws Exception {
    //這裡是亂序的枚舉數組
    Season[] seasons = new Season[]{Season.WINTER, Season.AUTUMN, Season.SPRING, Season.SUMMER};
    //調用sort方法排序,按預設次序排序
    Arrays.sort(seasons);
    for (Season season : seasons) {
        System.out.println(season);
    }
}           

輸出結果,按照預設次序排序:

SPRING
SUMMER
AUTUMN
WINTER           

原理

以枚舉Season為例,分析一下枚舉的底層。表面上看,一個枚舉很簡單:

public enum Season {
    //春
    SPRING,
    //夏
    SUMMER,
    //秋
    AUTUMN,
    //冬
    WINTER;
}           

實際上編譯器在編譯的時候做了很多動作,我們使用

javap -v

對Season.class檔案反編譯,可以看到很多細節。

首先我們看到枚舉是繼承了抽象類Enum的類。

Season extends java.lang.Enum<Season>           

第二,通過一段靜态代碼塊初始化枚舉。

static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=4, locals=0, args_size=0
         0: new           #4                  // class io/github/yehongzhi/user/redisLock/Season
         3: dup
         4: ldc           #7                  // String SPRING
         6: iconst_0
         7: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
        10: putstatic     #9                  // Field SPRING:Lio/github/yehongzhi/user/redisLock/Season;
        13: new           #4                  // class io/github/yehongzhi/user/redisLock/Season
        16: dup
        17: ldc           #10                 // String SUMMER
        19: iconst_1
        20: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
        23: putstatic     #11                 // Field SUMMER:Lio/github/yehongzhi/user/redisLock/Season;
        26: new           #4                  // class io/github/yehongzhi/user/redisLock/Season
        29: dup
        30: ldc           #12                 // String AUTUMN
        32: iconst_2
        33: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
        36: putstatic     #13                 // Field AUTUMN:Lio/github/yehongzhi/user/redisLock/Season;
        39: new           #4                  // class io/github/yehongzhi/user/redisLock/Season
        42: dup
        43: ldc           #14                 // String WINTER
        45: iconst_3
        46: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
        49: putstatic     #15                 // Field WINTER:Lio/github/yehongzhi/user/redisLock/Season;
        52: iconst_4
        53: anewarray     #4                  // class io/github/yehongzhi/user/redisLock/Season
        56: dup
        57: iconst_0
        58: getstatic     #9                  // Field SPRING:Lio/github/yehongzhi/user/redisLock/Season;
        61: aastore
        62: dup
        63: iconst_1
        64: getstatic     #11                 // Field SUMMER:Lio/github/yehongzhi/user/redisLock/Season;
        67: aastore
        68: dup
        69: iconst_2
        70: getstatic     #13                 // Field AUTUMN:Lio/github/yehongzhi/user/redisLock/Season;
        73: aastore
        74: dup
        75: iconst_3
        76: getstatic     #15                 // Field WINTER:Lio/github/yehongzhi/user/redisLock/Season;
        79: aastore
        80: putstatic     #1                  // Field $VALUES:[Lio/github/yehongzhi/user/redisLock/Season;
        83: return           

這段靜态代碼塊的作用就是生成四個靜态常量字段的值,還生成了$VALUES字段,用于儲存枚舉類定義的枚舉常量。相當于執行了以下代碼:

Season SPRING = new Season1();
Season SUMMER = new Season2();
Season AUTUMN = new Season3();
Season WINTER = new Season4();
Season[] $VALUES = new Season[4];
$VALUES[0] = SPRING;
$VALUES[1] = SUMMER;
$VALUES[2] = AUTUMN;
$VALUES[3] = WINTER;           

第三個,關于values()方法,這是一個靜态方法,作用是傳回該枚舉類的數組,底層實作原理,其實是這樣的。

public static io.github.yehongzhi.user.redisLock.Season[] values();
    Code:
       0: getstatic     #1                  // Field $VALUES:[Lio/github/yehongzhi/user/redisLock/Season;
       3: invokevirtual #2                  // Method "[Lio/github/yehongzhi/user/redisLock/Season;".clone:()Ljava/lang/Object;
       6: checkcast     #3                  // class "[Lio/github/yehongzhi/user/redisLock/Season;"
       9: areturn           

其實是将靜态代碼塊初始化的$VALUES數組克隆一份,然後強轉成Season[]傳回。相當于這樣:

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

是以表面上,隻是加了一個enum關鍵字定義枚舉,但是底層一旦确認是枚舉類,則會由編譯器對枚舉類進行特殊處理,通過靜态代碼塊初始化枚舉,隻要是枚舉就一定會提供values()方法。

通過反編譯我們也知道所有的枚舉父類都是抽象類Enum,是以Enum有的成員變量,實作的接口,子類也會有。

是以隻要是枚舉都會有name,ordinal這兩個字段,并且我們看Enum的構造器。

/**
* Sole constructor.  Programmers cannot invoke this constructor.
* It is for use by code emitted by the compiler in response to
* enum type declarations.
*/
protected Enum(String name, int ordinal) {
    this.name = name;
    this.ordinal = ordinal;
}           

翻譯一下上面那段英文,意思大概是:唯一的構造器,程式員沒法調用此構造器,它是供編譯器響應枚舉類型聲明而使用的。得出結論,枚舉執行個體的建立也是由編譯器完成的。

枚舉實作單例

很多人都說,枚舉類是最好的實作單例的一種方式,因為枚舉類的單例是線程安全,并且是唯一一種不會被破壞的單例模式實作。也就是不能通過反射的方式建立執行個體,保證了整個應用中隻有一個執行個體,非常硬核的單例。

public class SingletonObj {
    //内部類使用枚舉
    private enum SingletonEnum {
        INSTANCE;

        private SingletonObj singletonObj;
            //在枚舉類的構造器裡初始化singletonObj
        SingletonEnum() {
            singletonObj = new SingletonObj();
        }

        private SingletonObj getSingletonObj() {
            return singletonObj;
        }
    }

    //對外部提供的擷取單例的方法
    public static SingletonObj getInstance() {
        //擷取單例對象,傳回
        return SingletonEnum.INSTANCE.getSingletonObj();
    }

    //測試
    public static void main(String[] args) {
        SingletonObj a = SingletonObj.getInstance();
        SingletonObj b = SingletonObj.getInstance();
        System.out.println(a == b);//true
    }
}           

假如有人想通過反射建立枚舉類呢,我們以Season枚舉為例。

public static void main(String[] args) throws Exception {
    Constructor<Season> constructor = Season.class.getDeclaredConstructor(String.class, int.class);
    constructor.setAccessible(true);
    //通過反射調用構造器,建立枚舉
    Season season = constructor.newInstance("NEW_SPRING", 4);
    System.out.println(season);
}           

然後就會報錯,因為不允許對枚舉的構造器使用反射調用。

枚舉的底層原理是什麼?什麼是枚舉枚舉的使用枚舉本身的方法原理枚舉實作單例總結

檢視源碼,就可以看到,有個專門針對枚舉的

if

判斷。

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);
        }
    }
    //判斷是否是枚舉,如果是枚舉的話,報、抛出異常
    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;
}           

總結

枚舉看起來好像是很小一部分的知識,其實深入挖掘的話,我們會發現還是有很多地方值得學習的。第一點使用枚舉定義常量更容易擴充,而且代碼可讀性更強,維護性更好。接着第二點是需要了解枚舉自帶的方法。第三點通過反編譯,探索編譯器在編譯階段為枚舉做了什麼事情。最後再講一下枚舉實作單例模式的例子。

這篇文章講到這裡了,感謝大家的閱讀,希望看完這篇文章能有所收獲!

覺得有用就點個贊吧,你的點贊是我創作的最大動力~

我是一個努力讓大家記住的程式員。我們下期再見!!!

枚舉的底層原理是什麼?什麼是枚舉枚舉的使用枚舉本身的方法原理枚舉實作單例總結
能力有限,如果有什麼錯誤或者不當之處,請大家批評指正,一起學習交流!