Enum — tập giá trị cố định, type-safe thay cho magic number
Vì sao static final int không type-safe và enum giải quyết thế nào: cú pháp, switch exhaustive, field và method trong enum, built-in methods như name()/values(), và pitfall kinh điển với ordinal().
TL;DR: Khi một biến chỉ được phép nhận vài giá trị cố định (trạng thái đơn hàng, phân loại BMI), static final int không bảo vệ được gì — mọi int đều lọt qua compiler. enum định nghĩa một kiểu mới mà giá trị hợp lệ là một tập đóng: compiler chặn giá trị lạ, switch expression được kiểm tra exhaustive, và mỗi value có thể mang field + method riêng. Hai quy tắc sống còn: không bao giờ dùng ordinal() cho logic nghiệp vụ (thứ tự khai báo có thể đổi khi refactor), và persist enum bằng name() hoặc field code riêng thay vì số thứ tự.
Bài trước dùng static final cho hằng số đơn lẻ như MAX_RETRIES = 3. Nhưng khi hằng số là một tập giá trị liên quan nhau — trạng thái đơn hàng, mức độ ưu tiên, phân loại BMI — static final int lộ điểm yếu lớn: compiler không ngăn được ai truyền giá trị ngoài tập. Java có hẳn một cơ chế ngôn ngữ cho bài toán này: enum.
1. Analogy — Menu in trên bảng đèn
enum như menu in trên bảng đèn trong nhà hàng: tập hợp lựa chọn là cố định, bạn chỉ có thể chọn trong menu đó, không tự thêm món tuỳ tiện — và mỗi món có thể kèm thông tin riêng (giá, độ cay).
| Đời thường | Concept |
|---|---|
| Menu in cố định trên bảng đèn | Tập value của enum — đóng, không thêm lúc runtime |
| Gọi món ngoài menu bị từ chối | Compiler chặn giá trị ngoài enum |
| Mỗi món kèm giá + mô tả | Enum value có field + method riêng |
2. Vì sao cần enum?
Trước khi có enum, người ta dùng int hằng số:
// ANTI-PATTERN: int constants
static final int TRANG_THAI_MOI = 1;
static final int TRANG_THAI_XU_LY = 2;
static final int TRANG_THAI_HOAN_TAT = 3;
void xuLy(int trangThai) {
if (trangThai == 1) { ... }
// van co the truyen 999 -- compiler khong phat hien!
}
Vấn đề: không type-safe — bất kỳ int nào đều được chấp nhận, kể cả giá trị không hợp lệ.
3. Cú pháp enum cơ bản
public enum TrangThai {
MOI, DANG_XU_LY, HOAN_TAT, HUY_BO;
}
Dùng:
TrangThai t = TrangThai.MOI; // compiler chi chap nhan gia tri trong enum
void xuLy(TrangThai trangThai) { // type-safe: chi nhan TrangThai
if (trangThai == TrangThai.MOI) { ... }
// khong the truyen int 999 -- compile error
}
4. switch với enum — rất sạch
TrangThai t = TrangThai.DANG_XU_LY;
// Switch expression (Java 14+)
String label = switch (t) {
case MOI -> "Moi tao";
case DANG_XU_LY -> "Dang xu ly";
case HOAN_TAT -> "Da xong";
case HUY_BO -> "Da huy";
};
// Compiler bao loi neu quen 1 case -- exhaustive check
5. Enum có field và method
public enum PhanLoaiBmi {
GAY("Gay", 0, 18.5),
BINH_THUONG("Binh thuong", 18.5, 25),
THUA_CAN("Thua can", 25, 30),
BEO_PHI("Beo phi", 30, Double.MAX_VALUE);
private final String nhan;
private final double min;
private final double max;
PhanLoaiBmi(String nhan, double min, double max) {
this.nhan = nhan;
this.min = min;
this.max = max;
}
public String getNhan() { return nhan; }
// Static factory: tu BMI -> PhanLoaiBmi
public static PhanLoaiBmi tuBmi(double bmi) {
for (PhanLoaiBmi pl : values()) {
if (bmi >= pl.min && bmi < pl.max) return pl;
}
return BEO_PHI;
}
}
// Dung:
double bmi = 22.4;
PhanLoaiBmi pl = PhanLoaiBmi.tuBmi(bmi);
System.out.println(pl.getNhan()); // "Binh thuong"
6. Enum built-in methods
TrangThai t = TrangThai.DANG_XU_LY;
t.name() // "DANG_XU_LY" -- ten khai bao
t.ordinal() // 1 -- vi tri 0-indexed trong enum (MOI=0, DANG_XU_LY=1...)
t.toString() // "DANG_XU_LY" -- mac dinh giong name()
TrangThai[] all = TrangThai.values(); // mang tat ca gia tri
TrangThai t2 = TrangThai.valueOf("MOI"); // tu String -> enum; IllegalArgumentException neu sai
ordinal() trả vị trí khai báo trong enum (0, 1, 2...). Nếu ai thêm hoặc đổi thứ tự enum value, ordinal() thay đổi theo — logic dựa vào ordinal() bị vỡ ngầm. Thêm field riêng như code hoặc displayOrder nếu cần số thứ tự ổn định.
7. Khi nào dùng enum vs static final int?
enum | static final int | |
|---|---|---|
| Type-safe? | ✅ Compiler kiểm tra | ❌ Mọi int đều được nhận |
| Có thể thêm field/method? | ✅ Dễ dàng | ❌ Phức tạp |
| Switch exhaustive check? | ✅ Compiler cảnh báo case thiếu | ❌ Không |
| Serialization/persistence | Dùng .name() (String) tốt hơn .ordinal() | Stable nếu không đổi value |
| Dùng khi | Tập hợp giá trị liên quan, có logic | Giá trị đơn lẻ không liên quan nhau |
✅ Dùng enum khi: trạng thái (OPEN/CLOSED/PENDING), mức độ (LOW/MEDIUM/HIGH), phân loại (màu sắc, loại sản phẩm), loại sự kiện.
✅ Dùng static final khi: hằng số đơn lẻ không thuộc "họ" nào (MAX_RETRIES = 3, TIMEOUT_MS = 5000).
❌ Dùng magic number: if (status == 2) thay vì if (status == TrangThai.DANG_XU_LY) — khó đọc, không type-safe, dễ sai.
7.1 ✅/❌ Bảng chọn nhanh
| Tình huống | ✅ Dùng | ❌ Tránh |
|---|---|---|
| Tập trạng thái liên quan (OPEN, CLOSED...) | enum | int magic numbers |
| Phân loại cố định (loại sản phẩm, màu sắc) | enum | String hardcoded |
| Cần switch exhaustive | enum | int/String (compiler không check) |
| Hằng số đơn lẻ như timeout, retry limit | static final int | enum (overkill) |
| Hằng số string như URL prefix | static final String | enum (không thêm value) |
| Magic number trong if | ❌ Không dùng gì cả — tạo named constant | if (status == 2) |
enum là "menu cố định" — chỉ có thể chọn trong danh sách, không tự thêm tuỳ tiện, và mỗi lựa chọn có thể mang thêm thông tin. Còn static final là "bảng giá niêm yết" cho từng con số đơn lẻ.
8. Mermaid — enum lifecycle
flowchart LR decl["enum TrangThai declaration"] vals["values() -- array of all constants"] switch_use["switch expression -- exhaustive"] name_method[".name() returns String"] ordinal_method[".ordinal() returns int index"] valueof["valueOf(String) -- parse from name"] decl --> vals decl --> switch_use decl --> name_method decl --> ordinal_method decl --> valueof
9. Code example — enum với field, method và switch
public class OrderDemo {
public enum TrangThai {
PENDING("Cho xu ly"),
PROCESSING("Dang xu ly"),
SHIPPED("Da giao hang"),
DELIVERED("Da nhan hang"),
CANCELLED("Da huy");
private final String moTa;
TrangThai(String moTa) {
this.moTa = moTa;
}
public String getMoTa() { return moTa; }
public boolean canCancel() {
return this == PENDING || this == PROCESSING;
}
}
public static void main(String[] args) {
TrangThai t = TrangThai.PROCESSING;
System.out.println("Trang thai: " + t.getMoTa());
System.out.println("Co the huy: " + t.canCancel());
// switch expression -- exhaustive
String icon = switch (t) {
case PENDING -> "CLOCK";
case PROCESSING -> "GEAR";
case SHIPPED -> "TRUCK";
case DELIVERED -> "CHECK";
case CANCELLED -> "X";
};
System.out.println("Icon: " + icon);
// Built-in methods
System.out.println(t.name()); // PROCESSING
System.out.println(TrangThai.values().length); // 5
System.out.println(TrangThai.valueOf("SHIPPED")); // SHIPPED
}
}
10. Pitfall tổng hợp
❌ Dùng ordinal() cho logic: thứ tự khai báo enum có thể thay đổi khi refactor, ordinal() sẽ trả giá trị khác mà không báo lỗi. Persist/serialize bằng name() hoặc field code riêng.
❌ Magic number: if (status == 2) thay vì if (status == TrangThai.PROCESSING) — khó đọc, không type-safe.
❌ valueOf() với input chưa normalize: TrangThai.valueOf("moi") throw IllegalArgumentException vì match case-sensitive với name(). Input từ user phải normalize (toUpperCase()) + bọc try-catch.
❌ Thêm default vào switch expression đã đủ case: value mới thêm vào enum sẽ rơi im lặng vào default thay vì gây compile error nhắc bạn cập nhật — mất lợi ích exhaustive check.
11. 📚 Deep Dive Oracle
Spec và API chính thức Java 21:
- JLS §8.9 — Enum Classes: enum là class đặc biệt, implicitly extends
java.lang.Enum, constructor luôn private, không thể extend hay implement thêm. - Enum API — Java 21:
name(),ordinal(),valueOf(),compareTo().
Diễn giải đơn giản: JLS §8.9 quy định enum values là public static final instance của enum class, được khởi tạo theo thứ tự khai báo khi class load — mỗi value đúng một instance duy nhất, đó là lý do so sánh enum bằng == an toàn. ordinal() là thứ tự khai báo (0-indexed) — spec không đảm bảo giá trị ổn định qua refactor, nên đừng persist nó.
12. Tóm tắt
enum: kiểu mới với tập giá trị đóng, type-safe — compiler chặn giá trị ngoài tập, thay thế magic number.- Mỗi enum value là một instance duy nhất được JVM khởi tạo khi load class → so sánh bằng
==an toàn. - Switch expression với enum được kiểm tra exhaustive — thêm value mới mà thiếu case → compile error (đừng thêm
defaultvô tội vạ). - Enum có thể mang field + method + static factory — gắn data và behavior vào từng value.
name()trả tên khai báo,values()trả mảng tất cả value,valueOf()parse từ String (case-sensitive, throw nếu sai).- Không dùng
ordinal()cho logic nghiệp vụ — thêm field riêng nếu cần số ổn định. - Hằng số đơn lẻ (timeout, retry limit) vẫn dùng
static final— enum dành cho tập giá trị liên quan.
13. Tự kiểm tra
Q1Enum TrangThai có values MOI, XU_LY, XONG. TrangThai.MOI.ordinal() trả gì? Nếu thêm DANG_CHO vào đầu danh sách, ordinal của MOI đổi không?▸
TrangThai có values MOI, XU_LY, XONG. TrangThai.MOI.ordinal() trả gì? Nếu thêm DANG_CHO vào đầu danh sách, ordinal của MOI đổi không?TrangThai.MOI.ordinal() = 0 ban đầu. Nếu thêm DANG_CHO vào đầu → DANG_CHO.ordinal() = 0, MOI.ordinal() = 1 — ordinal của MOI đã thay đổi. Đây là lý do không bao giờ dùng ordinal() cho logic nghiệp vụ, persist vào DB, serialize, hay compare. Thay vào đó: dùng name() (tên String) hoặc thêm field riêng (code, id) vào enum để có giá trị ổn định.Q2Vì sao nên dùng enum thay static final int STATUS_ACTIVE = 1; static final int STATUS_INACTIVE = 2;?▸
enum thay static final int STATUS_ACTIVE = 1; static final int STATUS_INACTIVE = 2;?- Type safety: method nhận
TrangThaikhông thể truyền nhầmint 999hay giá trị khác — compiler chặn. Vớiintconstant, không có check, bug im lặng. - Switch exhaustive: Java 21 pattern matching switch báo warning/error nếu thiếu case cho enum value mới thêm; với int thì không.
- Có method/field: enum có thể có method (
getDisplayName()), field (code,description) — gắn hành vi vào mỗi value. - Reflection/serialization:
valueOf("ACTIVE"),values(), Jackson/Gson auto-convert từ String → enum. - Tự-document: đọc
status = TrangThai.XU_LYrõ nghĩa hơnstatus = 2.
Q3Vì sao Java không cho phép new TrangThai(...) từ bên ngoài — enum constructor luôn private? Nếu cho phép thì điều gì vỡ?▸
new TrangThai(...) từ bên ngoài — enum constructor luôn private? Nếu cho phép thì điều gì vỡ?new, tập giá trị không còn "đóng": xuất hiện instance TrangThai thứ N+1 nằm ngoài values() → so sánh bằng == không còn đáng tin, switch exhaustive mất ý nghĩa, serialization và singleton guarantee vỡ theo. Tính "mỗi value đúng một instance" chính là nền tảng để enum dùng == an toàn.Q4TrangThai.valueOf("moi") trả gì khi enum có value MOI? Xử lý input từ user thế nào cho an toàn?▸
TrangThai.valueOf("moi") trả gì khi enum có value MOI? Xử lý input từ user thế nào cho an toàn?IllegalArgumentException — valueOf() match chính xác, case-sensitive với name(), mà name là "MOI" chứ không phải "moi". Với input từ user/API: normalize trước (TrangThai.valueOf(input.trim().toUpperCase())) và bọc try-catch IllegalArgumentException để trả lỗi rõ ràng thay vì crash. Pattern khác: viết static factory tự duyệt values() và trả Optional khi không match.Q5Vì sao switch expression với enum không nên thêm nhánh default khi đã liệt kê đủ mọi case?▸
default khi đã liệt kê đủ mọi case?default. Lợi ích lớn nhất nằm ở tương lai: khi ai đó thêm value mới vào enum, mọi switch thiếu case sẽ compile error ngay — bạn được nhắc cập nhật từng chỗ dùng. Nếu có default, value mới rơi im lặng vào nhánh default → bug runtime khó phát hiện. default chỉ hợp lý khi thực sự muốn "mọi value còn lại xử lý giống nhau", kể cả value tương lai.Bài tiếp theo: Mini-challenge: Máy tính BMI
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên