Enum — tập giá trị cố định, type-safe thay cho magic number
Tìm hiểu kiểu dữ liệu enum trong Java: tính an toàn type-safe thay thế magic number, switch exhaustive, khai báo field và method, cùng các pitfall 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 bảng đèn mô tả kiểu enum như thế nào?
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. Tại sao Java cần đến enum thay vì hằng số nguyên truyền thống?
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 khai báo và sử dụng enum cơ bản như thế nào?
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. Cấu trúc switch kết hợp với enum mang lại lợi ích gì?
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
Thử ngẫmbạn vừa thêm một trạng thái đơn hàng mới vào enum, và một switch expression cũ trong dự án lập tức báo compile error — bạn coi đó là phiền toái, hay đúng thứ bạn muốn xảy ra? Vì sao?
5. Khai báo field và method bên trong enum hoạt động ra sao?
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. Các phương thức built-in chuẩn của enum gồm những gì?
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.
Thử ngẫmmột bảng database của bạn lưu trạng thái đơn hàng bằng số lấy từ ordinal() — nếu ai chèn thêm một trạng thái mới vào giữa danh sách, dữ liệu cũ trong bảng còn đúng nghĩa không?
7. Khi nào nên dùng enum thay cho 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ẻ.
Thử ngẫmdự án bạn có 4 loại phí vận chuyển, mỗi loại chỉ khác nhau ở một con số cố định, không có behavior riêng — bạn chọn enum có field hay static final cho từng biến? Vì sao?
8. Trình biên dịch Java sinh ra cấu trúc gì đằng sau một enum?

9. Mã nguồn ví dụ minh họa enum với thuộc tính và phương thức ra sao?
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. Các bẫy pitfall thường gặp nhất khi làm việc với enum là gì?
❌ 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. Tài liệu Deep Dive Oracle quy định gì về kiểu enum?
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. Điểm cốt lõi cần nhớ về enum trong Java là gì?
- Type safety: Enum định nghĩa tập giá trị đóng; compiler chặn tuyệt đối các giá trị không hợp lệ.
- Singleton instance: Mỗi giá trị enum là một thể hiện duy nhất được tạo khi nạp class, cho phép so sánh an toàn bằng
==. - Exhaustive switch: Switch expression với enum kiểm tra đủ trường hợp tại compile-time mà không cần nhánh default.
- Tránh ordinal(): Không dùng
.ordinal()cho logic hay lưu DB; dùng.name()hoặc thuộc tính mã định danh riêng.
13. Tự kiểm tra kiến thức về kiểu dữ liệu enum như thế nào?
- Q1Enum
TrangThaicó valuesMOI, XU_LY, XONG.TrangThai.MOI.ordinal()trả gì? Nếu thêmDANG_CHOvào đầu danh sách, ordinal củaMOIđổi không? - Q2Vì sao nên dùng
enumthaystatic final int STATUS_ACTIVE = 1; static final int STATUS_INACTIVE = 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ỡ? - Q4
TrangThai.valueOf("moi")trả gì khi enum có valueMOI? Xử lý input từ user thế nào cho an toàn? - Q5Vì sao switch expression với enum không nên thêm nhánh
defaultkhi đã liệt kê đủ mọi case?
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?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Hằng số và final — giá trị khắc trên đá
Bài tiếpThực hành tính chỉ số BMI và phân loại theo chuẩn WHO
Lộ trình Junior Java Backend Developer
Chặng 2/21 · Lập trình từ số 0 với Java — bài 18/32 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này