Command Palette

Search for a command to run...

[Advanced Java] Enum nâng cao trong Java: constructor, constant body, EnumMap và enum singleton

Phần lớn code Java dùng enum như một danh sách tên có type: enum Day { MON, TUE }, một switch trên nó, hết. Cách dùng đó không sai, nhưng nó chỉ là một góc nhỏ của những gì enum làm được.

Một khai báo enum chính là một khai báo class. Nó compile thành một class extends java.lang.Enum, mỗi constant là một instance của class đó, và gần như mọi thứ một class mang được — field, constructor, method, interface được implement, thậm chí một body method riêng cho từng constant — thì enum cũng mang được. Bài này đi qua toàn bộ những thứ đó và kiểm chứng từng khẳng định bằng chính compiler.

Một hàng chip tên enum trơn ở trên, và hàng dưới là chính các constant đó mang body method riêng

Mọi đoạn code bên dưới đều được compile và chạy trên OpenJDK 21.0.6 (arm64). Mọi output của javap, mọi danh sách file class, mọi lỗi compiler và message của exception đều chép lại từ lần chạy đó, không viết theo trí nhớ.

Enum thực sự là gì

Bắt đầu từ khai báo mà ai cũng viết.

enum Day { MON, TUE, WED }

javac Day.java sinh ra Day.class, và javap Day.class in ra phần public của nó:

Compiled from "Day.java"
final class Day extends java.lang.Enum<Day> {
  public static final Day MON;
  public static final Day TUE;
  public static final Day WED;
  public static Day[] values();
  public static Day valueOf(java.lang.String);
  static {};
}

Ba dòng code trở thành một class final với ba field public static final mang chính type của nó. Thêm -p là thấy luôn các member private:

Compiled from "Day.java"
final class Day extends java.lang.Enum<Day> {
  public static final Day MON;
  public static final Day TUE;
  public static final Day WED;
  private static final Day[] $VALUES;
  public static Day[] values();
  public static Day valueOf(java.lang.String);
  private Day();
  private static Day[] $values();
  static {};
}

Code của một enum ba constant ở bên trái, bên phải là output javap của class mà nó compile ra kèm chú thích từng dòng

Toàn bộ những gì nằm dưới ba constant đều do compiler viết. $VALUES là array thật sự giữ chúng, $values() là helper dựng array đó, values()valueOf(String) là hai accessor public, private Day() là constructor bạn không hề khai báo, còn static {} là class initializer dựng từng constant theo đúng thứ tự khai báo.

javap giấu mất hai parameter của constructor. Reflection thì không:

for (Constructor<?> c : Day.class.getDeclaredConstructors()) {
    System.out.println(c);
}
private Day(java.lang.String,int)

Constructor của mọi enum thật ra nhận một name và một ordinal đứng trước các parameter của bạn, rồi đẩy thẳng lên java.lang.Enum. Đó chính là nơi name()ordinal() lấy giá trị.

values() được sinh ra mỏng hơn vẻ ngoài. javap -c -p Day.class cho thấy phần body của nó:

public static Day[] values();
  Code:
     0: getstatic     #13                 // Field $VALUES:[LDay;
     3: invokevirtual #17                 // Method "[LDay;".clone:()Ljava/lang/Object;
     6: checkcast     #18                 // class "[LDay;"
     9: areturn

Nó trả về $VALUES.clone() — một bản sao mới ở mỗi lần gọi. Nhớ chi tiết này, cuối bài nó quay lại thành một cái bẫy.

Ba hệ quả rơi thẳng ra từ hình dạng đã compile:

  • Class là final nên không gì subclass được nó. class E2 extends Day { } hỏng hai lần: cannot inherit from final Dayenum classes are not extensible.
  • Tập constant cố định ngay lúc compile. new Colour() bị từ chối với enum classes may not be instantiated.
  • Enum đã extends java.lang.Enum rồi nên không extends thêm được gì nữa. enum E1 extends Base { A } thậm chí không parse nổi; javac báo '{' expected ngay tại extends.

Field, constructor và method trên enum

Vì constant là một instance nên nó giữ được state, và giá trị được truyền ngay trong danh sách constant.

enum Status {
    OK(200, "OK"),
    MOVED(301, "Moved Permanently"),
    NOT_FOUND(404, "Not Found"),
    SERVER_ERROR(500, "Internal Server Error");

