1. 程式人生 > 實用技巧 >A Guide to Java Enums[JAVA Enums 指南]

A Guide to Java Enums[JAVA Enums 指南]

from:https://www.baeldung.com/a-guide-to-java-enums

1. 概述

在這篇文章中,我們將看到什麼是Java 列舉,它解決了什麼問題,以及它們在實踐中的一些設計模式。

Java 5中引入了"enum"關鍵字。它表示一種特殊型別的類,該類始終擴充套件自java.lang.Enum類。有關其使用情況的官方文件,請檢視文件

這樣定義的常量使程式碼更具可讀性,允許編譯時檢查,預先記錄接受值的列表,避免由於傳遞無效值而導致的意外。

下面是一個快速而簡單的示例,用於定義比薩餅訂單的狀態;訂單狀態可以訂購就緒交付

public enum PizzaStatus {
    ORDERED,
//已訂購 READY, //已準備 DELIVERED; //已交付 }

此外,它們還具有許多有用的方法,如果您使用傳統的公共靜態最終常量(public static final constants),你必須自己編寫這些方法。

2. 自定義分分方法

好了,現在我們基本瞭解了什麼是項級以及如何使用它們,讓我們通過定義一些額外的 API 方法,將前面的示例放在下一個級別:

public class Pizza {
    private PizzaStatus status;
    public enum PizzaStatus {
        ORDERED,
        READY,
        DELIVERED;
    }
 
    
public boolean isDeliverable() { if (getStatus() == PizzaStatus.READY) { return true; } return false; } // Methods that set and get the status variable. }

3. 使用"=="運算子比較列舉型別

由於 enum 型別確保 JVM 中只存在一個常量例項,因此我們可以如上安全地使用"=="運算子比較兩個變數,"=="運算子還提供編譯時和執行時安全。

讓我們首先看執行時安全,其中"=="運算子用於比較狀態,如果任一值為 null ,則不會引發NullPointerException

。相反,如果使用等值方法(equals),將引發 NullPointerException:

if(testPz.getStatus().equals(Pizza.PizzaStatus.DELIVERED)); //may null
if(testPz.getStatus() == Pizza.PizzaStatus.DELIVERED);//not null exception

至於編譯時安全,讓我們看另一個示例,其中equals 方法比較不同型別的值可能是true - 因為列舉值與getStatus 方法的值恰好一樣,但從邏輯上講,這個比較是錯誤的(型別不同應該是false)。使用"=="可避免此問題。

編譯器將比較標記為不相容錯誤:

if(testPz.getStatus().equals(TestColor.GREEN));
if(testPz.getStatus() == TestColor.GREEN);

4. 在switch語句中使用列舉型別

Enum 型別也可用於switch語句:

public int getDeliveryTimeInDays() {
switch (status) {
case ORDERED: return 5;
case READY: return 2;
case DELIVERED: return 0;
}
return 0;
}

5. 在欄位、方法和建構函式中的列舉

您可以在建構函式、方法和欄位中定義列舉,使其非常強大。

讓我們擴充套件上面的示例,實現披薩狀態的過渡,並看看我們如何擺脫之前使用的 if語句和 switch語句:

public class Pizza {
private PizzaStatus status;
public enum PizzaStatus {
ORDERED (5){
@Override
public boolean isOrdered() {
return true;
}
},
READY (2){
@Override
public boolean isReady() {
return true;
}
},
DELIVERED (0){
@Override
public boolean isDelivered() {
return true;
}
};
private int timeToDelivery;
public boolean isOrdered() {return false;}
public boolean isReady() {return false;}
public boolean isDelivered(){return false;}
public int getTimeToDelivery() {
return timeToDelivery;
}
PizzaStatus (int timeToDelivery) {
this.timeToDelivery = timeToDelivery;
}
}
public boolean isDeliverable() {
return this.status.isReady();
}
public void printTimeToDeliver() {
System.out.println("Time to delivery is " +
this.getStatus().getTimeToDelivery());
}
// Methods that set and get the status variable.
}

下面的測試片段演示了它是如何工作的:

@Test
public void givenPizaOrder_whenReady_thenDeliverable() {
Pizza testPz = new Pizza();
testPz.setStatus(Pizza.PizzaStatus.READY);
assertTrue(testPz.isDeliverable());
}

6.列舉集(EnumSet)和列舉圖(EnumMap)

6.1.EnumSet

EnumSet 是專用的 Set實現,用於與 Enum 型別一起使用。

與雜湊集相比,由於內部使用了位向量(internalBit Vector)表示,因此它是高效緊湊的Enum常數表示形式。它安全地替代了傳統基於int 形的位標記“bit flags”,使我們能夠編寫易讀易維護的程式碼。

EnumSet是一個抽象類,它有兩個實現:"RegularEnumSet""JumboEnumSet",它根據例項化時依賴的常量數來選擇。

因此我們最好在需要集合常量時使用EnumSet(如子設定、新增、刪除和批量操作(如"包含 All""刪除全部")以及使用 Enum.values ()

在下面的程式碼展示如何使用EnumSet建立常量子集:

public class Pizza {
private static EnumSet<PizzaStatus> undeliveredPizzaStatuses =
EnumSet.of(PizzaStatus.ORDERED, PizzaStatus.READY);
private PizzaStatus status;
public enum PizzaStatus {
...
}
public boolean isDeliverable() {
return this.status.isReady();
}
public void printTimeToDeliver() {
System.out.println("Time to delivery is " +
this.getStatus().getTimeToDelivery() + " days");
}
public static List<Pizza> getAllUndeliveredPizzas(List<Pizza> input) {
return input.stream().filter(
(s) -> undeliveredPizzaStatuses.contains(s.getStatus()))
.collect(Collectors.toList());
}
public void deliver() {
if (isDeliverable()) {
PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy()
.deliver(this);
this.setStatus(PizzaStatus.DELIVERED);
}
}
// Methods that set and get the status variable.
}

執行以下測試演示了 Set 介面的EnumSet實現功能

@Test
public void givenPizaOrders_whenRetrievingUnDeliveredPzs_thenCorrectlyRetrieved() {
List<Pizza> pzList = new ArrayList<>();
Pizza pz1 = new Pizza();
pz1.setStatus(Pizza.PizzaStatus.DELIVERED);
Pizza pz2 = new Pizza();
pz2.setStatus(Pizza.PizzaStatus.ORDERED);
Pizza pz3 = new Pizza();
pz3.setStatus(Pizza.PizzaStatus.ORDERED);
Pizza pz4 = new Pizza();
pz4.setStatus(Pizza.PizzaStatus.READY);
pzList.add(pz1);
pzList.add(pz2);
pzList.add(pz3);
pzList.add(pz4);
List<Pizza> undeliveredPzs = Pizza.getAllUndeliveredPizzas(pzList);
assertTrue(undeliveredPzs.size() == 3);
}

6.2. EnumMap

EnumMap是一個專門的對映實現,用於將項值用作鍵。與對應的HashMap相比,它是一種高效緊湊的實現,在內部表示為陣列:

EnumMap<Pizza.PizzaStatus, Pizza> map;

讓我們快速瞭解一個真實示例,說明如何在實踐中使用它:

public static EnumMap<PizzaStatus, List<Pizza>>
groupPizzaByStatus(List<Pizza> pizzaList) {
EnumMap<PizzaStatus, List<Pizza>> pzByStatus =
new EnumMap<PizzaStatus, List<Pizza>>(PizzaStatus.class);
for (Pizza pz : pizzaList) {
PizzaStatus status = pz.getStatus();
if (pzByStatus.containsKey(status)) {
pzByStatus.get(status).add(pz);
} else {
List<Pizza> newPzList = new ArrayList<Pizza>();
newPzList.add(pz);
pzByStatus.put(status, newPzList);
}
}
return pzByStatus;
}

執行以下測試演示了對映介面的 EnumMap實現功能

@Test
public void givenPizaOrders_whenGroupByStatusCalled_thenCorrectlyGrouped() {
List<Pizza> pzList = new ArrayList<>();
Pizza pz1 = new Pizza();
pz1.setStatus(Pizza.PizzaStatus.DELIVERED);
Pizza pz2 = new Pizza();
pz2.setStatus(Pizza.PizzaStatus.ORDERED);
Pizza pz3 = new Pizza();
pz3.setStatus(Pizza.PizzaStatus.ORDERED);
Pizza pz4 = new Pizza();
pz4.setStatus(Pizza.PizzaStatus.READY);
pzList.add(pz1);
pzList.add(pz2);
pzList.add(pz3);
pzList.add(pz4);
EnumMap<Pizza.PizzaStatus,List<Pizza>> map = Pizza.groupPizzaByStatus(pzList);
assertTrue(map.get(Pizza.PizzaStatus.DELIVERED).size() == 1);
assertTrue(map.get(Pizza.PizzaStatus.ORDERED).size() == 2);
assertTrue(map.get(Pizza.PizzaStatus.READY).size() == 1);
}

7. 使用分號實現設計模式

7.1. 單頓模式

通常,使用單頓模式實現類是相當不平凡的。Enums 提供了一種實現單元的快速簡單方法。

此外,由於 enum 類在機罩下實現可序列化介面,因此 JVM 保證該類是單例,這與常規實現不同,在非序列化期間,我們必須確保不建立任何新例項。

在下面的程式碼片段中,我們將瞭解如何實現單元模式:

public enum PizzaDeliverySystemConfiguration {
INSTANCE;
PizzaDeliverySystemConfiguration() {
// Initialization configuration which involves
// overriding defaults like delivery strategy
}
private PizzaDeliveryStrategy deliveryStrategy = PizzaDeliveryStrategy.NORMAL;
public static PizzaDeliverySystemConfiguration getInstance() {
return INSTANCE;
}
public PizzaDeliveryStrategy getDeliveryStrategy() {
return deliveryStrategy;
}
}

7.2. 戰略模式

通常,策略模式是通過具有由不同類實現的介面編寫的。

新增新策略意味著新增新的實現類。使用"一元",只需減少工作量,新增新實現意味著只需使用某種實現定義另一個例項。

下面的程式碼段顯示瞭如何實現策略模式:

public enum PizzaDeliveryStrategy {
EXPRESS {
@Override
public void deliver(Pizza pz) {
System.out.println("Pizza will be delivered in express mode");
}
},
NORMAL {
@Override
public void deliver(Pizza pz) {
System.out.println("Pizza will be delivered in normal mode");
}
};
public abstract void deliver(Pizza pz);
}

將以下方法新增到Pizza類:

public void deliver() {
if (isDeliverable()) {
PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy()
.deliver(this);
this.setStatus(PizzaStatus.DELIVERED);
}
}
@Test
public void givenPizaOrder_whenDelivered_thenPizzaGetsDeliveredAndStatusChanges() {
Pizza pz = new Pizza();
pz.setStatus(Pizza.PizzaStatus.READY);
pz.deliver();
assertTrue(pz.getStatus() == Pizza.PizzaStatus.DELIVERED);
}

8. Java 8 和 enum

在 Java8 中可以重寫 Pizza 類,您可以看到方法如何獲得全交付披薩() 和組披薩比狀態()變得如此簡潔,使用 lambdas和流API:

public static List<Pizza> getAllUndeliveredPizzas(List<Pizza> input) {
return input.stream().filter(
(s) -> !deliveredPizzaStatuses.contains(s.getStatus()))
.collect(Collectors.toList());
}
public static EnumMap<PizzaStatus, List<Pizza>>
groupPizzaByStatus(List<Pizza> pzList) {
EnumMap<PizzaStatus, List<Pizza>> map = pzList.stream().collect(
Collectors.groupingBy(Pizza::getStatus,
() -> new EnumMap<>(PizzaStatus.class), Collectors.toList()));
return map;
}

9. JSON 代表 Enum

使用 Jackson 庫,可以像 POJOs 一樣具有重則小的 JSON 表示型別。下面的程式碼段顯示了可用於相同的傑克遜註釋:

@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum PizzaStatus {
ORDERED (5){
@Override
public boolean isOrdered() {
return true;
}
},
READY (2){
@Override
public boolean isReady() {
return true;
}
},
DELIVERED (0){
@Override
public boolean isDelivered() {
return true;
}
};
private int timeToDelivery;
public boolean isOrdered() {return false;}
public boolean isReady() {return false;}
public boolean isDelivered(){return false;}
@JsonProperty("timeToDelivery")
public int getTimeToDelivery() {
return timeToDelivery;
}
private PizzaStatus (int timeToDelivery) {
this.timeToDelivery = timeToDelivery;
}
}

我們可以使用披薩和披薩統計如下:

Pizza pz = new Pizza();
pz.setStatus(Pizza.PizzaStatus.READY);
System.out.println(Pizza.getJsonString(pz));

生成披薩狀態的以下 JSON表示形式

{
"status" : {
"timeToDelivery" : 2,
"ready" : true,
"ordered" : false,
"delivered" : false
},
"deliverable" : true
}

有關億萬類的 JSON 序列化/去序列化(包括自定義)的資訊,請參閱 Jackson + 序列化億萬作為 JSON 物件

10. 結論

在這篇文章中,我們探討了Java的例會,從語言基礎知識到更高階和更有趣的實際用例。

本文中的程式碼段可以在Github 儲存庫中找到。

我剛剛宣佈了新的學習春季課程