Java列舉解讀

DOONDO 發表於 2020-09-13

Java列舉

列舉類概念的理解與定義

  • 一個類的物件是有限個,確定的,我們稱此為列舉類。
  • 當需要定義和維護一組常量時,強烈建議使用列舉類。
  • 如果一個列舉類中只有一個物件,則可以作為單例模式的實現方式。
通俗的說:一個類被設計為包含固定例項數量的特殊類,我們給他的定義是列舉類。

注意:
     1.列舉類不能被 new 出來,列舉類因為預設的類修飾符為 final 所以也不能被派生(繼承),同理列舉類也不能為當作實現。
     2.列舉類自身可以實現介面,既可以進行統一實現重寫介面抽象方法,也可以按照列舉型別單個實現重寫。

列舉類的定義

關於列舉類的定義,這塊主要想和大家分享兩種方式

  1. jdk 5.0之前,自定義列舉類方式
  2. jdk 5.0之後,Enum關鍵字方式定義

實踐

一、準備工作

我們新建一個 Java Project ,並建立一個包,以及一個測試類
Java列舉解讀

二、自定義列舉的三種方式(jdk 5.0 之前)

1. 定義一個抽象類,在抽象類中定義常量進行維護,我們接下來以 Java 類庫中的 Calendar 類示例來進行說明

新建一個類 EnumDemo01.java 程式碼如下:

package org.taoguoguo;
import java.util.Calendar;

/**
 * @author taoGG
 * @description jdk 5.0 之前 抽象類列舉方案Demo
 * @create 2020-09-13 14:20
 */
public class EnumDemo01 {
    public static void main(String[] args) {
        Calendar calendar = Calendar.getInstance();
        System.out.println(calendar.get(1));
    }
}

Console 結果輸出:

2020
Process finished with exit code 0

如果熟悉 Calendar API 的小夥伴 應該馬上能反應過來,這個是獲取當前的年份,類似的值還有

3 - 一年中的第幾個星期
4 - 一年中的第幾個月
5 - 當前的日期 
......

但是這麼多值,我們怎麼能記得住呢?萬一我輸入錯誤,隨便取了一個範圍怎麼辦?

沒錯,這是 jdk 5.0之前的痛點,為了解決例項數量固定,便於維護這些問題,在jdk 5.0之後更新Enum列舉類解決了這個問題。那在jdk 5.0之前官方是怎麼做的呢?難道需要我們一個個去記住 Calendar 的數字?

實際上官方本身,採用的就是我們現在說的第一種方式,在抽象類中定義常量進行維護

現在我們將程式碼做些修改:

package org.taoguoguo;
import java.util.Calendar;

/**
 * @author taoGG
 * @description jdk 5.0 之前 抽象類列舉方案Demo
 * @create 2020-09-13 14:20
 */
public class EnumDemo01 {
    public static void main(String[] args) {
        Calendar calendar = Calendar.getInstance();
        System.out.println(calendar.get(Calendar.YEAR));
    }
}

我們執行進行輸出:

2020
Process finished with exit code 0

結果與之前一致,這時我們就清楚,在開發過程中作為開發者我們肯定願意使用 Calendar.YEAR 這種寫法,一來方便記憶,二來可讀性高。那麼官方的做法時怎樣的呢?我們點進去原始碼看一下

  1. 首先 Calendar 本身是一個抽象類,實現了序列化、克隆、以及比較排序介面,這邊和我們列舉沒有太大關係,我們繼續往下看
    Java列舉解讀

  2. 在抽象類中,定義了很多個靜態常量進行維護,而當我們需要使用時,直接呼叫,這樣就比我們寫一個個的具體值要方便和易用了。

    Java列舉解讀

2. 定義一個介面,在介面中定義常量維護列舉值

我們新建一個interface CustomerInf.java

package org.taoguoguo;

/**
 * @author taoGG
 * @description 介面常量維護列舉值
 * @create 2020-09-13 15:47
 */
public interface CustomerInf {
   int RED = 1;
   int GREEN = 2;
   int BLUE = 3;
}

EnumTest 進行測試

package org.taoguoguo;

/**
 * @author taoGG
 * @description Java列舉測試類
 * @create 2020-09-13 14:54
 *
 */
public class EnumTest {
    public static void main(String[] args) {
        System.out.println(CustomerInf.RED);
    }
}

測試結果:

1
Process finished with exit code 0

這種做法我們達到了和在抽象類中維護常量相同的目的。上面這兩種做法都非常的簡單易用,但也有弊端。比如我們只知道一個狀態值,當我們要獲取狀態的屬性或者相關的內容時,我們該怎麼做呢?

下面我們使用第三種方式,自定義列舉類,這種基本上達到和 Enum 關鍵字相同的作用,但有一點不足就是會較為複雜

3.自定義列舉類,通過為類私有化構造器和固定例項物件進行列舉維護

新建一個class SeasonEnum.java,程式碼如下:

package org.taoguoguo;

/**
 * @author taoGG
 * @description
 * @create 2020-09-13 15:58
 */
public class SeasonEnum {
    //1.宣告列舉物件的屬性
    private final String seasonName;
    private final int code;

    //2.私有化類的構造器
    private SeasonEnum(String seasonName,int code){
        this.seasonName = seasonName;
        this.code = code;
    }

    //3.提供當前列舉類的多個物件 public static final
    public static final SeasonEnum SPRING = new SeasonEnum("春天",100);
    public static final SeasonEnum SUMMER = new SeasonEnum("夏天",200);
    public static final SeasonEnum AUTUMN = new SeasonEnum("秋天",300);
    public static final SeasonEnum WINTER = new SeasonEnum("冬天",400);

    //4.為類提供獲取屬性的方法
    public String getSeasonName() {
        return seasonName;
    }
    public int getCode() {
        return code;
    }
    //5.重寫toString方法
    @Override
    public String toString() {
        return "SeasonEnum{" +
                "seasonName='" + seasonName + '\'' +
                ", code=" + code +
                '}';
    }
}

新建一個class SeasonEnumTest 進行測試,當我們通過自定義列舉類引用例項物件時,如下圖可以看到,我們已經可以獲取到我們的列舉物件了。
Java列舉解讀
獲取到列舉物件,我們當然也可以獲取到對應的屬性及方法,這種可用性就提高了很多,我們在開發程式進行判斷,可以根據各種列舉值的指定屬性來進行,提高了程式碼的可維護性。
Java列舉解讀
SeasonEnumTest 測試程式碼

package org.taoguoguo;

/**
 * @author taoGG
 * @description
 * @create 2020-09-13 16:04
 */
public class SeasonEnumTest {
    public static void main(String[] args) {
        SeasonEnum spring = SeasonEnum.SPRING;
        System.out.println("自定義列舉類物件:" + spring);
        System.out.println("自定義列舉類屬性:" + spring.getSeasonName());
        System.out.println("自定義列舉類屬性:" + spring.getCode());
    }
}

根據我們上面的自定義列舉類方式,我們基本已經實現了列舉的功能了,但是就像上面說到的,如果開發中列舉型別較多,開發多個這樣的自定義列舉類會非常的耗時,所以 jdk 5.0 之後,推出了 Enum 關鍵字定義列舉類

三、Enum 關鍵字定義列舉類(jdk 5.0之後)

enum 全稱為 enumeration,是jdk 5.0 中引入的新特性,在Java 中被 enum 關鍵字修飾的型別就是列舉型別

我們通過程式碼來示例來講解和理解 enum 的用法,還是用我們剛剛自定以列舉類的例子,看看使用enum如何來寫

新建一個Java class ,Kind 型別選擇 enum 如圖:
Java列舉解讀
列舉類建立注意:

  • 列舉例項必須在 enum關鍵字宣告的類中顯式的指定(首行開始的以第一個分號結束)
  • 列舉不允許使用new,clone,反射,序列化手動建立列舉例項
package org.taoguoguo;

/**
 * @author taoGG
 * @description
 * @create 2020-09-13 16:23
 */
public enum Season {
    SPRING("春天",100),
    SUMMER("夏天",200),
    AUTUMN("秋天",300),
    WINTER("冬天",400);

    private final String seasonName;
    private final int code;

    Season(String seasonName, int code){
        this.seasonName = seasonName;
        this.code = code;
    }

    public String getSeasonName() {
        return seasonName;
    }
    public int getCode() {
        return code;
    }
}

使用 SeasonTest 測試類進行測試:

package org.taoguoguo;

/**
 * @author taoGG
 * @description
 * @create 2020-09-13 16:27
 */
public class SeasonTest {
    public static void main(String[] args) {
        Season spring = Season.SPRING;
        System.out.println(spring);
    }
}

輸出結果:

SPRING
Process finished with exit code 0

注意,在enmu 列舉類中如果沒有重寫 toString方法,會預設使用Enum類本身提供的 toString 方法,返回列舉類名稱,因為定義的列舉類預設隱式繼承於java.lang.Enum

1.列舉類主要方法介紹

  • values()  :該方法可以返回當前列舉型別的物件陣列,可以很方便的遍歷所有列舉值。一般我們可以根據列舉類的相關屬性通過此方法遍歷獲取對應的列舉物件及列舉值
  • valueOf(String str) : 根據列舉類名稱獲取列舉類物件
  • toString(): 預設使用 java.lang.Enum的 toString方法,返回當前物件常量的名稱,列舉類推薦重寫返回自定義友好描述
  • name(): 返回當前列舉物件名稱,和toString作用上類似,當時toString支援重寫,name方法是不能重寫的,在本質上 toString 也是呼叫的 name方法,列舉定義 name 方法就是為了返回列舉物件名稱,而 toString 應該根據需要進行重寫
  • ordinal(): 返回當前列舉物件的序號, 實現了 Comparable 介面,表明它是支援排序的 可以通過 Collections.sort 進行自動排序比較此列舉與指定物件的順序
  • compareTo(): 基於ordinal進行序號大小比較

方式演示程式碼,小夥伴們可以自行執行輸出一下,看看各個方法的作用,熟悉一下相關的方法api

package org.taoguoguo;

/**
 * @author taoGG
 * @description
 * @create 2020-09-13 16:27
 */
public class SeasonTest {
    public static void main(String[] args) {
        System.out.println("========values()方法=======");
        for (Season season : Season.values()) {
            System.out.println(season);
        }
        System.out.println("===========================");
 
        System.out.println("========valueOf方法========");
        Season spring = Season.valueOf("SPRING");
        System.out.println(spring);
        System.out.println("===========================");

        System.out.println("========toString方法========");
        System.out.println(spring.toString());
        System.out.println("===========================");

        System.out.println("========name方法========");
        System.out.println(spring.name());
        System.out.println("===========================");

        System.out.println("========ordinal方法========");
        System.out.println(spring.ordinal());
        System.out.println("===========================");

        System.out.println("========compareTo方法========");
        System.out.println(spring.compareTo(Season.WINTER));
        System.out.println("===========================");
    }
}

2.列舉類對介面的實現方式

準備工作

新建一個EnumInf 介面,定義一個抽象方法

package org.taoguoguo;

/**
 * @author taoGG
 * @description
 * @create 2020-09-13 17:25
 */
public interface EnumInf {
    void show();
}

1.實現介面,在enum中統一實現抽象方法

新建一個EnumInf 介面,定義抽象方法 show()

package org.taoguoguo;

/**
 * @author taoGG
 * @description
 * @create 2020-09-13 17:25
 */
public interface EnumInf {
    void show();
}

新建一個OrderStatus 列舉類 實現 EnumInf 介面

package org.taoguoguo;

/**
 * @author taoGG
 * @description
 * @create 2020-09-13 17:27
 */
public enum OrderStatus implements EnumInf{

    SUCCESS(200,"交易成功"),
    Fail(500,"交易失敗");

    private final int code;
    private final String desc;

    OrderStatus(int code, String desc){
        this.code = code;
        this.desc = desc;
    }

    public int getCode() {
        return code;
    }
    public String getDesc() {
        return desc;
    }

    /**
     * 第一種方式,列舉統一重寫介面抽象方法
     */
    @Override
    public void show() {
        System.out.println("訂單列舉物件");
    }
}

進行測試

package org.taoguoguo;

/**
 * @author taoGG
 * @description
 * @create 2020-09-13 17:32
 */
public class OrderStatusTest {
    public static void main(String[] args) {
        OrderStatus success = OrderStatus.SUCCESS;
        success.show();
    }
}

輸出結果

訂單列舉物件

Process finished with exit code 0

跟我們常用類實現沒有什麼區別,列舉也是可以統一實現的,那如果想針對不同的列舉物件進行不同狀態的實現怎麼辦呢?比如我們的OA系統、或者電商系統中,根據不同狀態 我們需要回寫對應的資料,下面我們就來看看如何實現。

2.列舉物件分別實現介面中的抽象方法

案例跟介面統一實現一致,我們這邊修改一下OrderStatus 列舉類,程式碼如下

package org.taoguoguo;

/**
 * @author taoGG
 * @description
 * @create 2020-09-13 17:27
 */
public enum OrderStatus implements EnumInf{

    SUCCESS(200,"交易成功") {
        @Override
        public void show() {
            System.out.println("回寫交易成功狀態");
        }
    },
    Fail(500,"交易失敗") {
        @Override
        public void show() {
            System.out.println("回寫交易失敗狀態");
        }
    };

    private final int code;
    private final String desc;

    OrderStatus(int code, String desc){
        this.code = code;
        this.desc = desc;
    }

    public int getCode() {
        return code;
    }
    public String getDesc() {
        return desc;
    }

}