    private final int code;
    private final String reason;

    Status(int code, String reason) {
        this.code = code;
        this.reason = reason;
    }

    public int code() { return code; }

    public boolean isError() { return code >= 400; }

    @Override
    public String toString() { return code + " " + reason; }
}
OK -> 200 OK, error=false
MOVED -> 301 Moved Permanently, error=false
NOT_FOUND -> 404 Not Found, error=true
SERVER_ERROR -> 500 Internal Server Error, error=true

Dấu chấm phẩy sau constant cuối cùng là thứ ngăn cách danh sách constant với phần body của class, và nó bắt buộc ngay khi có bất cứ thứ gì đứng sau các constant. javap -p Status.class xác nhận field và method nằm đúng chỗ bạn đặt:

final class Status extends java.lang.Enum<Status> {
  public static final Status OK;
  public static final Status MOVED;
  public static final Status NOT_FOUND;
  public static final Status SERVER_ERROR;
  private final int code;
  private final java.lang.String reason;
  private static final Status[] $VALUES;
  public static Status[] values();
  public static Status valueOf(java.lang.String);
  private Status(int, java.lang.String);
  public int code();
  public boolean isError();
  public java.lang.String toString();
  private static Status[] $values();
  static {};
}

Để ý constructor là private dù trong code không hề ghi modifier nào. Nó private ngầm định, và compiler chặn mọi nỗ lực mở rộng:

PubCtor.java:6: error: modifier public not allowed here
    public PubCtor(int n) { this.n = n; }
           ^
1 error

protected cũng hỏng y hệt với modifier protected not allowed here. Ghi private ra tường minh thì hợp lệ nhưng thừa. Lý do chính là toàn bộ ý nghĩa của enum: nếu ai đó gọi được constructor thì danh sách constant không còn là tập instance đầy đủ nữa, và mọi bảo đảm suy ra từ đó — so sánh bằng ==, EnumSet, switch exhaustive, singleton — đều sụp.

Constructor chạy trước mọi field static của enum, vì chính các constant là thứ đầu tiên class initializer dựng lên. Nên đoạn này không compile được:

enum Code {
    A(1), B(2);
    private static final Map<Integer, Code> BY_NUM = new HashMap<>();
    private final int num;
    Code(int num) {
        this.num = num;
        BY_NUM.put(num, this);   // BY_NUM chưa tồn tại
    }
}
StaticInit.java:8: error: illegal reference to static field from initializer
        BY_NUM.put(num, this);
        ^
1 error

Hãy dựng bảng tra trong một block static, vì block đó chạy sau khi mọi constant đã tồn tại:

enum Code {
    A(1), B(2), C(3);

    private static final Map<Integer, Code> BY_NUM = new HashMap<>();

    static {
        for (Code c : values()) BY_NUM.put(c.num, c);
    }

    private final int num;

    Code(int num) { this.num = num; }

    public static Optional<Code> fromNum(int n) { return Optional.ofNullable(BY_NUM.get(n)); }
}
Optional[B]
Optional.empty

Constant-specific class body

Đây là phần mà hầu hết tutorial không bao giờ chạm tới. Một constant có thể đi kèm một block ngoặc nhọn, và trong block đó nó override một method của enum cho riêng mình.

enum Op {
    PLUS("+") {
        @Override public double apply(double a, double b) { return a + b; }
    },
    MINUS("-") {
        @Override public double apply(double a, double b) { return a - b; }
    },
    TIMES("*") {
        @Override public double apply(double a, double b) { return a * b; }
    },
    DIVIDE("/") {
        @Override public double apply(double a, double b) {
            if (b == 0) throw new ArithmeticException("divide by zero");
            return a / b;
        }
    };

    private final String symbol;

    Op(String symbol) { this.symbol = symbol; }

    public abstract double apply(double a, double b);

    @Override public String toString() { return symbol; }
}
12.0 + 4.0 = 16.0
12.0 - 4.0 = 8.0
12.0 * 4.0 = 48.0
12.0 / 4.0 = 3.0

Mỗi body đó được compile thành một anonymous subclass của enum, nên javac Op.java ghi ra sáu file class chứ không phải hai:

Op$1.class
Op$2.class
Op$3.class
Op$4.class
Op.class
OpMain.class

