面試官:爲啥需要枚舉?枚舉有什麼作用?怎麼用枚舉實現單例?

點贊再看,養成習慣,微信搜索【三太子敖丙】關注這個互聯網苟且偷生的工具人。

本文 GitHub https://github.com/JavaFamily 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

Java基礎:枚舉的用法與原理

在學習過程中,我們也只是在定義常量的時候,會意識到枚舉的存在,而定義常量其實可以在類中實現,這時就會感覺枚舉有點雞肋。但在實際項目開發的過程中,枚舉因相當迷人的特性而受到越來越多的關注。

本文將按以下小節點來,一一介紹枚舉:

  • 枚舉的實現
  • 枚舉的用法
  • 枚舉的原理
  • 枚舉與單例

1. 枚舉的實現

枚舉是JDK1.5之後的特性,在此之前一般是在類中對常量進行定義。那麼爲什麼需要枚舉呢?舉個栗子:

使用靜態變量定義四季

假如我們需要使用四個變量來代表“春夏秋冬”:

public class Season {
    public final static int SRPING = 1;
    public final static int SUMMER = 2;
    public final static int AUTUMN = 3;
    public final static int WINTER = 4;
}

這時候只要直接引用Season.SPRING就可以了,我們不需要去操心SPRING在存儲時是什麼數據。但是如果我們想做更多的事:知道下一個季節是什麼,還想把季節打印出來:

public class Season {

    private Season(){}

    public final static Season SPRING = new Season();
    public final static Season SUMMER = new Season();
    public final static Season AUTUMN = new Season();
    public final static Season WINTER = new Season();

    public static Season getNextSeason(Season nowSeason){
        if(nowSeason == SPRING){
            return SUMMER;
        }else if(nowSeason == SUMMER){
            return AUTUMN;
        }else if(nowSeason == AUTUMN){
            return WINTER;
        }else{
            return SPRING;
        }
    }

    public static void printNowSeason(Season nowSeason){
        if(nowSeason == SPRING){
            System.out.println("春季");
        }else if(nowSeason == SUMMER){
            System.out.println("夏季");
        }else if(nowSeason == AUTUMN){
            System.out.println("秋季");
        }else{
            System.out.println("冬季");
        }
    }

    public static void main(String[] args){
        Season nowSeason = Season.SUMMER;
        Season.printNowSeason(nowSeason);
        Season nextSeason = Season.getNextSeason(nowSeason);
        Season.printNowSeason(nextSeason);
    }
}

因爲將Season類的構造方法私有化,外界就不能創建該類的對象了,這就避免了其他奇怪的季節的出現,所有Season對象都在該內部創建。

但是有個問題,用於存儲的int值不見了,所以我們還需要設定另一個方法:

    public static int toInt(Season nowSeason){
        if(nowSeason == SPRING){
            return 1;
        }else if(nowSeason == SUMMER){
            return 2;
        }else if(nowSeason == AUTUMN){
            return 3;
        }else{
            return 4;
        }
    }

這時如果需要一個Season對象對應的int數據,只需要Season.toInt(Season.SPRING)即可。

但是這種寫法有一個隱患:如果想要擴展功能,需要寫大量的if-else判斷。

這時,枚舉來啦。

枚舉定義四季

我們還是以四季作爲栗子:

public enum Season {
    SPRING, SUMMER, AUTUMN, WINTER;
}

好啦,枚舉定義完了。我們來看看怎麼使用它:

class Test{
    public static void main(String[] args){
        System.out.println(Season.SUMMER);  //輸出:SUMMER
    }
}

在枚舉中,默認的toString()方法返回的就是枚舉類中對應的名稱。但是我們上面要求打印出來的是如”春季“等,而不是名稱本身,且四季對應的int值也是必要的。所以我們還得自己完善枚舉:

public enum Season {
    SPRING(0), SUMMER(1), AUTUMN(2), WINTER(3);

    private int value;

    private Season(int value){
        this.value = value;
    }

    public static Season getNextSeason(Season nowSeason){
        int nextDayValue = nowSeason.value;
        if(++nextDayValue == 3){
            nextDayValue = 0;
        }
        return getSeasonByValue(nextDayValue);
    }

