語法糖甜不甜?巧用列舉實現“狀態”轉換限制

阿Q說程式碼發表於2021-10-14

語法糖

語法糖(Syntactic sugar),也被譯為糖衣語法,是由英國電腦科學家彼得·約翰·蘭達(Peter J. Landin)發明的一個術語,指計算機語言中新增的某種語法,這種語法對語言的功能並沒有影響,但是更方便程式設計師使用。通常來說使用語法糖能夠增加程式的可讀性,從而減少程式程式碼出錯的機會。——摘抄自百度百科

本質上,JVM 並不支援語法糖,語法糖只存在於編譯期。當編譯器將 .java 原始檔編譯成 .class 位元組碼檔案時,會進行解語法糖的操作,來還原最原始的基礎語法結構。

我們所熟悉的程式語言中幾乎都會包含語法糖,當然 JAVA 也不例外。JAVA 中的語法糖包含條件編譯斷言switch 支援 String 與列舉可變引數自動裝箱/拆箱列舉內部類泛型擦除增強for迴圈lambda表示式try-with-resources等等。今天我們先來了解下列舉

列舉類

JDK5 提供了一種新的特殊的類——列舉類,一般在類物件有限且固定的場景下使用,用來替代類中定義常量的方式。列舉相較於常量更加直觀且型別安全。

列舉類的使用非常簡單,用 enum 關鍵字來定義,多個列舉變數直接用逗號隔開。我們先來定義一個簡單的列舉類 OrderStatus.java

public enum OrderStatus {
    //未支付、已支付、退款中、退款成功、退款失敗;
    NO_PAY, PAY, REFUNDING, REFUNDED, FAIL_REFUNDED, ;
}

在其他類中使用 enum 變數的時候,只需要【類名.變數名】就可以了,和使用靜態變數一樣。另外,列舉型別可以確保 JVM 中僅存在一個常量例項,所以我們可以放心的使用“ ==”來比較兩個變數。

注意事項:

  1. 列舉類的第一行必須是列舉項,最後一個列舉項後的分號是可以省略的,但是如果列舉類有其它的東西,這個分號就不能省略。建議不要省略!
  2. 列舉變數最好大寫,多個單詞之間使用”_”隔開(比如:NO_PAY)。

反編譯

我們可以先通過 javac 命令或者 IDEA 的編譯功能將OrderStatus.java 編譯為OrderStatus.class 位元組碼檔案,然後用DJ Java Decompiler 反編譯器對 .class 檔案進行反編譯。

如果需要 DJ Java Decompiler 反編譯器的小夥伴可以私信阿Q獲取!

public final class OrderStatus extends Enum
{

    //該方法會返回包括所有列舉變數的陣列,可以方便的用來做迴圈。
    public static OrderStatus[] values()
    {
        return (OrderStatus[])$VALUES.clone();
    }

    //根據傳入的字串,轉變為對應的列舉變數。
    //前提是傳的字串和定義列舉變數的字串一抹一樣,區分大小寫。
    //如果傳了一個不存在的字串,那麼會丟擲異常。
    public static OrderStatus valueOf(String name)
    
{
        return (OrderStatus)Enum.valueOf(com/itcast/java/enumpack/OrderStatus, name);
    }

    private OrderStatus(String s, int i)
    
{
        super(s, i);
    }

    public static final OrderStatus NO_PAY;
    public static final OrderStatus PAY;
    public static final OrderStatus REFUNDING;
    public static final OrderStatus REFUNDED;
    public static final OrderStatus FAIL_REFUNDED;
    private static final OrderStatus $VALUES[];

    static 
    {
        NO_PAY = new OrderStatus("NO_PAY"0);
        PAY = new OrderStatus("PAY"1);
        REFUNDING = new OrderStatus("REFUNDING"2);
        REFUNDED = new OrderStatus("REFUNDED"3);
        FAIL_REFUNDED = new OrderStatus("FAIL_REFUNDED"4);
        $VALUES = (new OrderStatus[] {
            NO_PAY, PAY, REFUNDING, REFUNDED, FAIL_REFUNDED
        });
    }
}

如原始碼所示:

  • 編譯器會自動幫我們建立一個 final 型別的類繼承 Enum 類,所以列舉類不能被繼承。
  • 會自動生成私有構造方法,當然我們也可以定義構造方法,但必須是私有的,這樣就不能在別處宣告此類的物件了。
  • 列舉項會被自動新增 public static final 修飾,並定義為 OrderStatus 型別,並在靜態程式碼塊中被初始化。
  • 並提供了 values()valueOf(String name) 的靜態方法。

我們定義的列舉變數實際上是編譯器幫我們自動生成了建構函式。

所有列舉類都是 Enum 的子類,列舉類可以實現一個或多個介面。

Enum

Enum 是所有 Java 語言列舉型別的公共基類,實現了 Comparable 和 Serializable 介面。它包含 final 型別的 name 和 ordinal (此列舉常量的序號,從0開始)屬性,下面我們來了解下它的方法

  • protected Enum(String name, int ordinal);——構造方法;
  • public String toString();——返回 name 欄位,即列舉定義列舉變數的字串;
  • protected final Object clone();——丟擲 CloneNotSupportedException 異常,保證列舉類永遠不會被克隆;
  • public final Class getDeclaringClass();——返回與此列舉常量的列舉型別對應的類物件;
  • protected final void finalize();—— 列舉類不能有 finalize 方法;
  • readObject(ObjectInputStream in);& readObjectNoData();—— 丟擲InvalidObjectException 異常,防止預設反序列化;

