本文部分摘自 On Java 8
列舉型別
Java5 中新增了一個 enum 關鍵字,通過 enum 關鍵字,我們可以將一組擁有具名的值的有限集合建立為一種新的型別,這些具名的值可以作為常規的程式元件使用,例如:
public enum Spiciness {
NOT, MILD, MEDIUM, HOT, FLAMING
}
這裡建立了一個名為 Spiciness 的列舉型別,它有 5 個值。由於列舉型別的例項是常量,因此按照命名慣例,它們都用大寫字母表示(如果名稱中含有多個單詞,使用下劃線分隔)
要使用 enum,需要建立一個該型別的引用,然後將其賦值給某個例項:
public class SimpleEnumUse {
public static void main(String[] args) {
Spiciness howHot = Spiciness.MEDIUM;
System.out.println(howHot);
}
}
// 輸出:MEDIUM
在 switch 中使用 enum,是 enum 提供的一項非常便利的功能。一般來說,在 switch 中只能使用整數值,而列舉例項天生就具備整數值的次序,並且可以通過 ordinal() 方法取得其次序,因此我們可以在 switch 語句中使用 enum
一般情況下我們必須使用 enum 型別來修飾一個 enum 例項,但是在 case 語句中卻不必如此。下面的例子使用 enum 構造了一個模擬紅綠燈狀態變化:
enum Signal { GREEN, YELLOW, RED, }
public class TrafficLight {
Signal color = Signal.RED;
public void change() {
switch(color) {
case RED: color = Signal.GREEN;
break;
case GREEN: color = Signal.YELLOW;
break;
case YELLOW: color = Signal.RED;
break;
}
}
@Override
public String toString() {
return "The traffic light is " + color;
}
public static void main(String[] args) {
TrafficLight t = new TrafficLight();
for(int i = 0; i < 7; i++) {
System.out.println(t);
t.change();
}
}
}
列舉的基本特性
Java 中的每一個列舉都繼承自 java.lang.Enum 類,所有列舉例項都可以呼叫 Enum 類的方法
呼叫 enum 的 values() 方法,返回 enum 例項的陣列,而且該陣列中的元素嚴格保持其在 enum 中宣告時的順序,因此你可以在迴圈中使用 values() 返回的陣列
enum Shrubbery { GROUND, CRAWLING, HANGING }
public class EnumClass {
public static void main(String[] args) {
for(Shrubbery s : Shrubbery.values()) {
System.out.println(s);
// 返回每個列舉例項在宣告時的次序
System.out.println(s.ordinal());
// 返回與此列舉常量的列舉型別相對應的 Class 物件
System.out.println(s.getDeclaringClass());
// 返回列舉例項宣告時的名字,效果等同於直接列印
System.out.println(s.name());
...
}
}
}
// 輸出:
// GROUND
// 0
// GROUND
// CRAWLING
// 1
// CRAWLING
// HANGING
// 2
// HANGING
可以使用 == 來比較 enum 例項,編譯器會自動為你提供 equals() 和 hashCode() 方法。同時,Enum 類實現了 Comparable 介面,所以它具有 compareTo() 方法,同時,它還實現了 Serializable 介面
ValueOf() 方法是在 Enum 中定義的 static 方法,根據給定的名字返回相應的 enum 例項,如果不存在給定名字的例項,將丟擲異常
Shrubbery shrub = Enum.valueOf(Shrubbery.class, "HANGING");
我們再來看看 values() 方法,為什麼要說這個呢?前面提到,編譯器為你建立的 enum 類都繼承自 Enum 類。然而,如果你研究一下 Enum 類就會發現,它並沒有 values() 方法。可我們明明已經用過該方法了呀,難道是這個方法被藏起來了?答案是,values() 是由編譯器新增的 static 方法,編譯器還會為建立的列舉類標記為 static final,所以無法被繼承
由於 values() 方法是由編譯器插入到 enum 定義中的 static 方法,所以,如果你將 enum 例項向上轉型為 Enum,那麼 values() 方法就不可用了。不過,在 Class 中有一個 getEnumConstants() 方法,所以即便 Enum 介面中沒有 values() 方法,我們仍然可以通過 Class 物件取得所有 enum 例項
enum Search { HITHER, YON }
public class UpcastEnum {
public static void main(String[] args) {
for(Enum en : e.getClass().getEnumConstants())
System.out.println(en);
}
}
因為 getEnumConstants() 是 Class 上的方法,所以你甚至可以對不是列舉的類呼叫此方法,只不過,此時該方法返回 null
方法新增
除了不能繼承自一個 enum 之外,我們基本上可以將 enum 看作一個常規的類。也就是說我們可以向 enum 中新增方法。enum 甚至可以有 main() 方法
我們希望每個列舉例項能夠返回對自身的描述,而不僅僅只是預設的 toString() 實現,這隻能返回列舉例項的名字。為此,你可以提供一個構造器,專門負責處理這個額外的資訊,然後新增一個方法,返回這個描述資訊。看一看下面的示例:
public enum OzWitch {
WEST("Miss Gulch, aka the Wicked Witch of the West"),
NORTH("Glinda, the Good Witch of the North"),
EAST("Wicked Witch of the East, wearer of the Ruby "),
SOUTH("Good by inference, but missing"); // 必須在 enum 例項序列的最後新增一個分號
private String description;
private OzWitch(String description) {
this.description = description;
}
public String getDescription() { return description; }
public static void main(String[] args) {
for(OzWitch witch : OzWitch.values())
System.out.println(witch + ": " + witch.getDescription());
}
}
在這個例子中,我們有意識地將 enum 的構造器宣告為 private,但對於它的可訪問性而言,並沒有什麼變化,因為(即使不宣告為 private)我們只能在 enum 定義的內部使用其構造器建立 enum 例項。一旦 enum 的定義結束,編譯器就不允許我們再使用其構造器來建立任何例項了
如果希望覆蓋 enum 中的方法,例如覆蓋 toString() 方法,與覆蓋一般類的方法沒有區別:
public enum SpaceShip {
SCOUT, CARGO, TRANSPORT,
CRUISER, BATTLESHIP, MOTHERSHIP;
@Override
public String toString() {
String id = name();
String lower = id.substring(1).toLowerCase();
return id.charAt(0) + lower;
}
public static void main(String[] args) {
Stream.of(values()).forEach(System.out::println);
}
}
實現介面
我們已經知道,所有的 enum 都繼承自 Java.lang.Enum 類。由於 Java 不支援多重繼承,所以 enum 不能再繼承其他類,但可以實現一個或多個介面
enum CartoonCharacter implements Supplier<CartoonCharacter> {
SLAPPY, SPANKY, PUNCHY,
SILLY, BOUNCY, NUTTY, BOB;
private Random rand = new Random(47);
@Override
public CartoonCharacter get() {
return values()[rand.nextInt(values().length)];
}
}
通過實現一個供給型介面,就可以通過呼叫 get() 方法得到一個隨機的列舉值。我們可以藉助泛型,使隨機選擇這個工作更加一般化,成為一個通用的工具類
public class Enums {
private static Random rand = new Random(47);
public static <T extends Enum<T>> T random(Class<T> ec) {
return random(ec.getEnumConstants());
}
public static <T> T random(T[] values) {
return values[rand.nextInt(values.length)];
}
}
<T extends Enum
使用介面可以幫助我們實現將列舉元素分類的目的,舉例來說,假設你想用 enum 來表示不同類別的食物,同時還希望每個 enum 元素仍然保持 Food 型別,那可以這樣實現:
public interface Food {
enum Appetizer implements Food {
SALAD, SOUP, SPRING_ROLLS;
}
enum MainCourse implements Food {
LASAGNE, BURRITO, PAD_THAI,LENTILS, HUMMOUS, VINDALOO;
}
enum Dessert implements Food {
TIRAMISU, GELATO, BLACK_FOREST_CAKE,FRUIT, CREME_CARAMEL;
}
enum Coffee implements Food {
BLACK_COFFEE, DECAF_COFFEE, ESPRESSO,LATTE, CAPPUCCINO, TEA, HERB_TEA;
}
}
對於 enum 而言,實現介面是使其子類化的唯一辦法,所以嵌入在 Food 中的每個 enum 都實現了 Food 介面。現在,在下面的程式中,我們可以說“所有東西都是某種型別的 Food"
public class TypeOfFood {
public static void main(String[] args) {
Food food = Appetizer.SALAD;
food = MainCourse.LASAGNE;
food = Dessert.GELATO;
food = Coffee.CAPPUCCINO;
}
}
如果 enum 的數量太多,那麼一個介面中的程式碼量可能就很大。我們可以利用 getEnumConstants() 方法,根據某個 Class 物件取得某個 Food 子類的所有 enum 例項
public enum Course {
APPETIZER(Food.Appetizer.class),
MAINCOURSE(Food.MainCourse.class),
DESSERT(Food.Dessert.class),
COFFEE(Food.Coffee.class);
private Food[] values;
private Course(Class<? extends Food> kind) {
values = kind.getEnumConstants();
}
// 隨機獲取 Food 子類的某個 enum 例項
public Food randomSelection() {
return Enums.random(values);
}
}
如果對內部類熟悉的話,我們還可以使用更加簡潔的管理列舉的方式,就是將第一種方式中的介面巢狀在列舉裡,使得程式碼具有更清晰的結構
enum SecurityCategory {
STOCK(Security.Stock.class),
BOND(Security.Bond.class);
Security[] values;
SecurityCategory(Class<? extends Security> kind) {
values = kind.getEnumConstants();
}
interface Security {
enum Stock implements Security {
SHORT, LONG, MARGIN
}
enum Bond implements Security {
MUNICIPAL, JUNK
}
}
public Security randomSelection() {
return Enums.random(values);
}
public static void main(String[] args) {
for(int i = 0; i < 10; i++) {
SecurityCategory category = Enums.random(SecurityCategory.class);
System.out.println(category + ": " + category.randomSelection());
}
}
}
EnumSet
EnumSet 是一個用來操作 Enum 的集合,可以用來存放屬於同一列舉型別的列舉常量,其中元素存放的次序決定於 enum 例項定義時的次序。EnumSet 的設計初衷是為了替代傳統的基於 int 的“位標誌”。傳統的“位標誌”可以用來表示某種“開/關”資訊,不過,使用這種標誌,我們最終操作的只是一些 bit,而不是這些 bit 想要表達的概念,因此很容易寫出令人費解的程式碼
既然 EnumSet 要替代 bit 標誌,那麼它的效能應該要做到與使用 bit 一樣高效才對。EnumSet 的基礎是 long,一個 long 值有 64 位,一個 enum 例項只需一位 bit 表示其是否存在。 也就是說,在不超過一個 long 的表達能力的情況下,你的 EnumSet 可以應用於最多不超過 64 個元素的 enum。如果 enum 超過了 64 個元素,EnumSet 會在必要時增加一個 long
EnumSet 的方法如下:
方法 | 作用 |
---|---|
allOf(Class elementType) | 建立一個包含指定列舉類裡所有列舉值的 EnumSet 集合 |
complementOf(EnumSet e) | 建立一個元素型別與指定 EnumSet 裡元素型別相同的 EnumSet 集合,新 EnumSet 集合包含原 EnumSet 集合所不包含的的列舉值 |
copyOf(Collection c) | 使用一個普通集合來建立 EnumSet 集合 |
copyOf(EnumSet e) | 建立一個指定 EnumSet 具有相同元素型別、相同集合元素的 EnumSet 集合 |
noneOf(Class elementType) | 建立一個元素型別為指定列舉型別的空 EnumSet |
of(E first,E…rest) | 建立一個包含一個或多個列舉值的 EnumSet 集合,傳入的多個列舉值必須屬於同一個列舉類 |
range(E from,E to) | 建立一個包含從 from 列舉值到 to 列舉值範圍內所有列舉值的 EnumSet 集合 |
示例程式碼:
public enum AlarmPoints {
STAIR1, STAIR2, LOBBY, OFFICE1, OFFICE2, OFFICE3,
OFFICE4, BATHROOM, UTILITY, KITCHEN
}
public class EnumSets {
public static void main(String[] args) {
EnumSet<AlarmPoints> points = EnumSet.noneOf(AlarmPoints.class);
points.add(BATHROOM);
System.out.println(points);
points.addAll(EnumSet.of(STAIR1, STAIR2, KITCHEN));
System.out.println(points);
points = EnumSet.allOf(AlarmPoints.class);
points.removeAll(EnumSet.of(STAIR1, STAIR2, KITCHEN));
System.out.println(points);
points.removeAll(EnumSet.range(OFFICE1, OFFICE4));
System.out.println(points);
points = EnumSet.complementOf(points);
System.out.println(points);
}
}
EnumMap
EnumMap 是一種特殊的 Map,它要求 key 必須為列舉型別,value 沒有限制,底層由雙陣列實現(一個存放 key,另一個存放 value),同時,當 value 為 null 時會特殊處理為一個 Object 物件,和 EnumSet 一樣,元素存放的次序決定於 enum 例項定義時的次序
// key 型別
private final Class<K> keyType;
// key 陣列
private transient K[] keyUniverse;
// value 陣列
private transient Object[] vals;
// 鍵值對個數
private transient int size = 0;
// value 為 null 時對應的值
private static final Object NULL = new Object() {
public int hashCode() {
return 0;
}
public String toString() {
return "java.util.EnumMap.NULL";
}
};
由於 EnumMap 可以存放列舉型別,所以初始化時必須指定列舉型別,EnumMap 提供了三個建構函式
// 使用指定的鍵型別建立一個空的列舉對映
EnumMap(Class<K> keyType);
// 建立與指定的列舉對映相同的鍵型別的列舉對映,最初包含相同的對映(如果有)
EnumMap(EnumMap<K,? extends V> m);
// 建立從指定對映初始化的列舉對映
EnumMap(Map<K,? extends V> m);
除此之外,EnumMap 與普通 Map 在操作上沒有區別,EnumMap 的優點在於允許程式設計師改變值物件,而常量相關的方法在編譯期就被固定了
常量特定方法
我們可以為 enum 定義一個或多個 abstract 方法,然後為每個 enum 例項實現該抽象方法
public enum ConstantSpecificMethod {
DATE_TIME {
@Override
String getInfo() {
return DateFormat.getDateInstance().format(new Date());
}
},
CLASSPATH {
@Override
String getInfo() {
return System.getenv("CLASSPATH");
}
},
VERSION {
@Override
String getInfo() {
return System.getProperty("java.version");
}
};
abstract String getInfo();
public static void main(String[] args) {
for(ConstantSpecificMethod csm : values())
System.out.println(csm.getInfo());
}
}
在物件導向的程式設計中,不同的行為與不同的類關聯。通過常量相關的方法,每個 enum 例項可以具備自己獨特的行為,這似乎說明每個 enum 例項就像一個獨特的類。在上面的例子中,enum 例項似乎被當作其超類 ConstantSpecificMethod 來使用,在呼叫 getInfo() 方法時,體現出多型的行為
然而,enum 例項與類的相似之處也僅限於此了。我們並不能真的將 enum 例項作為一個型別來使用,因為每一個 enum 元素都是指定列舉型別的 static final 例項
除了 abstract 方法外,程式設計師還可以覆蓋普通方法
public enum OverrideConstantSpecific {
NUT, BOLT,
WASHER {
@Override
void f() {
System.out.println("Overridden method");
}
};
void f() {
System.out.println("default behavior");
}
public static void main(String[] args) {
for(OverrideConstantSpecific ocs : values()) {
System.out.print(ocs + ": ");
ocs.f();
}
}
}
多路分發
現在有一個數學表示式 Number.plus(Number),我們知道 Number 是各種數字物件的超類,假設有 a 和 b 兩個 Number 型別的物件,根據上述的表示式代入得 a.plus(b),但你現在只知道 a、b 屬於 Number 型別,具體是什麼數字你並不知道,有可能是整數、浮點數,根據不同的數字型別,執行數學操作後的結果應該也不一樣才對,怎麼讓它們正確地互動呢?
如果你瞭解 Java 多型就知道,Java 多型的實現依賴的是 Java 的動態繫結機制,在執行時發現物件的真實型別。但 Java 只支援單路分發,就是說如果要執行的操作包含不止一個型別未知的物件,那麼 Java 的動態繫結機制只能處理其中一個型別,a.plus(b) 涉及到兩個型別,自然無法解決我們的問題,所以我們必須自己來判定其他型別
解決問題的方法就是多路分發,上面的例子,由於只有兩個分發,一般稱為兩路分發。多型只能發生在方法呼叫時,所以,如果你想使用兩路分發,那麼就必須有兩個方法呼叫:第一個方法呼叫決定第一個未知型別,第二個方法呼叫決定第二個未知的型別。程式設計師必須設定好某種配置,以便一個方法呼叫能夠引出其他方法呼叫,從而在這個過程中處理多種型別
來看一個猜拳的例子:
package enums;
public enum Outcome { WIN, LOSE, DRAW } // 猜拳的結果:勝、負、平手
package enums;
import java.util.*;
import static enums.Outcome.*; // 引入 enums,這樣就不用寫字首 Outcome 了
interface Item {
Outcome compete(Item it);
Outcome eval(Paper p);
Outcome eval(Scissors s);
Outcome eval(Rock r);
}
class Paper implements Item {
@Override
public Outcome compete(Item it) {
return it.eval(this);
}
@Override
public Outcome eval(Paper p) { return DRAW; }
@Override
public Outcome eval(Scissors s) { return WIN; }
@Override
public Outcome eval(Rock r) { return LOSE; }
@Override
public String toString() { return "Paper"; }
}
class Scissors implements Item {
@Override
public Outcome compete(Item it) {
return it.eval(this);
}
@Override
public Outcome eval(Paper p) { return LOSE; }
@Override
public Outcome eval(Scissors s) { return DRAW; }
@Override
public Outcome eval(Rock r) { return WIN; }
@Override
public String toString() { return "Scissors"; }
}
class Rock implements Item {
@Override
public Outcome compete(Item it) {
return it.eval(this);
}
@Override
public Outcome eval(Paper p) { return WIN; }
@Override
public Outcome eval(Scissors s) { return LOSE; }
@Override
public Outcome eval(Rock r) { return DRAW; }
@Override
public String toString() { return "Rock"; }
}
public class RoShamBo1 {
static final int SIZE = 20;
private static Random rand = new Random(47);
public static Item newItem() {
switch(rand.nextInt(3)) {
default:
case 0: return new Scissors();
case 1: return new Paper();
case 2: return new Rock();
}
}
public static void match(Item a, Item b) {
System.out.println(
a + " vs. " + b + ": " + a.compete(b));
}
public static void main(String[] args) {
for(int i = 0; i < SIZE; i++)
match(newItem(), newItem());
}
}
上面就是多路分發的實現,它的好處在於避免判定多個物件的型別的冗餘程式碼,不過配置過程需要很多道工序。我們既然學習了列舉,自然可以考慮用列舉對程式碼進行優化
public interface Competitor<T extends Competitor<T>> {
Outcome compete(T competitor);
}
public class RoShamBo {
public static <T extends Competitor<T>> void match(T a, T b) {
System.out.println(a + " vs. " + b + ": " + a.compete(b));
}
public static <T extends Enum<T> & Competitor<T>>
void play(Class<T> rsbClass, int size) {
for(int i = 0; i < size; i++)
match(Enums.random(rsbClass),Enums.random(rsbClass));
}
}
public enum RoShamBo2 implements Competitor<RoShamBo2> {
PAPER(DRAW, LOSE, WIN),
SCISSORS(WIN, DRAW, LOSE),
ROCK(LOSE, WIN, DRAW);
private Outcome vPAPER, vSCISSORS, vROCK;
RoShamBo2(Outcome paper, Outcome scissors, Outcome rock) {
this.vPAPER = paper;
this.vSCISSORS = scissors;
this.vROCK = rock;
}
@Override
public Outcome compete(RoShamBo2 it) {
switch(it) {
default:
case PAPER: return vPAPER;
case SCISSORS: return vSCISSORS;
case ROCK: return vROCK;
}
}
public static void main(String[] args) {
RoShamBo.play(RoShamBo2.class, 20);
}
}
也可以使用將 enum 用在 switch 語句中
import static enums.Outcome.*;
public enum RoShamBo3 implements Competitor<RoShamBo3> {
PAPER {
@Override
public Outcome compete(RoShamBo3 it) {
switch(it) {
default:
case PAPER: return DRAW;
case SCISSORS: return LOSE;
case ROCK: return WIN;
}
}
},
SCISSORS {
@Override
public Outcome compete(RoShamBo3 it) {
switch(it) {
default:
case PAPER: return WIN;
case SCISSORS: return DRAW;
case ROCK: return LOSE;
}
}
},
ROCK {
@Override
public Outcome compete(RoShamBo3 it) {
switch(it) {
default:
case PAPER: return LOSE;
case SCISSORS: return WIN;
case ROCK: return DRAW;
}
}
};
@Override
public abstract Outcome compete(RoShamBo3 it);
public static void main(String[] args) {
RoShamBo.play(RoShamBo3.class, 20);
}
}
對上述程式碼還可以再壓縮一下
public enum RoShamBo4 implements Competitor<RoShamBo4> {
ROCK {
@Override
public Outcome compete(RoShamBo4 opponent) {
return compete(SCISSORS, opponent);
}
},
SCISSORS {
@Override
public Outcome compete(RoShamBo4 opponent) {
return compete(PAPER, opponent);
}
},
PAPER {
@Override
public Outcome compete(RoShamBo4 opponent) {
return compete(ROCK, opponent);
}
};
Outcome compete(RoShamBo4 loser, RoShamBo4 opponent) {
return ((opponent == this) ? Outcome.DRAW
: ((opponent == loser) ? Outcome.WIN
: Outcome.LOSE));
}
public static void main(String[] args) {
RoShamBo.play(RoShamBo4.class, 20);
}
}
使用 EnumMap 能夠實現真正的兩路分發。EnumMap 是為 enum 專門設計的一種效能非常好的特殊 Map。由於我們的目的是摸索出兩種未知的型別,所以可以用一個 EnumMap 的 EnumMap 來實現兩路分發:
package enums;
import java.util.*;
import static enums.Outcome.*;
enum RoShamBo5 implements Competitor<RoShamBo5> {
PAPER, SCISSORS, ROCK;
static EnumMap<RoShamBo5,EnumMap<RoShamBo5,Outcome>>
table = new EnumMap<>(RoShamBo5.class);
static {
for(RoShamBo5 it : RoShamBo5.values())
table.put(it, new EnumMap<>(RoShamBo5.class));
initRow(PAPER, DRAW, LOSE, WIN);
initRow(SCISSORS, WIN, DRAW, LOSE);
initRow(ROCK, LOSE, WIN, DRAW);
}
static void initRow(RoShamBo5 it,
Outcome vPAPER, Outcome vSCISSORS, Outcome vROCK) {
EnumMap<RoShamBo5,Outcome> row =
RoShamBo5.table.get(it);
row.put(RoShamBo5.PAPER, vPAPER);
row.put(RoShamBo5.SCISSORS, vSCISSORS);
row.put(RoShamBo5.ROCK, vROCK);
}
@Override
public Outcome compete(RoShamBo5 it) {
return table.get(this).get(it);
}
public static void main(String[] args) {
RoShamBo.play(RoShamBo5.class, 20);
}
}
我們還可以進一步簡化實現兩路分發的解決方案。我們注意到,每個 enum 例項都有一個固定的值(基於其宣告的次序),並且可以通過 ordinal() 方法取得該值。因此我們可以使用二維陣列,將競爭者對映到競爭結果。採用這種方式能夠獲得最簡潔、最直接的解決方案
package enums;
import static enums.Outcome.*;
enum RoShamBo6 implements Competitor<RoShamBo6> {
PAPER, SCISSORS, ROCK;
private static Outcome[][] table = {
{ DRAW, LOSE, WIN }, // PAPER
{ WIN, DRAW, LOSE }, // SCISSORS
{ LOSE, WIN, DRAW }, // ROCK
};
@Override
public Outcome compete(RoShamBo6 other) {
return table[this.ordinal()][other.ordinal()];
}
public static void main(String[] args) {
RoShamBo.play(RoShamBo6.class, 20);
}
}