1. 程式人生 > >Java:列舉類也就這麼回事

Java:列舉類也就這麼回事

目錄

  • 一、前言
  • 二、源自一道面試題
  • 三、列舉的由來
  • 四、列舉的定義形式
  • 五、Enum類裡有啥?
    • 1、唯一的構造器
    • 2、重要的方法們
    • 3、憑空出現的values()方法
  • 六、反編譯列舉類
  • 七、列舉類實現單例
  • 八、參考資料

一、前言

本篇部落格是對JDK1.5的新特性列舉的一波小小的總結,主要是昨天在看一部分面試題的時候,遇到了列舉型別的題目,發現自己有許多細節還需要加強,做起來都模稜兩可,是時候總結一波了。

二、源自一道面試題

不多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

至於結果為啥是這個,且看我慢慢總結。

三、列舉的由來

存在即合理。

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

列舉一定有他存在的價值,在一些時候,我們需要定義一個類,這個類中的物件是有限且固定的,比如我們一年有四個季節,春夏秋冬。

在列舉被支援之前,我們該如何定義這個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類可以完成我們的預期,它們各自代表一個例項,且不能被改變,外部也不能隨便建立例項。

但,通過自定義類實現列舉的效果有個顯著的問題:程式碼量非常大。

於是,JDK1.5,列舉類應運而生。

四、列舉的定義形式

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語句進行了擴充套件,其控制表示式可以是任意列舉型別,且可以直接使用列舉值的名稱,無需新增列舉類作為限定。

五、Enum類裡有啥?

Enum類是所有enum關鍵字修飾的列舉類的頂級父類,裡頭定義的方法預設情況下,是通用的,我們來瞅它一瞅:

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

我們可以發現,Enum其實是一個繼承自Object類的抽象類(Object類果然是頂級父類,不可撼動),並實現了兩個介面:

  • Comparable:支援列舉物件的比較。
  • Serializable:支援列舉物件的序列化。

1、唯一的構造器

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

官方文件這樣說的:程式設計師不能去呼叫這個構造器,它用於編譯器響應enum型別聲明發出的程式碼,關於這一點,我們後面體會會更加深刻一些。

2、重要的方法們

關於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)

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

3、憑空出現的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()方法,它將返回一個數組,按照列舉常量宣告的順序存放它們。

這樣一來,列舉類就可以和集合等玩意兒很好地配合在一起了,具體咋配合,以後遇到了就知道了。

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

六、反編譯列舉類

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

那麼,回到我們文章開頭提到的那到面試題,我們根據結果來推測程式執行之後發生的情況:

  • 其中的構造器被呼叫了三次,說明定義的列舉常量確實是三個活生生的例項,也就是說,每次建立例項就會呼叫一次構造器。
  • 然後,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()方法,用以返回存放列舉常量的陣列。

七、列舉類實現單例

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

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

八、參考資料

通過javap命令分析java彙編指令
Java中的列舉與values()