教妹學(xué)Java 第 45 講:枚舉
“今天我們來學(xué)習(xí)枚舉吧,三妹!”我說,“同學(xué)讓你去她家玩了兩天,感覺怎么樣呀?”
“心情放松了不少。”三妹說,“可以開始學(xué) Java 了,二哥。”
“OK。”
“枚舉(enum),是 Java 1.5 時引入的關(guān)鍵字,它表示一種特殊類型的類,繼承自 java.lang.Enum。”
“我們來新建一個枚舉 PlayerType。”
public enum PlayerType {
TENNIS,
FOOTBALL,
BASKETBALL
}
“二哥,我沒看到有繼承關(guān)系呀!”
“別著急,看一下反編譯后的字節(jié)碼,你就明白了。”
public final class PlayerType extends Enum
{
public static PlayerType[] values()
{
return (PlayerType[])$VALUES.clone();
}
public static PlayerType valueOf(String name)
{
return (PlayerType)Enum.valueOf(com/cmower/baeldung/enum1/PlayerType, name);
}
private PlayerType(String s, int i)
{
super(s, i);
}
public static final PlayerType TENNIS;
public static final PlayerType FOOTBALL;
public static final PlayerType BASKETBALL;
private static final PlayerType $VALUES[];
static
{
TENNIS = new PlayerType("TENNIS", 0);
FOOTBALL = new PlayerType("FOOTBALL", 1);
BASKETBALL = new PlayerType("BASKETBALL", 2);
$VALUES = (new PlayerType[] {
TENNIS, FOOTBALL, BASKETBALL
});
}
}
“看到?jīng)]?Java 編譯器幫我們做了很多隱式的工作,不然手寫一個枚舉就沒那么省心省事了。”
要繼承 Enum 類; 要寫構(gòu)造方法; 要聲明靜態(tài)變量和數(shù)組; 要用 static 塊來初始化靜態(tài)變量和數(shù)組; 要提供靜態(tài)方法,比如說 values()和valueOf(String name)。
“確實,作為開發(fā)者,我們的代碼量減少了,枚舉看起來簡潔明了。”三妹說。
“既然枚舉是一種特殊的類,那它其實是可以定義在一個類的內(nèi)部的,這樣它的作用域就可以限定于這個外部類中使用。”我說。
public class Player {
private PlayerType type;
public enum PlayerType {
TENNIS,
FOOTBALL,
BASKETBALL
}
public boolean isBasketballPlayer() {
return getType() == PlayerType.BASKETBALL;
}
public PlayerType getType() {
return type;
}
public void setType(PlayerType type) {
this.type = type;
}
}
PlayerType 就相當(dāng)于 Player 的內(nèi)部類。
由于枚舉是 final 的,所以可以確保在 Java 虛擬機(jī)中僅有一個常量對象,基于這個原因,我們可以使用“==”運算符來比較兩個枚舉是否相等,參照 isBasketballPlayer() 方法。
“那為什么不使用 equals() 方法判斷呢?”三妹問。
if(player.getType().equals(Player.PlayerType.BASKETBALL)){};
“我來給你解釋下。”
“==”運算符比較的時候,如果兩個對象都為 null,并不會發(fā)生 NullPointerException,而 equals() 方法則會。
另外, “==”運算符會在編譯時進(jìn)行檢查,如果兩側(cè)的類型不匹配,會提示錯誤,而 equals() 方法則不會。

“枚舉還可用于 switch 語句,和基本數(shù)據(jù)類型的用法一致。”我說。
switch (playerType) {
case TENNIS:
return "網(wǎng)球運動員費德勒";
case FOOTBALL:
return "足球運動員C羅";
case BASKETBALL:
return "籃球運動員詹姆斯";
case UNKNOWN:
throw new IllegalArgumentException("未知");
default:
throw new IllegalArgumentException(
"運動員類型: " + playerType);
}
“如果枚舉中需要包含更多信息的話,可以為其添加一些字段,比如下面示例中的 name,此時需要為枚舉添加一個帶參的構(gòu)造方法,這樣就可以在定義枚舉時添加對應(yīng)的名稱了。”我繼續(xù)說。
public enum PlayerType {
TENNIS("網(wǎng)球"),
FOOTBALL("足球"),
BASKETBALL("籃球");
private String name;
PlayerType(String name) {
this.name = name;
}
}
“get 了吧,三妹?”
“嗯,比較好理解。”
“那接下來,我就來說點不一樣的。”
“來吧,我準(zhǔn)備好了。”
“EnumSet 是一個專門針對枚舉類型的 Set 接口(后面會講)的實現(xiàn)類,它是處理枚舉類型數(shù)據(jù)的一把利器,非常高效。”我說,“從名字上就可以看得出,EnumSet 不僅和 Set 有關(guān)系,和枚舉也有關(guān)系。”
“因為 EnumSet 是一個抽象類,所以創(chuàng)建 EnumSet 時不能使用 new 關(guān)鍵字。不過,EnumSet 提供了很多有用的靜態(tài)工廠方法。”