Bốn constant của enum mỗi cái có body riêng ở bên trái, mũi tên nối sang bốn file class mà javac sinh thêm ở bên phải

javap -p trên enum và trên file đầu tiên trong số đó cho thấy chuyện gì đã xảy ra:

abstract class Op extends java.lang.Enum<Op> {
  public static final Op PLUS;
  ...
  private Op(java.lang.String);
  public abstract double apply(double, double);
}

final class Op$1 extends Op {
  private Op$1(java.lang.String, int, java.lang.String);
  public double apply(double, double);
}

Hai thứ đã đổi. Op giờ là abstract chứ không còn final, vì hai constant chạy code khác nhau và bản thân type enum không có một body chung nào để đưa ra. Và mỗi constant body là một subclass final của Op. Để ý javap in đầy đủ constructor của subclass — (String, int, String) — vì nó chỉ giấu name với ordinal synthetic trên chính class enum. Hệ quả thực tế là các constant không còn chung một runtime class nữa:

Op.PLUS.getClass()        = class Op$1
Op.PLUS.getDeclaringClass = class Op
superclass of PLUS body   = class Op

getClass() cho ra subclass được sinh; getDeclaringClass() cho ra type enum, và đó gần như luôn là thứ bạn muốn mỗi khi sự khác biệt này có ý nghĩa.

Có một giới hạn nên biết trước khi dùng. Constant body override được, nhưng không mở rộng được bề mặt public của enum. Khai báo một method hoàn toàn mới bên trong body — A { public void extra() { ... } } — thì không gọi được qua constant:

E5.java:6: error: cannot find symbol
    public static void main(String[] x) { E5.A.extra(); }
                                              ^
  symbol:   method extra()
  location: variable A of type E5
1 error

Static type của E5.AE5, mà E5 không có extra(). Thứ gì một body cài đặt thì phải được khai báo trên enum, hoặc thừa hưởng từ một interface mà enum implement.

Abstract method buộc mọi constant phải trả lời

Khai báo method là abstract trên enum, đúng như Op làm, biến compiler thành người canh cửa: mọi constant phải cung cấp một body, và thêm một constant mà quên body là build hỏng ngay.

enum E3 {
    A { @Override public int v() { return 1; } },
    B;
    public abstract int v();
}
E3.java:3: error: E3 is abstract; cannot be instantiated
    B;
    ^
1 error

Lỗi chỉ thẳng vào B, đúng cái constant chưa có implementation. Nếu không constant nào có body thì message lại là message quen thuộc của một class quên implement abstract method:

E4.java:1: error: E4 is not abstract and does not override abstract method v() in E4
enum E4 {
^
1 error

Đó là lý do thực dụng để chọn abstract method thay vì một switch đặt trong một method concrete. Một switch bỏ sót constant mới có thể chỉ hỏng lúc runtime; một abstract method thì không quên được, vì file sẽ không compile.

Enum implement được interface

Enum không extends được class, nhưng implement bao nhiêu interface cũng được — kể cả interface có default method, thứ mà mọi constant đều thừa hưởng.

interface Priced {
    int cents();
    default String display() { return String.format("%d.%02d", cents() / 100, cents() % 100); }
}

enum Coin implements Priced {
    PENNY(1), NICKEL(5), DIME(10), QUARTER(25);

    private final int cents;
    Coin(int cents) { this.cents = cents; }

