Java:枚舉類也就這么回事


一、前言

本篇博客是對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()方法


免責聲明!

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



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