Java:枚舉類也就這麼回事

1、前言

本篇博客是對JDK1.5的新特性枚舉的一波小小的總結,主要是昨天在看一部分面試題的時候,遇到了枚舉類型的題目,發現本身有許多細節還須要增強,作起來都模棱兩可,是時候總結一波了。程序員

2、源自一道面試題

很少bb,直接開門見山,我遇到這樣一道也許很簡單的題目:面試

enum AccountType
{
    SAVING, FIXED, CURRENT;
    private AccountType()
    {
        System.out.println(「It is a account type」);
    }
}
class EnumOne
{
    public static void main(String[]args)
    {
        System.out.println(AccountType.FIXED);
    }
}

問打印的結果是啥?正確答案以下:數組

It is a account type
It is a account type
It is a account type
FIXED

至於結果爲啥是這個,且看我慢慢總結。數據結構

3、枚舉的由來

存在即合理。this

我賊喜歡這句聖經,每次我一解釋不了它爲何出現的時候,就不自覺地用上這句話。.net

枚舉必定有他存在的價值,在一些時候,咱們須要定義一個類,這個類中的對象是有限且固定的,好比咱們一年有四個季節,春夏秋冬。code

在枚舉被支持以前,咱們該如何定義這個Season類呢?可能會像下面這樣:對象

public class Season {
    //private修飾構造器,沒法隨意建立對象
    private Season(){}
    //final修飾提供的對象在類外不能改變
    public static final Season SPRING = new Season();
    public static final Season SUMMER = new Season();
    public static final Season AUTUMN = new Season();
    public static final Season WINTER = new Season();
}

在定義上,這個Season類能夠完成咱們的預期,它們各自表明一個實例,且不能被改變,外部也不能隨便建立實例。blog

但,經過自定義類實現枚舉的效果有個顯著的問題:代碼量很是大。

因而,JDK1.5,枚舉類應運而生。

4、枚舉的定義形式

enum關鍵字用以定義枚舉類,這是一個和classinterface關鍵字地位至關的關鍵字。也就是說,枚舉類和咱們以前使用的類差不太多,且enum和class修飾的類若是同名,會出錯。

有一部分規則,類須要遵循的,枚舉類也遵循:

  • 枚舉類也能夠定義成員變量、構造器、普通和抽象方法等。
  • 一個Java源文件最多隻能定義一個public的枚舉類,且類名與文件名相同。
  • 枚舉類能夠實現一個或多個接口。

也有一部分規則,枚舉類顯得不同凡響:

  • 枚舉類的實例必須在枚舉類的第一行顯式列出,以逗號分隔,列出的實例系統默認添加public static final修飾。
  • 枚舉類的構造器默認私有,且只能是私有,能夠重載。
  • 枚舉類默認final修飾,沒法被繼承。
  • 枚舉類都繼承了java.lang.Enum類,因此沒法繼承其餘的類。
  • 通常狀況下,枚舉常量須要用枚舉類.枚舉常量的方式調用。

知道這些以後,咱們能夠用enum關鍵字從新定義枚舉類:

public enum Season{
    //定義四個實例
    SPRING,SUMMER,AUTUMN,WINTER;
}

須要注意的是,在JDK1.5枚舉類加入以後,switch-case語句進行了擴展,其控制表達式能夠是任意枚舉類型,且能夠直接使用枚舉值的名稱,無需添加枚舉類做爲限定。

5、Enum類裏有啥?

Enum類是全部enum關鍵字修飾的枚舉類的頂級父類,裏頭定義的方法默認狀況下,是通用的,咱們來瞅它一瞅:

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

咱們能夠發現,Enum實際上是一個繼承自Object類的抽象類(Object類果真是頂級父類,不可撼動),並實現了兩個接口:

  • Comparable:支持枚舉對象的比較。
  • Serializable:支持枚舉對象的序列化。

一、惟一的構造器

protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

官方文檔這樣說的:程序員不能去調用這個構造器,它用於編譯器響應enum類型聲明發出的代碼,關於這一點,咱們後面體會會更加深入一些。

二、重要的方法們

關於Object類中的方法,這邊就不贅述了,主要提一提特殊的方法。

public final String name()

返回這個枚舉常量的名稱。官方建議:大多數狀況,最好使用toString()方法,由於能夠返回一個友好的名字。而name()方法以final修飾,沒法被重寫。

public String toString()

源碼上看,toString()方法和name()方法是相同的,可是建議:若是有更友好的常量名稱顯示,能夠重寫toString()方法。

public final int ordinal()

返回此枚舉常量的序號(其在enum聲明中的位置,其中初始常量的序號爲零)。

大多數程序員將不須要這種方法。它被用於複雜的基於枚舉的數據結構中,如EnumSet和EnumMap。

public final int compareTo(E o)

這個方法用於指定枚舉對象比較順序,同一個枚舉實例只能與相同類型的枚舉實例進行比較。

