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ọ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 {};
}

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() và 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() và 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à
finalnên không gì subclass được nó.class E2 extends Day { }hỏng hai lần:cannot inherit from final Dayvàenum classes are not extensible. - Tập constant cố định ngay lúc compile.
new Colour()bị từ chối vớienum classes may not be instantiated. - Enum đã extends
java.lang.Enumrồ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;javacbáo'{' expectedngay tạiextends.
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

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.A là E5, 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 HashMap và HashSet; 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) và EnumSet.range(Day.TUE, Day.THU). EnumSet.allOf với EnumSet.noneOf là hai cái còn lại.
EnumMap | HashMap | |
|---|---|---|
| Cách biểu diễn | một array index theo ordinal() | bucket array cộng một node mỗi entry |
| Tra cứu | index array | hash, bucket, rồi equals |
| Thứ tự iterate | thứ tự khai báo, được bảo đảm | không đặc tả |
Key null | NullPointerException | cho phép |
| Type của key | cố định lúc khởi tạo, từ Day.class | object 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 EnumMap và EnumSet 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;
}
}

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 -> []
| State | allowed() | Terminal |
|---|---|---|
NEW | [PAID, CANCELLED] | không |
PAID | [SHIPPED, CANCELLED] | không |
SHIPPED | [DELIVERED] | không |
DELIVERED | [] | có |
CANCELLED | [] | có |
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 EnumSet và EnumMap, 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.equals là final 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() và 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 protected — javac 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 EnumMap và EnumSet.
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.