一、前言
本篇博客是對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
關鍵字用以定義枚舉類,這是一個和class
,interface
關鍵字地位相當的關鍵字。也就是說,枚舉類和我們之前使用的類差不太多,且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;
}
}
這部分等到以后總結單例模式再侃,先在文末貼個地址。