    public static Season getSeasonByValue(int value){
        for(Season s : Season.values()){
            if(s.value == value){
                return s;
            }
        }
        return null;
    }
}
class Test{
    public static void main(String[] args){
        System.out.println("nowSeason->"+Season.SPRING+", value->"+Season.SPRING.ordinal());
        System.out.println("nextSeason->"+Season.getNextSeason(Season.SPRING));
    }
}

這樣,我們就實現了既定的目標,和之前的代碼相比,沒有那麼多if-else,是不是感覺少了很多煩惱呢?

所以,我們在定義有限的序列時,如星期、性別等,一般會通過靜態變量的形式進行定義,但是這種形式在添加功能的時候,就會需要很多不利於擴展和維護的代碼,所以枚舉的實現,可以簡化這些操作

2. 枚舉的用法

枚舉類中有些方法還是比較常用的,在此演示幾個比較重要的方法。以四季爲例:

public enum Season {
    SPRING, SUMMER, AUTUMN, WINTER
}

Season.valueOf()方法

此方法的作用是傳來一個字符串,然後將它轉換成對應的枚舉變量。前提是傳入的字符串和定義枚舉變量的字符串一模一樣,須區分大小寫。如果傳入了一個不存在的字符串,那麼會拋出異常。

System.out.println(Season.valueOf("spring".toUpperCase()));
System.out.println(Season.valueOf("nyfor2020"));

運行結果爲:

Exception in thread "main" SPRING
java.lang.IllegalArgumentException: No enum constant Season.nyfor2020
 at java.lang.Enum.valueOf(Enum.java:238)
 at Season.valueOf(Season.java:5)
 at Test.main(Season.java:11)

Season.values()方法和Season.ordinal()方法

Season.values()方法會返回包括所有枚舉變量的數據

默認情況下,枚舉會給所有的枚舉變量提供一個默認的次序,該次序類似數組的下標,從0開始,而Season.ordinal()方法正是可以獲取其次序的方法

for (Season s: Season.values()){
            System.out.println(s + ".ordinal() --> "+s.ordinal());
        }

運行結果爲:

SPRING.ordinal() --> 0
SUMMER.ordinal() --> 1
AUTUMN.ordinal() --> 2
WINTER.ordinal() --> 3

Season.toString()方法和Season.name()方法

Season.toString()方法會返回枚舉定義枚舉變量時的字符串。此方法同Season.name()方法是一樣的。

System.out.println("SEASON.SPRING.name --> "+Season.SPRING.name());
System.out.println("SEASON.SPRING.toString --> "+Season.SPRING.toString());

運行結果爲:

SEASON.SPRING.name --> SPRING
SEASON.SPRING.toString --> SPRING

實現過程來看,name()方法和toString()方法可以說是一樣的。

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

但它們之間唯一的區別是,toString()方法可以重寫,但name()方法被final修飾了,不能重寫。

Season.compareTo()方法

這個方法用於比較兩個枚舉變量的“大小”,實際上比較的是兩個枚舉變量之間的次序,並返回次序相減之後的結果。

System.out.println("SEASON.SPRING.compareTo(SEASON.WINTER) --> "+ Season.SPRING.compareTo(Season.WINTER));

運行結果爲:

SEASON.SPRING.compareTo(SEASON.WINTER) --> -3

我們來看看它的源碼:

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

在這裏其實我們就已經可以看到了,compareTo()方法中會先判斷是否屬於同一個枚舉的變量,然後再返回差值

那麼枚舉有什麼要注意的東西呢?

  • 枚舉使用的是enum關鍵字,而不是class;
  • 枚舉變量之間用逗號隔開,且枚舉變量最好用大寫,多個單詞之間使用“_"隔開(INT_SUM)。
  • 定義完變量之後,以分號結束,如果只是有枚舉變量,而不是自定義變量,分號可以省略。
  • 只需要類名.變量名就可以召喚枚舉變量了,跟使用靜態變量一樣。

枚舉與switch

枚舉是JDK1.5纔有的特性,同時switch也更新了。使用switch進行條件判斷的時候,條件整數一般只能是整型,字符型,而枚舉型確實也被switch所支持。還是用“四季“舉個栗子:

public enum Season {
    SPRING, SUMMER, AUTUMN, WINTER
}
class SeasonSwitch{
    public void judge(Season s){
        switch (s){
            case SPRING:
                System.out.println("spring");
                break;
            case SUMMER:
                System.out.println("summer");
                break;
            case AUTUMN:
                System.out.println("autumn");
                break;
            case WINTER:
                System.out.println("winter");
                break;
        }
    }
    public static void main(String[] args){
        Season s = Season.SPRING;
        SeasonSwitch seasonSwitch = new SeasonSwitch();
        seasonSwitch.judge(s);
    }
}

運行結果爲:

spring

枚舉的高級使用方法

我們還是拿四季來做個例子:

public enum Season {
    SPRING, SUMMER, AUTUMN, WINTER
}

在這裏,SPRING對應的ordinal值對應的就是0,SUMMER對應的就是1。如果我們想將SPRING的值爲1,那麼就需要自己定義變量:

public enum Season {
    SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4);

    private int value;

    private Season(int value){
        this.value = value;
    }
}

如果我們想對一個枚舉變量做兩個維度的描述呢?

public enum Season {
    SPRING(1"spring"), SUMMER(2"summer"), AUTUMN(3"autumn"), WINTER(4"winter");

    private int value;
    private String lab;

    private Season(int value, String lab){
        this.value = value;
        this.lab = lab;
    }
}

總結一下,如果需要自定義枚舉變量,需要注意一下幾點:

  • 一定要把枚舉變量的定義放在第一行,並且以分號結尾;
  • 構造函數必須私有化,但也不是一定要寫private,事實上枚舉的構造函數默認並強制爲private,寫public是無法通過編譯的。
  • ordinal還是按照它的規則給每個枚舉變量按次序賦值,自定義變量與默認的ordinal屬性並不衝突。

3. 枚舉的原理

我們還是拿“四季”作爲栗子:

public enum Season {
    SPRING() {
        @Override
        public Season getNextSeason() {
            return SUMMER;
        }
    }, SUMMER() {
        @Override
        public Season getNextSeason() {
            return AUTUMN;
        }
    }, AUTUMN() {
        @Override
        public Season getNextSeason() {
            return WINTER;
        }
    }, WINTER() {
        @Override
        public Season getNextSeason() {
            return SPRING;
        }
    };

    public abstract Season getNextSeason();
}

反編譯之後,我們可以看到:

>javap Season.class
Compiled from "Season.java"
public abstract class Season extends java.lang.Enum<Season{
  public static final Season SPRING;
  public static final Season SUMMER;
  public static final Season AUTUMN;
  public static final Season WINTER;
  public static Season[] values();
  public static Season valueOf(java.lang.String);
  public abstract Season getNextSeason();
  Season(java.lang.String, int, Season$1);
  static {};
}

經過編譯器編譯之後,Season是一個繼承了Enum類的抽象類,而且枚舉中定義的枚舉變量變成了相應的public static final屬性其類型爲抽象類Season類型,名字就是枚舉變量的名字。

同時我們可以看到,Season.class的相同路徑下看到四個內部類的.class文件:

也就是說,這四個枚舉常量分別使用了內部類來實現

同時還添加了兩個方法values()和valueOf(String s)。我們使用的是默認的無參構造函數,但現在的構造函數有兩個參數。還生成了一個靜態代碼塊。下面我們來詳細看下是怎麼回事兒:

>javap -c -v Season.class
Classfile /E:/Intellij IDEA/project/JVMTest/src/Season.class
  Last modified 2020-5-6; size 1114 bytes
  MD5 checksum 5fb619a1f14495913ba7820312371ded
  Compiled from "Season.java"
public abstract class Season extends java.lang.Enum<Season>
  minor version: 0
  major version: 52
  flagsACC_PUBLICACC_SUPERACC_ABSTRACTACC_ENUM
Constant pool:
   #1 
= Methodref          #5.#50         // Season."<init>":(Ljava/lang/String;
I)V
   #2 = Fieldref           #5.#51         // Season.$VALUES:[LSeason;
   #3 = Methodref          #52.#53        // "[LSeason;".clone:()Ljava/lang/Obje