    @Override public int cents() { return cents; }
}
QUARTER = 0.25
DIME = 0.10
total cents = 35
Coin.DIME instanceof Priced -> true

javap Coin.class cho thấy interface nằm ngay trên dòng khai báo class, cạnh superclass ngầm định:

final class Coin extends java.lang.Enum<Coin> implements Priced {
  public static final Coin PENNY;
  ...
  public int cents();
}

Chính sự kết hợp đó khiến enum trở thành một thành phần dùng được trong một thiết kế bình thường: một tập instance cố định nhưng vẫn thỏa mãn interface mà phần còn lại của code phụ thuộc vào. Constant body cũng có thể implement method của interface cho từng constant, ghép phần này với phần trước lại làm một.

EnumMap và EnumSet, và vì sao chúng không phải HashMap với HashSet

java.util có sẵn hai collection chỉ tồn tại cho key kiểu enum. Chúng không phải bản tối ưu vặt của HashMapHashSet; chúng có cách biểu diễn khác và một contract mạnh hơn.

Một EnumMap là một array duy nhất có một ô cho mỗi constant của enum, đánh index theo ordinal(). Reflection vào field private vals cho thấy đúng như vậy — một map giữ hai entry trên một enum bảy constant vẫn sở hữu bảy ô:

map.size()  = 2
vals.length = 7  (Day.values().length = 7)
  vals[0] (MON) = null
  vals[1] (TUE) = null
  vals[2] (WED) = gym
  vals[3] (THU) = null
  vals[4] (FRI) = null
  vals[5] (SAT) = rest
  vals[6] (SUN) = null

Không hashing, không bucket array, không xử lý collision và không lưu node Map.Entry nào cho mỗi key: một lần tra cứu chỉ là một phép index array. Hệ quả nhìn thấy được là thứ tự iterate. Bỏ cùng bốn key vào cả hai map theo cùng một thứ tự cố tình xáo trộn rồi đọc ra:

Map<Day, Integer> em = new EnumMap<>(Day.class);
Map<Day, Integer> hm = new HashMap<>();
for (Day d : new Day[]{ Day.SUN, Day.WED, Day.MON, Day.FRI }) {
    em.put(d, d.ordinal());
    hm.put(d, d.ordinal());
}
EnumMap : {MON=0, WED=2, FRI=4, SUN=6}
HashMap : {WED=2, FRI=4, SUN=6, MON=0}

EnumMap iterate theo thứ tự khai báo và specification bảo đảm điều đó. Thứ tự của HashMap là bất cứ thứ gì các hash code sinh ra trong lần chạy này; nó không được đặc tả, nên đừng dựa vào nó ngay cả khi trông có vẻ ổn định.

EnumSet cũng là ý tưởng đó nhưng dùng bit vector thay cho array. Bạn nhận được implementation nào là tùy kích thước enum, và tên class nói thẳng ra điều đó:

64 constants -> java.util.RegularEnumSet
65 constants -> java.util.JumboEnumSet

RegularEnumSet giữ toàn bộ set trong đúng một long, mỗi constant một bit, nên ranh giới rơi đúng vào 64. JumboEnumSet dùng một array long. Cả hai đều không có constructor public; bạn dựng chúng qua factory, và chúng cũng iterate theo thứ tự khai báo bất kể thứ tự thêm vào:

EnumSet<Day> s = EnumSet.noneOf(Day.class);
s.add(Day.SUN); s.add(Day.TUE); s.add(Day.FRI); s.add(Day.MON);
EnumSet : [MON, TUE, FRI, SUN]
LinkedHashSet : [SUN, TUE, FRI, MON]
HashSet : [MON, TUE, SUN, FRI]

Các factory phủ luôn những hình dạng hay dùng:

weekend = [SAT, SUN]
week    = [MON, TUE, WED, THU, FRI]
range   = [TUE, WED, THU]

lần lượt từ EnumSet.of(Day.SAT, Day.SUN), EnumSet.complementOf(weekend)EnumSet.range(Day.TUE, Day.THU). EnumSet.allOf với EnumSet.noneOf là hai cái còn lại.

EnumMapHashMap
Cách biểu diễnmột array index theo ordinal()bucket array cộng một node mỗi entry
Tra cứuindex arrayhash, bucket, rồi equals
Thứ tự iteratethứ tự khai báo, được bảo đảmkhông đặc tả
Key nullNullPointerExceptioncho phép
Type của keycố định lúc khởi tạo, từ Day.classobject bất kỳ

Việc từ chối null là thật và xảy ra ngay: em.put(null, 0) ném NullPointerException vì không có index array nào cho một key không có ordinal, còn hm.put(null, 0) thì được nhận. Hãy chọn EnumMapEnumSet vì contract — thứ tự xác định, type của key cố định lúc khởi tạo, không phụ thuộc hashCode — và coi cách biểu diễn gọn nhẹ là lý do khiến những bảo đảm đó rẻ.

switch trên enum và tính exhaustive

Trong một nhãn case, constant được viết không kèm tên enum, vì type đã biết từ biểu thức chọn:

static String action(Signal s) {
    return switch (s) {
        case RED    -> "stop";
        case AMBER  -> "prepare";
        case GREEN  -> "go";
    };
}

Java 21 cũng chấp nhận dạng đầy đủ case Signal.RED ->, compile và chạy y hệt. Dạng mũi tên nhận một expression đơn hoặc một block ngoặc nhọn, và không bao giờ rơi xuống case kế nên không cần break:

static int seconds(Signal s) {
    switch (s) {
        case RED -> { return 30; }
        case AMBER -> { return 4; }
        case GREEN -> { return 25; }
    }
    return -1;
}
RED: stop for 30s
AMBER: prepare for 4s
GREEN: go for 25s

Khác biệt quan trọng giữa hai đoạn trên không nằm ở cú pháp, mà ở chỗ một trong hai là switch expression. Một switch expression trên enum bắt buộc phải phủ hết constant, và compiler kiểm tra điều đó:

Sw2.java:5: error: the switch expression does not cover all possible input values
        return switch (s) {
               ^
1 error

Một switch statement thì không có ràng buộc đó. Bỏ sót một constant vẫn compile im lặng, không một cảnh báo nào kể cả với -Xlint:all trên OpenJDK 21.0.6, và lúc runtime nó đơn giản là không làm gì cho trường hợp bị thiếu.

Sự bất đối xứng đó quyết định cách viết code. Hãy ưu tiên dạng expression, và đừng thêm nhánh default khi bạn đã phủ hết constant: default sẽ thỏa mãn phép kiểm tra exhaustive mãi mãi, và đến ngày ai đó thêm một constant thì build vẫn xanh trong khi constant mới lặng lẽ rơi vào nhánh default. Không có default, việc thêm constant làm hỏng compile ở đúng mọi chỗ switch cần sửa — chính là lời nhắc bạn muốn có.

Enum singleton

Một enum chỉ có một constant cho bạn một instance mà ngôn ngữ bảo đảm là duy nhất, và bảo đảm đó đứng vững trước cả hai cơ chế vẫn thường phá được singleton viết tay.

enum Registry {
    INSTANCE;

    private int hits;

    public int hit() { return ++hits; }
    public int hits() { return hits; }
}

Serialization trước. Ghi Registry.INSTANCE vào một ObjectOutputStream, đọc lại rồi so sánh identity:

hits = 2
deserialized == INSTANCE ? true
hits after round trip = 2

Stream chỉ mang theo tên constant, và lúc deserialize nó được tra ngược qua valueOf trên type enum, nên không object thứ hai nào được tạo ra. Một singleton Serializable bình thường phải tự viết method readResolve mới có kết quả tương tự, và quên nó là một cách rất phổ biến để có hai instance.

Reflection mới là trường hợp thú vị. Constructor của enum là private, nhưng setAccessible(true) bình thường vẫn vượt qua được:

Constructor<?> c = Registry.class.getDeclaredConstructors()[0];
c.setAccessible(true);
Object rogue = c.newInstance("ROGUE", 1);
declared constructors: [private Registry(java.lang.String,int)]
java.lang.IllegalArgumentException: Cannot reflectively create enum objects

Constructor.newInstance kiểm tra modifier ENUM trên class khai báo và từ chối trước khi cấp phát bất cứ thứ gì. Phép kiểm tra đó nằm trong JDK chứ không nằm trong code của bạn, nên không có gì để quên. Đối chiếu với một singleton dựa trên class, nơi đúng ba dòng đó lại thành công:

final class Plain {
    static final Plain INSTANCE = new Plain();
    private Plain() {}
}
rogue == Plain.INSTANCE ? false

Hai object, và Plain.INSTANCE không còn là cái duy nhất nữa. Chỉ dạng enum mới được chính ngôn ngữ đóng lại trước chuyện này.

Hai giới hạn cần nói thẳng. Tính duy nhất ở đây là về identity chứ không phải về concurrency — một field mutable trên constant vẫn cần đồng bộ hóa như mọi object dùng chung khác. Và một enum một constant thì không extends được class, nên nếu type bắt buộc phải nằm dưới một base class có sẵn thì cách này không dùng được; còn interface thì nó implement thoải mái.

State machine dựng bằng enum

Mọi thứ ở trên gộp lại thành một trong những thứ hữu dụng nhất mà enum làm được: một tập state mà mỗi constant tự biết transition nào hợp lệ.

enum OrderState {
    NEW {
        @Override public Set<OrderState> allowed() { return EnumSet.of(PAID, CANCELLED); }
    },
    PAID {
        @Override public Set<OrderState> allowed() { return EnumSet.of(SHIPPED, CANCELLED); }
    },
    SHIPPED {
        @Override public Set<OrderState> allowed() { return EnumSet.of(DELIVERED); }
    },
    DELIVERED {
        @Override public Set<OrderState> allowed() { return EnumSet.noneOf(OrderState.class); }
    },
    CANCELLED {
        @Override public Set<OrderState> allowed() { return EnumSet.noneOf(OrderState.class); }
    };

    public abstract Set<OrderState> allowed();

    public boolean isTerminal() { return allowed().isEmpty(); }

    public OrderState to(OrderState target) {
        if (!allowed().contains(target)) {
            throw new IllegalStateException(
                "illegal transition " + this + " -> " + target + "; allowed from " + this + ": " + allowed());
        }
        return target;
    }
}

Các state NEW, PAID, SHIPPED và DELIVERED cùng những transition hợp lệ, và một đường bị từ chối từ DELIVERED sang CANCELLED

Chính allowed() dạng abstract mới làm thứ này an toàn: thêm state thứ sáu là file ngừng compile cho đến khi state đó khai báo nó được đi đâu. Chạy một đường hợp lệ rồi thử một bước sai:

start: NEW
  -> PAID
  -> SHIPPED
  -> DELIVERED (terminal)
rejected: illegal transition DELIVERED -> CANCELLED; allowed from DELIVERED: []
transition table:
  NEW -> [PAID, CANCELLED]
  PAID -> [SHIPPED, CANCELLED]
  SHIPPED -> [DELIVERED]
  DELIVERED -> []
  CANCELLED -> []
Stateallowed()Terminal
NEW[PAID, CANCELLED]không
PAID[SHIPPED, CANCELLED]không
SHIPPED[DELIVERED]không
DELIVERED[]
CANCELLED[]

Cách làm tưởng như hiển nhiên — truyền tập target vào constructor — lại không compile được, và lý do chính là thứ tự khởi tạo ở phần thứ hai:

enum Fwd {
    A(EnumSet.of(Fwd.B)),
    B(EnumSet.noneOf(Fwd.class));
    ...
}
Fwd.java:4: error: illegal forward reference
    A(EnumSet.of(Fwd.B)),
                    ^
1 error

B chưa tồn tại vào lúc A đang được dựng. Hoặc trả tập đó về từ một method được override như ở trên, vì method chỉ được tính lúc gọi, hoặc dựng một EnumMap transition dạng static trong một block static sau khi mọi constant đã có. Cách dùng method giữ luật của từng state nằm ngay cạnh state đó, và đó mới là lý do dùng enum cho việc này.

Các bẫy hay gặp

Dùng ordinal() làm giá trị lưu trữ. Ordinal là vị trí trong file code, không phải identity. Lưu nó xuống là đến ngày ai đó chèn thêm một constant thì ý nghĩa dữ liệu của bạn đổi. Với enum Level { LOW, HIGH }, giá trị 1 đã lưu đọc ra là HIGH:

0 = LOW
1 = HIGH
row stored as 1 reads back as: HIGH

Chèn MEDIUM vào giữa là mọi giá trị 1 đã lưu lặng lẽ thành thứ khác:

0 = LOW
1 = MEDIUM
2 = HIGH
row stored as 1 reads back as: MEDIUM

Không có gì hỏng, không exception nào được ném, và dữ liệu thì đã sai. Hãy lưu name() rồi đọc lại bằng valueOf, hoặc cho enum một field code tường minh do bạn kiểm soát. Javadoc của ordinal() nói thẳng rằng nó được thiết kế cho EnumSetEnumMap, và phần lớn lập trình viên sẽ chẳng cần đến nó.

values() đưa cho bạn một array mới ở mỗi lần gọi. Vì method được sinh ra trả về $VALUES.clone(), hai lần gọi là hai object khác nhau và mọi thay đổi ghi vào kết quả đều bị vứt đi:

first == second        ? false
Arrays.equals(f, s)    ? true
after first[0] = WED   : [WED, TUE, WED]
next Day.values() call : [MON, TUE, WED]

Đó chính là thứ bảo vệ array constant khỏi bị caller phá, và nó cũng có nghĩa values() cấp phát bộ nhớ. Gọi nó trong vòng lặp là copy array ở mỗi vòng; hãy đưa nó ra một array private static final, hoặc iterate EnumSet.allOf, khi lời gọi nằm trên đường chạy nóng.

equals so với ==. Cả hai đều chạy đúng, và == tốt hơn vì ba lý do tách bạch. Nó an toàn với null, trong khi equals trên một reference null thì ném exception:

d == Day.TUE           ? true
d.equals(Day.TUE)      ? true
nil == Day.TUE         ? false
nil.equals(Day.TUE)    -> NullPointerException

Nó cũng được kiểm tra type ngay lúc compile. So sánh constant của hai enum khác nhau bằng == là hỏng build:

Cross.java:5: error: incomparable types: A1 and B1
        System.out.println(A1.X == B1.Y);
                                ^
1 error

trong khi Day.MON.equals(Month.JAN) compile ngon lành và lặng lẽ trả về false — một phép so sánh luôn sai mà chẳng bao giờ kêu ca. Enum.equalsfinal và được cài đặt đúng bằng this == other, nên gọi nó chẳng được thêm gì.

valueOf ném exception, và nó phân biệt hoa thường. Nó không phải parser và nó không trả về null:

java.lang.IllegalArgumentException: No enum constant Day.FUNDAY
No enum constant Day.mon

Với input bạn không kiểm soát được, hãy bọc trong try/catch hoặc tra giá trị trong một Map dạng static dựng ở block static, đúng như Code.fromNum ở trên.

FAQ

Enum trong Java thực sự compile ra cái gì?

Một class. enum Day { MON, TUE, WED } trở thành final class Day extends java.lang.Enum<Day> với mỗi constant là một field public static final Day, một constructor private mà signature thật là (String name, int ordinal) cộng thêm parameter của bạn, một array $VALUES private, hai method values()valueOf(String) được sinh ra, và một class initializer dựng mọi constant theo thứ tự khai báo. javap -p trên file class cho thấy toàn bộ.

Enum trong Java có constructor và field được không?

Được. Đặt argument trong ngoặc sau mỗi constant, kết thúc danh sách constant bằng dấu chấm phẩy, rồi khai báo field và constructor bên dưới. Constructor là private ngầm định và không thể chuyển thành public hay protectedjavac trả lời modifier public not allowed here. Nó cũng chạy trước khi mọi field static của enum được khởi tạo, nên tham chiếu tới một field như vậy từ constructor sẽ hỏng với illegal reference to static field from initializer; hãy dựng state kiểu đó trong một block static.

Constant-specific class body là gì?

Là một block ngoặc nhọn viết ngay sau một constant, chứa các method override chỉ áp dụng cho constant đó. javac compile mỗi block thành một anonymous subclass, nên một enum có bốn constant body sinh ra Op.class cùng Op$1.class đến Op$4.class, class enum trở thành abstract thay vì final, và Op.PLUS.getClass() báo class Op$1 trong khi getDeclaringClass() vẫn báo class Op. Một body override được các method khai báo trên enum hoặc thừa hưởng từ interface của nó; còn method hoàn toàn mới khai báo trong body thì không gọi được qua constant.

Vì sao nên dùng EnumMap thay cho HashMap?

Vì những gì nó bảo đảm, không phải vì một phép đo. EnumMap là một array duy nhất index theo ordinal(), nên nó iterate theo thứ tự khai báo và specification bảo đảm thứ tự đó, nó không cần hashCode hay equals trên key, nó không lưu node cho từng entry, và nó từ chối key null bằng NullPointerException. HashMap thì không bảo đảm thứ tự iterate nào cả. Khi type của key là enum, EnumMap vừa là contract mạnh hơn vừa là cấu trúc nhỏ hơn.

Enum có phải cách viết singleton tốt nhất trong Java không?

Đó là dạng duy nhất được chính ngôn ngữ đóng lại. Một enum một constant đi qua serialization rồi quay về vẫn là đúng object đó mà không cần nhớ viết readResolve, và Constructor.newInstance trên nó hỏng với java.lang.IllegalArgumentException: Cannot reflectively create enum objects, trong khi đúng lời gọi reflection ấy trên một class có constructor private lại thành công và sinh ra instance thứ hai. Giới hạn là nó không extends được class, và tính duy nhất không nói gì về thread safety cho state mutable đặt trên constant.

Có nên lưu ordinal() xuống database không?

Không. ordinal() là vị trí của constant trong file code, nên chèn thêm hay sắp xếp lại constant là gán lại ý nghĩa cho mọi giá trị đã lưu, một cách lặng lẽ và không lỗi nào báo. Hãy lưu name() rồi đọc lại bằng valueOf, hoặc khai báo một field code tường minh trên enum và lưu field đó. Cứ để ordinal() làm đúng việc của nó, tức là đánh index bên trong EnumMapEnumSet.

Có cần nhánh default trong switch trên enum không?

Không cần khi bạn đã phủ hết constant, và bỏ nó đi thì tốt hơn. Một switch expression trên enum bắt buộc exhaustive, nên thiếu một constant là build hỏng với the switch expression does not cover all possible input values. Thêm default vào là phép kiểm tra đó bị thỏa mãn vĩnh viễn, nên constant thêm sau này sẽ lọt vào nhánh default mà không một cảnh báo nào. Còn switch statement thì không bị kiểm tra gì cả và im lặng kể cả với -Xlint:all, thêm một lý do để ưu tiên dạng expression.

Kết luận

Enum là một class có tập instance cố định ngay lúc compile, và mọi tính năng nâng cao đều là hệ quả của đúng sự thật đó. Vì constant là instance nên chúng giữ được field và được dựng bởi constructor. Vì constructor là private và class initializer là người gọi duy nhất, danh sách constant thực sự là tập instance đầy đủ — và chính điều đó cho phép == thay equals, cho phép EnumSet nằm gọn trong một long, cho phép EnumMap chỉ là một array, cho phép switch expression được kiểm tra exhaustive, và cho phép một enum một constant thành singleton mà reflection không giả mạo nổi. Vì constant là object nên nó mang được body method riêng, và một abstract method biến câu "mọi constant phải quyết định chuyện này" thành lỗi compile thay vì một dòng nhận xét lúc review.

Đó cũng là thứ khiến state machine bằng enum đáng viết: transition nằm ngay trên state, compiler không cho state mới bỏ qua chúng, và một bước đi sai luật hỏng to tiếng thay vì làm hỏng dữ liệu. Cái giá phải trả nhỏ và nên biết — thêm một file class cho mỗi constant body, một array mới ở mỗi lần gọi values(), và một ordinal() không bao giờ được phép rời khỏi tiến trình.

Bài tiếp theo trong series: generics — type parameter, bounded type, wildcard, và type erasure thực sự xóa đi những gì trong code đã compile.

Bài viết liên quan

[Advanced Java] Map nâng cao trong Java: TreeMap, LinkedHashMap, Hashtable và ConcurrentHashMap

Các Map implementation ngoài HashMap trên OpenJDK 21 - TreeMap cùng các method NavigableMap floorEntry, ceilingKey, headMap và subMap, access order của LinkedHashMap và LRU cache năm dòng với removeEldestEntry, vì sao Hashtable là legacy, và ConcurrentHashMap thực sự cam kết gì về lock, weakly consistent iterator và các compound operation atomic.

[Advanced Java] Functional Interface trong Java: Supplier, Consumer, Function và Predicate

Functional interface trong java.util.function trên OpenJDK 21: lưới shape đứng sau cả 43 interface, @FunctionalInterface thực sự kiểm tra cái gì, vì sao một abstract equals không phá vỡ quy tắc một abstract method, andThen và compose, các combinator của Predicate và Consumer, primitive specialisation cùng phần boxing chúng loại bỏ, và cách tự viết một cái cho riêng mình.

[Advanced Java] Generics trong Java: type parameter, bounded type, wildcard và type erasure

Generics trong Java trên OpenJDK 21: container Object thời chưa có generics và ClassCastException mà nó sinh ra, cách viết generic class và generic method, bounded type parameter và nhiều bound, wildcard cùng PECS với đúng message lỗi của javac, type erasure chứng minh bằng javap, Signature attribute, bridge method, và lời hứa thật sự của SuppressWarnings unchecked.

[Advanced Java] Stream API trong Java: map, filter, reduce và collect

Stream API của Java trên OpenJDK 21: pipeline gồm source, intermediate và terminal, tính lazy được chứng minh bằng trace println xen kẽ, map, filter, cả ba overload của reduce, collect cùng bộ Collectors, primitive stream và chi phí allocation của boxing, và các bẫy quanh peek, findAny, lambda có state cùng parallelStream.