我們再修改下測試類程式碼:

package org.taoguoguo;

/**
 * @author taoGG
 * @description
 * @create 2020-09-13 17:32
 */
public class OrderStatusTest {
    public static void main(String[] args) {
        OrderStatus success = OrderStatus.SUCCESS;
        success.show();
        OrderStatus fail = OrderStatus.Fail;
        fail.show();
    }
}

輸出結果

回寫交易成功狀態
回寫交易失敗狀態

Process finished with exit code 0

通過這種方式就可以輕而易舉地定義每個列舉例項不同的行為方式,也達到了我們預期的效果,其實在開發過程中根據列舉的設計和設計模式的鋪墊可以極大的簡化我們的業務程式碼。

3.Enum列舉類的工具類及應用場景

1.EnumSet 和 EnumMap

Java 中提供了兩個方便操作enum的工具類——EnumSet 和 EnumMap。

EnumSet 是列舉型別的高效能 Set 實現。它要求放入它的列舉常量必須屬於同一列舉型別。

// EnumSet的使用
System.out.println("EnumSet展示");
EnumSet<OrderStatus> errSet = EnumSet.allOf(OrderStatus.class);
for (OrderStatus e : errSet) {
    System.out.println(e.name() + " : " + e.ordinal());
}

EnumMap 是專門為列舉型別量身定做的 Map 實現。雖然使用其它的 Map 實現(如HashMap)也能完成列舉型別例項到值得對映,但是使用 EnumMap 會更加高效:它只能接收同一列舉型別的例項作為鍵值,並且由於列舉型別例項的數量相對固定並且有限,所以 EnumMap 使用陣列來存放與列舉型別對應的值。(計算機處理連續的資源使用區域性記憶體效率更高)這使得 EnumMap 的效率非常高。

// EnumMap的使用
System.out.println("EnumMap展示");
EnumMap<StateMachine.Signal, String> errMap = new EnumMap(StateMachine.Signal.class);
errMap.put(StateMachine.Signal.RED, "紅燈");
errMap.put(StateMachine.Signal.YELLOW, "黃燈");
errMap.put(StateMachine.Signal.GREEN, "綠燈");
for (Iterator<Map.Entry<StateMachine.Signal, String>> iter =errMap.entrySet().iterator(); iter.hasNext();) {
    Map.Entry<StateMachine.Signal, String> entry = iter.next();
    System.out.println(entry.getKey().name() + " : " + entry.getValue());
}
2.列舉類與 Switch 的配合使用

關於列舉與switch是個比較簡單的話題,使用switch進行條件判斷時,條件引數一般只能是整型,字元型。而列舉型確實也被switch所支援,在java 1.7後switch也對字串進行了支援。

實踐

新建一個 BizEnum 的java class,程式碼如下

package org.taoguoguo;

/**
 * @author taoGG
 * @description 企業型別列舉
 * @create 2020-09-13 21:24
 */
public enum BizEnum {

    COUNTRIES(101,"國有企業"),

    PRIVETE(102,"私營企業"),

    SOHO(103,"個體單位");

    private final int code;
    private final String desc;

    BizEnum(int code, String desc){
        this.code = code;
        this.desc = desc;
    }

    public int getCode() {
        return code;
    }
    public String getDesc() {
        return desc;
    }

    //根據編碼獲取當前列舉物件的方法
    public static BizEnum getBizTypeByCode(int code){
        for (BizEnum bizEnum : BizEnum.values()) {
            if(code == bizEnum.getCode()){
                return bizEnum;
            }
        }
        return null;
    }
}

結合Switch進行測試

package org.taoguoguo;

/**
 * @author taoGG
 * @description
 * @create 2020-09-13 21:31
 */
public class BizTest {
    public static void main(String[] args) {
        BizEnum bizType = BizEnum.getBizTypeByCode(101);
        switch (bizType){
            case COUNTRIES:
                System.out.println("國有企業");
                break;
            case PRIVETE:
                System.out.println("私營企業");
                break;
            case SOHO:
                System.out.println("個體單位");
                break;
            default:
                System.out.println("創業中");
        }
    }
}

輸出結果:

國有企業

Process finished with exit code 0

總結

  1. jdk 5.0之前我們可以自定義列舉類,jdk 5.0之後使用enum關鍵字定義列舉類,列舉類預設繼承自java.lang.Enum,使用列舉類將常量組織起來,便於統一管理。例如錯誤碼狀態機等場景中,較為合適使用列舉類。
  2. 列舉類常用方法介紹及列舉類實現抽象類、介面等抽象方法的兩種方式。
  3. 列舉常用的工具類及與switch使用的場景。