擴充套件

  1. 列舉類中可以自定義屬性

    自定義的屬性值最好用 private final 修飾,防止生成的 set 方法在使用時修改屬性值,使程式碼更加安全。

  2. 列舉類中可以自定義建構函式

    建構函式必須為 private 修飾,防止在別處宣告此類物件。

  3. 列舉類可以自定義方法,列舉項可以選擇性覆蓋自定義的方法。

    public enum OrderStatus{
        NO_PAY("未支付",0),
        PAY("已支付",1){
            @Override
            public void printOrderStatus() {
                System.out.println("已支付");
            }
        },
        REFUNDING("退款中",2),
        REFUNDED("退款成功",3),
        FAIL_REFUNDED("退款失敗",4),
        ;

        private final String name;
        private final int status;

        private OrderStatus(String name,int status){
            this.name = name;
            this.status = status;
        }

        public void printOrderStatus(){
            System.out.println("列印訂單狀態");
        }
    }


    public class EnumTest {
        public static void main(String[] args) {
            OrderStatus.PAY.printOrderStatus();
            OrderStatus.NO_PAY.printOrderStatus();
        }
    }

列舉類也可以有抽象方法,但是列舉項必須重寫該方法。

  1. 列舉類實現介面

    與普通類一樣,實現介面的時候需要實現介面的抽象方法,也可以讓列舉類的不同物件實現不同的行為。

//定義一個介面
public interface Order {
    void printOrderStatus();
}

//列舉類實現該介面
public enum OrderStatus implements Order{
    NO_PAY("未支付",0){
        @Override
        public void printOrderStatus() {
            System.out.println("未支付");
        }
    },
    PAY("已支付",1){
        @Override
        public void printOrderStatus() {
            System.out.println("已支付");
        }
    },
    REFUNDING("退款中",2){
        @Override
        public void printOrderStatus() {
            System.out.println("退款中");
        }
    },
    REFUNDED("退款成功",3){
        @Override
        public void printOrderStatus() {
            System.out.println("退款成功");
        }
    },
    FAIL_REFUNDED("退款失敗",4){
        @Override
        public void printOrderStatus() {
            System.out.println("退款失敗");
        }
    },
    ;

    private final String name;
    private final int status;

    private OrderStatus(String name,int status){
        this.name = name;
        this.status = status;
    }
}

此時檢視編譯後的檔案,會發現除了生成 OrderStatus.class 檔案之外,還生成了多個 .class 檔案:

它們是 OrderStatus.class 中生成的匿名內部類的檔案。

狀態轉換

需求

訂單是電商專案中不可缺少的組成部分,而訂單狀態的轉換也是我們經常討論的問題。我們都知道訂單狀態的轉換是有一定的邏輯性的,不可以隨意轉換。

:你想購買某個商品,只是把它加入了購物車,此時應該是未支付狀態。如果來個請求想把它轉換為退款狀態,那麼系統應該丟擲提示資訊“狀態轉換失敗,請先完成購買!”

接下來我們就用列舉來完成一下訂單狀態轉換的限制。

實現

列舉類定義:

public enum OrderStatus{
    NO_PAY("未支付",0){
        @Override
        public Boolean canChange(OrderStatus orderStatus) {
            switch (orderStatus){
                case PAY:
                    return true;
                default:
                    return false;
            }
        }
    },
    PAY("已支付",1){
        @Override
        public Boolean canChange(OrderStatus orderStatus) {
            //因為退款介面一般都會有延遲,所以會先轉化為“退款中”狀態
            switch (orderStatus){
                case REFUNDING:
                    return true;
                default:
                    return false;
            }
        }
    },
    REFUNDING("退款中",2){
        @Override
        public Boolean canChange(OrderStatus orderStatus) {
            switch (orderStatus){
                case REFUNDED:
                case FAIL_REFUNDED:
                    return true;
                default:
                    return false;
            }
        }
    },
    REFUNDED("退款成功",3),
    FAIL_REFUNDED("退款失敗",4),
    ;

    private final String name;
    private final int status;

    private OrderStatus(String name,int status){
        this.name = name;
        this.status = status;
    }

    //自定義轉換方法
    public Boolean canChange(OrderStatus orderStatus){
        return false;
    }
}

呼叫方法:

public class EnumTest {

    public static void main(String[] args) {
        Boolean aBoolean = OrderStatus.NO_PAY.canChange(OrderStatus.PAY);
        String statusStr = aBoolean?"可以":"不可以";
        System.out.println("是否可以完成狀態轉換:"+ statusStr);

        Boolean flag = OrderStatus.REFUNDED.canChange(OrderStatus.FAIL_REFUNDED);
        String flagStr = flag?"可以":"不可以";
        System.out.println("是否可以完成狀態轉換:"+ flagStr);
    }
}

返回結果:

這樣我們就用列舉類實現了訂單狀態轉換的限制。此例子只是為狀態轉換提供一種思路,具體的流程還需要根據自己系統中的業務來具體處理。

如果你有不同的意見或者更好的idea,歡迎聯絡阿Q,新增阿Q可以加入技術交流群參與討論呦!

相關文章