“來看下面這個例子,我們使用 noneOf() 靜態(tài)工廠方法創(chuàng)建了一個空的 PlayerType 類型的 EnumSet;使用 allOf() 靜態(tài)工廠方法創(chuàng)建了一個包含所有 PlayerType 類型的 EnumSet。”
public class EnumSetTest {
public enum PlayerType {
TENNIS,
FOOTBALL,
BASKETBALL
}
public static void main(String[] args) {
EnumSet<PlayerType> enumSetNone = EnumSet.noneOf(PlayerType.class);
System.out.println(enumSetNone);
EnumSet<PlayerType> enumSetAll = EnumSet.allOf(PlayerType.class);
System.out.println(enumSetAll);
}
}
“來看一下輸出結(jié)果。”
[]
[TENNIS, FOOTBALL, BASKETBALL]
有了 EnumSet 后,就可以使用 Set 的一些方法了,見下圖。

“除了 EnumSet,還有 EnumMap,是一個專門針對枚舉類型的 Map 接口的實現(xiàn)類,它可以將枚舉常量作為鍵來使用。EnumMap 的效率比 HashMap 還要高,可以直接通過數(shù)組下標(biāo)(枚舉的 ordinal 值)訪問到元素。”
“和 EnumSet 不同,EnumMap 不是一個抽象類,所以創(chuàng)建 EnumMap 時可以使用 new 關(guān)鍵字。”
EnumMap<PlayerType, String> enumMap = new EnumMap<>(PlayerType.class);
有了 EnumMap 對象后就可以使用 Map 的一些方法了,見下圖。

和 HashMap(后面會講)的使用方法大致相同,來看下面的例子。
EnumMap<PlayerType, String> enumMap = new EnumMap<>(PlayerType.class);
enumMap.put(PlayerType.BASKETBALL,"籃球運動員");
enumMap.put(PlayerType.FOOTBALL,"足球運動員");
enumMap.put(PlayerType.TENNIS,"網(wǎng)球運動員");
System.out.println(enumMap);
System.out.println(enumMap.get(PlayerType.BASKETBALL));
System.out.println(enumMap.containsKey(PlayerType.BASKETBALL));
System.out.println(enumMap.remove(PlayerType.BASKETBALL));
“來看一下輸出結(jié)果。”
{TENNIS=網(wǎng)球運動員, FOOTBALL=足球運動員, BASKETBALL=籃球運動員}
籃球運動員
true
籃球運動員
“除了以上這些,《Effective Java》這本書里還提到了一點,如果要實現(xiàn)單例的話,最好使用枚舉的方式。”我說。
“等等二哥,單例是什么?”三妹沒等我往下說,就連忙問道。
“單例(Singleton)用來保證一個類僅有一個對象,并提供一個訪問它的全局訪問點,在一個進(jìn)程中。因為這個類只有一個對象,所以就不能再使用 new 關(guān)鍵字來創(chuàng)建新的對象了。”
“Java 標(biāo)準(zhǔn)庫有一些類就是單例,比如說 Runtime 這個類。”
Runtime runtime = Runtime.getRuntime();
“Runtime 類可以用來獲取 Java 程序運行時的環(huán)境。”
“關(guān)于單例,懂了些吧?”我問三妹。
“噢噢噢噢。”三妹點了點頭。
“通常情況下,實現(xiàn)單例并非易事,來看下面這種寫法。”
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
“要用到 volatile、synchronized 關(guān)鍵字等等,但枚舉的出現(xiàn),讓代碼量減少到極致。”
public enum EasySingleton{
INSTANCE;
}
“就這?”三妹睜大了眼睛。
“對啊,枚舉默認(rèn)實現(xiàn)了 Serializable 接口,因此 Java 虛擬機(jī)可以保證該類為單例,這與傳統(tǒng)的實現(xiàn)方式不大相同。傳統(tǒng)方式中,我們必須確保單例在反序列化期間不能創(chuàng)建任何新實例。”我說。
“好了,關(guān)于枚舉就講這么多吧,三妹,你把這些代碼都手敲一遍吧!”
“好勒,這就安排。二哥,你去休息吧。”
“嗯嗯。”講了這么多,必須跑去抽煙機(jī)那里安排一根華子了。
PS:點擊「閱讀原文」可直達(dá)《教妹學(xué)Java》專欄的在線閱讀地址,可以收藏夾伺候一波了!