public final int compareTo(E o) {
        Enum<?> other = (Enum<?>)o;
        Enum<E> self = this;
        if (self.getClass() != other.getClass() && // optimization
            //getDeclaringClass()方法返回該枚舉常量對應Enum類的類對象
            self.getDeclaringClass() != other.getDeclaringClass())
            throw new ClassCastException();
        //該枚舉常量順序在o常量以前,返回負整數
        return self.ordinal - other.ordinal;
    }

public static <T extends Enum > T valueOf(Class enumType,
String name)

該靜態方法返回指定枚舉類中指定名稱的枚舉常量。

三、憑空出現的values()方法

爲何會想到總結這個方法呢?其實也是有必定心路歷程的,官方文檔特別強調了一句話:

Note that when using an enumeration type as the type of a set or as the type of the keys in a map, specialized and efficient set and map implementations are available.

通常Note開頭的玩意兒,仍是比較重要的。大體意思以下:

當使用枚舉類型做爲集合的類型或映射中的鍵的類型時,可使用專門化且有效的集合和映射實現。

看完很是不理解,因而開始查找資料,發現有一種用法:

Arrays.asList(AccountType.values())

很明顯調用了這個枚舉類的values()方法,可是剛纔對枚舉類的方法一通分析,也沒看到有values()方法啊。可是編譯器確實提示,有,確實有!

這是怎麼回事呢?JDK文檔是這麼說的:

The compiler automatically adds some special methods when it creates an enum. For example, they have a static values method that returns an array containing all of the values of the enum in the order they are declared.

編譯器會在建立一個枚舉類的時候,自動在裏面加入一些特殊的方法,例如靜態的values()方法,它將返回一個數組,按照枚舉常量聲明的順序存放它們。

這樣一來,枚舉類就能夠和集合等玩意兒很好地配合在一塊兒了,具體咋配合,之後遇到了就知道了。

關於這一點,待會反編譯以後會更加印象深入。

6、反編譯枚舉類

注:因爲學識尚淺,這部份內容總結起來虛虛的,可是總歸查找了許多的資料,若有說的不對的地方,還望評論區批評指正。

那麼,回到咱們文章開頭提到的那到面試題,咱們根據結果來推測程序運行以後發生的狀況:

  • 其中的構造器被調用了三次,說明定義的枚舉常量確實是三個活生生的實例,也就是說,每次建立實例就會調用一次構造器。
  • 而後,System.out.println(AccountType.FIXED);將會調用toString()方法,因爲子類沒有重寫,那麼將會返回name值,也就是"FIXED"

至此,咱們的猜想結束,其實確實也大差不差了,大體就是這個過程。在一番查閱資料以後,我又嘗試着去反編譯這個枚舉類文件:

咱們先用javap -p AccountType.class命令試着反編譯以後查看全部類和成員。

爲了看看static中發生的狀況,我試着用更加詳細的指令,javap -c -l AccountType.class,試圖獲取本地變量信息表和行號,雖然我大機率仍是看不太懂的。

咱們以其中一個爲例,參看虛擬機字節碼指令表,大體過程以下:

static {};
    Code:
       0: new           #4                  //建立一個對象,將其引用值壓入棧
       3: dup                               //複製棧頂數值並將複製值壓入棧頂
       4: ldc           #10                 //將String常量值SAVING從常量池推送至棧頂
       6: iconst_0                          //將int型0推送至棧頂
       7: invokespecial #11                 //調用超類構造器
      10: putstatic     #12                 //爲指定的靜態域賦值

如下爲由我的理解簡化的編譯結構:

public final class AccountType extends java.lang.Enum<AccountType> {
    //靜態枚舉常量
    public static final AccountType SAVING;

    public static final AccountType FIXED;

    public static final AccountType CURRENT;

    //存儲靜態枚舉常量的私有靜態域
    private static final AccountType[] $VALUES;

    //編譯器新加入的靜態方法
    public static AccountType[] values();

    //調用實例方法獲取指定名稱的枚舉常量
    public static AccountType valueOf(java.lang.String);

    static {
        //建立對象,傳入枚舉常量名和順序
        SAVING = new AccountType("SAVING",0);
        FIXED = new AccountType("FIXED",1);
        CURRENT = new AccountType("CURRENT",2);
        //給靜態域賦值
        $VALUES = new AccountType[]{
            SAVING,FIXED,CURRENT
        }
    };     
}

Enum類的構造器,在感應到enum關鍵字修飾的類以後,將會被調用,傳入枚舉常量的字符串字面量值(name)和索引(ordinal),建立的實例存在私有靜態域&VALUES中。

並且編譯器確實會添加靜態的values()方法,用以返回存放枚舉常量的數組。

7、枚舉類實現單例

public enum  EnumSingleton {
    INSTANCE;
    public EnumSingleton getInstance(){
        return INSTANCE;
    }
}

這部分等到之後總結單例模式再侃,先在文末貼個地址。

8、參考資料

經過javap命令分析java彙編指令
Java中的枚舉與values()方法

相關文章
相關標籤/搜索