Java Foundations/Enum — tập giá trị cố định, type-safe thay cho magic number
16/37
Bài 16 / 37~14 phútCú pháp Java & Kiểu dữ liệuMiễn phí lượt xem

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ườngConcept
Menu in cố định trên bảng đènTập value của enum — đóng, không thêm lúc runtime
Gọi món ngoài menu bị từ chốiCompiler 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
Pitfall — đừng dùng ordinal() cho logic

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?

enumstatic 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/persistenceDùng .name() (String) tốt hơn .ordinal()Stable nếu không đổi value
Dùng khiTập hợp giá trị liên quan, có logicGiá 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...)enumint magic numbers
Phân loại cố định (loại sản phẩm, màu sắc)enumString hardcoded
Cần switch exhaustiveenumint/String (compiler không check)
Hằng số đơn lẻ như timeout, retry limitstatic final intenum (overkill)
Hằng số string như URL prefixstatic final Stringenum (không thêm value)
Magic number trong if❌ Không dùng gì cả — tạo named constantif (status == 2)
💡 Cách nhớ

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

📚 Deep Dive Oracle (optional)

Spec và API chính thức Java 21:

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 default vô 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

Tự kiểm tra
Q1
Enum 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() = 1ordinal 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.
Q2
Vì sao nên dùng enum thay static final int STATUS_ACTIVE = 1; static final int STATUS_INACTIVE = 2;?
  • Type safety: method nhận TrangThai không thể truyền nhầm int 999 hay giá trị khác — compiler chặn. Với int constant, 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_LY rõ nghĩa hơn status = 2.
Q3
Vì 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ỡ?
JLS §8.9 quy định enum constant là instance duy nhất, được JVM khởi tạo một lần khi load class; constructor ngầm hiểu là private. Nếu cho phép 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.
Q4
TrangThai.valueOf("moi") trả gì khi enum có value MOI? Xử lý input từ user thế nào cho an toàn?
Throw IllegalArgumentExceptionvalueOf() 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.
Q5
Vì sao switch expression với enum không nên thêm nhánh default khi đã liệt kê đủ mọi case?
Compiler biết tập value của enum là đóng → khi đủ case, switch expression được công nhận exhaustive mà không cần 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

Đặt 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