ct;
   #4 = Class              #32            // "[LSeason;"
   #5 = Class              #54            // Season
   #6 = Methodref          #24.#55        // java/lang/Enum.valueOf:(Ljava/lang/
Class;Ljava/lang/String;)Ljava/lang/Enum;
   #7 = Methodref          #24.#50        // java/lang/Enum."<init>":(Ljava/lang
/String;I)V
   #8 = Class              #56            // Season$1
   #9 = String             #26            // SPRING
  #10 = Methodref          #8.#50         // Season$1."<init>":(Ljava/lang/Strin
g;I)V
  #11 = Fieldref           #5.#57         // Season.SPRING:LSeason;
  #12 = Class              #58            // Season$2
  #13 = String             #28            // SUMMER
  #14 = Methodref          #12.#50        // Season$2."<init>":(Ljava/lang/Strin
g;I)V
  #15 = Fieldref           #5.#59         // Season.SUMMER:LSeason;
  #16 = Class              #60            // Season$3
  #17 = String             #29            // AUTUMN
  #18 = Methodref          #16.#50        // Season$3."<init>":(Ljava/lang/Strin
g;I)V
  #19 = Fieldref           #5.#61         // Season.AUTUMN:LSeason;
  #20 = Class              #62            // Season$4
  #21 = String             #30            // WINTER
  #22 = Methodref          #20.#50        // Season$4."<init>":(Ljava/lang/Strin
g;I)V
  #23 = Fieldref           #5.#63         // Season.WINTER:LSeason;
  #24 = Class              #64            // java/lang/Enum
  #25 = Utf8               InnerClasses
  #26 = Utf8               SPRING
  #27 = Utf8               LSeason;
  #28 = Utf8               SUMMER
  #29 = Utf8               AUTUMN
  #30 = Utf8               WINTER
  #31 = Utf8               $VALUES
  #32 = Utf8               [LSeason;
  #33 = Utf8               values
  #34 = Utf8               ()[LSeason;
  #35 = Utf8               Code
  #36 = Utf8               LineNumberTable
  #37 = Utf8               valueOf
  #38 = Utf8               (Ljava/lang/String;)LSeason;
  #39 = Utf8               <init>
  #40 = Utf8               (Ljava/lang/String;I)V
  #41 = Utf8               Signature
  #42 = Utf8               ()V
  #43 = Utf8               getNextSeason
  #44 = Utf8               ()LSeason;
  #45 = Utf8               (Ljava/lang/String;ILSeason$1;)V
  #46 = Utf8               <clinit>
  #47 = Utf8               Ljava/lang/Enum<LSeason;>;
  #48 = Utf8               SourceFile
  #49 = Utf8               Season.java
  #50 = NameAndType        #39:#40        // "<init>":(Ljava/lang/String;I)V
  #51 = NameAndType        #31:#32        // $VALUES:[LSeason;
  #52 = Class              #32            // "[LSeason;"
  #53 = NameAndType        #65:#66        // clone:()Ljava/lang/Object;
  #54 = Utf8               Season
  #55 = NameAndType        #37:#67        // valueOf:(Ljava/lang/Class;Ljava/lan
g/String;)Ljava/lang/Enum;
  #56 = Utf8               Season$1
  #57 = NameAndType        #26:#27        // SPRING:LSeason;
  #58 = Utf8               Season$2
  #59 = NameAndType        #28:#27        // SUMMER:LSeason;
  #60 = Utf8               Season$3
  #61 = NameAndType        #29:#27        // AUTUMN:LSeason;
  #62 = Utf8               Season$4
  #63 = NameAndType        #30:#27        // WINTER:LSeason;
  #64 = Utf8               java/lang/Enum
  #65 = Utf8               clone
  #66 = Utf8               ()Ljava/lang/Object;
  #67 = Utf8               (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;

{
  public static final Season SPRING;
    descriptor: LSeason;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static final Season SUMMER;
    descriptor: LSeason;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static final Season AUTUMN;
    descriptor: LSeason;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static final Season WINTER;
    descriptor: LSeason;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static Season[] values();
    descriptor: ()[LSeason;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #2                  // Field $VALUES:[LSeason;
         3: invokevirtual #3                  // Method "[LSeason;".clone:()Ljav
a/lang/Object;
         6: checkcast     #4                  // class "[LSeason;"
         9: areturn
      LineNumberTable:
        line 70

  public static Season valueOf(java.lang.String);
    descriptor: (Ljava/lang/String;)LSeason;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: ldc           #5                  // class Season
         2: aload_0
         3: invokestatic  #6                  // Method java/lang/Enum.valueOf:(
Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
         6: checkcast     #5                  // class Season
         9: areturn
      LineNumberTable:
        line 70

  public abstract Season getNextSeason();
    descriptor: ()LSeason;
    flags: ACC_PUBLIC, ACC_ABSTRACT

  Season(java.lang.String, int, Season$1)
;
    descriptor: (Ljava/lang/String;ILSeason$1;)V
    flags: ACC_SYNTHETIC
    Code:
      stack=3, locals=4, args_size=4
         0: aload_0
         1: aload_1
         2: iload_2
         3: invokespecial #1                  // Method "<init>":(Ljava/lang/Str
ing;I)V
         6return
      LineNumberTable:
        line 70

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

下面分析一下字節碼中各部分內容,先拿靜態代碼塊下手:

靜態代碼塊

static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=4, locals=0, args_size=0
      //創建一個Season$1的內部類對象
         0new           #8                  // class Season$1
         3: dup
         //接下來的兩條指令,是將兩個參數推送到棧頂,調用Season$1的編譯器生成的<init>方法
         4: ldc           #9                  // String SPRING
         6: iconst_0
         //調用Season$1的<init>方法
         7: invokespecial #10                 // Method Season$1."<init>":(Ljava/lang/String;I)V
  //設置SPRING屬性的值爲新創建的對象
        10: putstatic     #11                 // Field SPRING:LSeason;
        //接下來說是分別初始化另外三個屬性SUMMER、AUTUMU、WINTER,此處就不贅述了
        13new           #12                 // class Season$2
        16: dup
        17: ldc           #13                 // String SUMMER
        19: iconst_1
        20: invokespecial #14                 // Method Season$2."<init>":(Ljava/lang/String;I)V
        23: putstatic     #15                 // Field SUMMER:LSeason;
        26new           #16                 // class Season$3
        29: dup
        30: ldc           #17                 // String AUTUMN
        32: iconst_2
        33: invokespecial #18                 // Method Season$3."<init>":(Ljava/lang/String;I)V
        36: putstatic     #19                 // Field AUTUMN:LSeason;
        39new           #20                 // class Season$4
        42: dup
        43: ldc           #21                 // String WINTER
        45: iconst_3
        46: invokespecial #22                 // Method Season$4."<init>":(Ljava/lang/String;I)V
        49: putstatic     #23                 // Field WINTER:LSeason;
        52: iconst_4
        53: anewarray     #5                  // class Season
        56: dup
        57: iconst_0
        58: getstatic     #11                 // Field SPRING:LSeason;
        61: aastore
        62: dup
        63: iconst_1
        64: getstatic     #15                 // Field SUMMER:LSeason;
        67: aastore
        68: dup
        69: iconst_2
        70: getstatic     #19                 // Field AUTUMN:LSeason;
        73: aastore
        74: dup
        75: iconst_3
        76: getstatic     #23                 // Field WINTER:LSeason;
        79: aastore
        //將剛創建的數組設置爲屬性$VALUES的值
        80: putstatic     #2                  // Field $VALUES:[LSeason;
        83return

靜態代碼塊部分做的工作,就是分別設置生成的四個公共靜態常量字段的值,同時編譯器還生成一個靜態字段$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()方法

接下來我們來看看編譯器爲我們生成的values()方法:

  public static Season[] values();
    descriptor: ()[LSeason;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #2                  // Field $VALUES:[LSeason;
         3: invokevirtual #3                  // Method "[LSeason;".clone:()Ljav
a/lang/Object;
         6: checkcast     #4                  // class "[LSeason;"
         9: areturn

values()方法是一個公共的靜態方法,所以我們可以直接調用該方法,返回枚舉的數組。而這個方法實現的是,將靜態代碼塊中初始化的$VALUES字段的值克隆出來,並且強制轉換成Season[]類型返回,就相當於以下代碼:

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

valueOf()方法

接下來我們來看另一個由編譯器生成的valueOf()方法:

  public static Season valueOf(java.lang.String);
    descriptor: (Ljava/lang/String;)LSeason;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: ldc           #5                  // class Season
         2: aload_0
         3: invokestatic  #6                  // Method java/lang/Enum.valueOf:(
Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
         6: checkcast     #5                  // class Season
         9: areturn

valueOf()也是一個公共的靜態方法,所以可以直接調用這個方法並返回參數字符串表示的枚舉變量,另外,這個方法的實現是調用Enum.valueOf()方法,並把類型強制轉換爲Season,它相當於如下的代碼:

public static Season valueOf(String s){
 return (Season)Enum.valueOf(Season.class, s);
}

最後,我們來看下編譯器生成的內部類是什麼樣的。

內部類

我們以Season$1爲例:

>javap Season$1.class
Compiled from "Season.java"
final class Season$1 extends Season {
  Season$1(java.lang.String, int);
  public Season getNextSeason();
}

可以看到,Season1的構造函數有兩個入參呢?

關於這個問題,我們還是得從Season的父類Enum說起。

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

從Enum中我們可以看到,每個枚舉都定義了兩個屬性,name和ordinal,name表示枚舉變量的名稱,而ordinal則是根據變量定義的順序授予的整型值,從0開始。

在枚舉變量初始化的時候,會自動初始化這兩個字段,設置相應的值,所以會在Season()的構造方法中添加兩個參數。

而且我們可以從Enum的源碼中看到,大部分的方法都是final修飾的,特別是clone、readObject、writeObject這三個方法,保證了枚舉類型的不可變性,不能通過克隆、序列化和反序列化複製枚舉,這就保證了枚舉變量只是一個實例,即是單例的。

總結一下,其實枚舉本質上也是通過普通的類來實現的,只是編譯器爲我們進行了處理。每個枚舉類型都繼承自Enum類,並由編譯器自動添加了values()和valueOf()方法每個枚舉變量是一個靜態常量字段,由內部類實現,而這個內部類繼承了此枚舉類。

所有的枚舉變量都是通過靜態代碼塊進行初始化,也就是說在類加載期間就實現了。

另外,通過把clone、readObject、writeObject這三個方法定義爲final,保證了每個枚舉類型及枚舉常量都是不可變的,也就是說,可以用枚舉實現線程安全的單例。

4. 枚舉與單例

枚舉類實現單例模式相當硬核,因爲枚舉類型是線程安全的,且只會裝載一次。使用枚舉類來實現單例模式,是所有的單例實現中唯一一種不會被破壞的單例模式實現。

public class SingletonObject {

    private SingletonObject() {
    }

    private enum Singleton {
        INSTANCE;

        private final SingletonObject instance;

        Singleton() {
            instance = new SingletonObject();
        }

        private SingletonObject getInstance() {
            return instance;
        }
    }

    public static SingletonObject getInstance() {
        return Singleton.INSTANCE.getInstance();
    }
}

結語

在學習Java枚舉類的時候,原本列出來了很多問題如Java枚舉的線程安全和序列化問題,但是在瞭解完Java枚舉的原理之後,這些問題,都迎刃而解了,也許在未來可能會碰上枚舉的特例吧。

本文主要對final關鍵字進行介紹,如果本文對你有幫助,請給一個贊吧,這會是我最大的動力~

我是敖丙,一個在互聯網苟且偷生的工具人。

你知道的越多,你不知道的越多人才們的 【三連】 就是丙丙創作的最大動力,我們下期見!

注:如果本篇博客有任何錯誤和建議,歡迎人才們留言,你快說句話啊


文章持續更新,可以微信搜索「 三太子敖丙 」第一時間閱讀,回覆【資料】有我準備的一線大廠面試資料和簡歷模板,本文 GitHub https://github.com/JavaFamily 已經收錄,有大廠面試完整考點,歡迎Star。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章