Java — Từ Zero đến Senior/Cú pháp Java & Kiểu dữ liệu/Hằng số, final và enum — giá trị khắc trên đá
7/8
~16 phútCú pháp Java & Kiểu dữ liệu

Hằng số, final và enum — giá trị khắc trên đá

Nắm vững 3 cơ chế 'không đổi được' trong Java: final biến/method/class, static final hằng số cấp class, và enum type-safe cho tập giá trị cố định. Phân biệt final reference với immutable object.

Bài 1 đến 6 tập trung vào giá trị thay đổi được. Bài này đi vào chiều ngược lại: khi nào bạn muốn một giá trị không được phép thay đổi — và Java có 3 cơ chế khác nhau để thể hiện điều đó.

1. Analogy — Khắc trên đá

Hãy nghĩ về hai kiểu "ghi":

  • Viết trên giấy (biến thường) — dễ tẩy xoá, gán lại tuỳ ý.
  • Khắc trên đá (final) — một khi đã khắc, không đổi được. Muốn "đổi" phải đục bỏ tảng đá cũ, lấy tảng mới.

Tương tự, 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.

2. Ba cơ chế "không đổi được" trong Java

Cơ chếÁp dụng lênÝ nghĩaConvention đặt tên
final biếnLocal var, field, paramKhông thể gán lại sau khi khởi tạocamelCase thường
static finalField cấp classHằng số dùng chung, compile-time constantUPPER_SNAKE_CASE
enumKiểu dữ liệu mớiTập hợp giá trị cố định, type-safeUPPER_SNAKE_CASE cho values

3. final trên biến

3.1 Local variable

final int MAX_SIZE = 100;
MAX_SIZE = 200;  // COMPILE ERROR: cannot assign a value to final variable

final local variable phải được gán đúng 1 lần trước khi dùng. Không nhất thiết gán ngay lúc khai báo:

final int threshold;
if (mode == "strict") {
    threshold = 10;
} else {
    threshold = 50;
}
// threshold da duoc gan 1 lan -- OK
System.out.println(threshold);

// threshold = 99;  // COMPILE ERROR

3.2 Field

public class Circle {
    final double radius;  // phai gan trong constructor hoac ngay tai day

    Circle(double r) {
        this.radius = r;  // gan 1 lan trong constructor
        // this.radius = r * 2;  // COMPILE ERROR: da gan roi
    }
}

3.3 Parameter

void process(final String input) {
    // input = "other";  // COMPILE ERROR
    System.out.println(input.toUpperCase());  // doc thoai mai
}

Khai báo parameter final ngăn vô tình gán lại bên trong method — hữu ích để đọc code rõ intent.

4. static final — hằng số cấp class

public class Config {
    public static final int    MAX_RETRIES    = 3;
    public static final double TAX_RATE       = 0.1;
    public static final String DEFAULT_LOCALE = "vi-VN";
}

// Dung o noi khac:
int retry = Config.MAX_RETRIES;  // khong can tao object Config

Quy tắc:

  • Convention đặt tên: UPPER_SNAKE_CASE
  • public static final với primitive hoặc String literal → compile-time constant — compiler inline giá trị vào chỗ dùng, không cần truy cập field lúc runtime.

⚠️ Anti-pattern: Constant Interface

// ANTI-PATTERN -- khong lam the nay
interface AppConstants {
    int MAX_RETRIES = 3;  // ngam la public static final
    String LOCALE = "vi-VN";
}
class MyService implements AppConstants { ... }  // BAD!

"Constant interface pattern" bị xem là anti-pattern trong Java (Effective Java Item 22): interface nên mô tả hành vi, không nên là nơi chứa hằng số. Hằng số nên đặt trong utility class hoặc enum. Nếu class implements interface hằng số, các constant đó "lộ" ra API public của class — không cần thiết.

Thay vào đó:

// DUNG: utility class voi constructor private
public final class AppConstants {
    private AppConstants() {}  // ngan instantiate
    public static final int MAX_RETRIES = 3;
    public static final String LOCALE = "vi-VN";
}

5. final trên class và method

5.1 final class — không thể extends

public final class Ssn {  // Social Security Number
    private final String value;
    Ssn(String value) { this.value = value; }
    // khong ai co the extends Ssn va override logic validation
}

// class MaliciousSsn extends Ssn { ... }  // COMPILE ERROR

Java standard library dùng nhiều final class: String, Integer, Double, LocalDate, UUID... Lợi ích:

  • Security: ngăn ai ghi đè method để bypass logic quan trọng
  • Performance: JIT compiler biết không có subclass → có thể inline method call
  • Design: truyền đạt "class này là final, không được mở rộng"

5.2 final method — không thể override

public class Payment {
    public final void deduct(double amount) {
        // logic kieu toan quan trong, khong duoc phep override
        validate(amount);
        doDeduct(amount);
        log(amount);
    }

    protected void doDeduct(double amount) { /* subclass co the customize */ }
}

final method cho phép subclass kế thừa nhưng không thể thay đổi method đó. Hữu ích trong template method pattern: skeleton của thuật toán là final, các bước cụ thể thì protected có thể override.

6. final reference KHÔNG phải immutable object

Đây là điểm quan trọng nhất phần này:

final List<String> list = new ArrayList<>();

// list = new ArrayList<>();  // COMPILE ERROR: reference khong doi duoc

list.add("hello");  // OK! -- reference co dinh, nhung object thi van sua duoc
list.add("world");  // OK!
list.clear();       // OK!

final chỉ cố định địa chỉ (reference), không cố định nội dung object mà reference trỏ tới.

final referenceImmutable object
Reference thay đổi?❌ KhôngCó thể thay đổi
Nội dung object thay đổi?✅ Vẫn được (nếu object mutable)❌ Không được
Ví dụfinal List<String> listString, List.of(...), LocalDate
// Immutable object thuc su: List.of(), String, LocalDate
final List<String> immutableList = List.of("a", "b", "c");
// immutableList.add("d");  // UnsupportedOperationException -- object immutable
// immutableList = new ArrayList<>();  // COMPILE ERROR -- final reference

// final + mutable object: reference co dinh nhung noi dung doi duoc
final StringBuilder sb = new StringBuilder("hi");
sb.append("!");   // OK -- object van sua duoc
// sb = new StringBuilder();  // COMPILE ERROR -- reference co dinh

💡 💡 Cách nhớ — final reference vs immutable

final như khắc địa chỉ nhà lên đá: địa chỉ không đổi được, nhưng đồ trong nhà vẫn di chuyển được.

Immutable object như ngôi nhà xi măng đổ liền: nội dung không đổi, nhưng địa chỉ (reference) vẫn có thể trỏ sang.

7. enum — type-safe constants

7.1 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ệ.

7.2 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
}

7.3 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

7.4 Enum có field và method

public enum PhanLoaiBmi {
    GAY("Gau", 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"

7.5 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ư ma hoặc thuTu nếu cần số thứ tự ổn định.

8. 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 (MauSac.DO/XANH/VANG), 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.

💡 💡 Cách nhớ

  • final: "Khắc đá" — một khi gán, không đổi reference được. Nhưng nội dung object vẫn thay đổi nếu object mutable.
  • enum: "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.

9. Immutable class pattern

Kết hợp final class + final fields + không có setter → class hoàn toàn immutable:

public final class Money {
    private final long amountCents;  // tien tinh bang xu, tranh float
    private final String currency;   // "VND", "USD"

    public Money(long amountCents, String currency) {
        if (amountCents < 0) throw new IllegalArgumentException("amount < 0");
        this.amountCents = amountCents;
        this.currency    = currency;
    }

    public long getAmountCents() { return amountCents; }
    public String getCurrency()  { return currency; }

    // "Thay doi" tao object moi, khong sua object hien tai
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) throw new IllegalArgumentException("Currency mismatch");
        return new Money(this.amountCents + other.amountCents, this.currency);
    }

    @Override
    public String toString() {
        return amountCents + " " + currency;
    }
}

// Su dung
Money a = new Money(100_000, "VND");
Money b = new Money(50_000, "VND");
Money total = a.add(b);      // tao Money moi 150_000 VND
System.out.println(total);   // 150000 VND
System.out.println(a);       // 100000 VND -- a khong doi

Immutable class có nhiều lợi ích: thread-safe tự nhiên, dễ reasoning, an toàn làm HashMap key.

10. 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

11. ✅/❌ Khi nào dùng enum vs int constants vs String constants?

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)

12. Code example — enum với field, switch, final biến

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 coTheBHuy() {
            return this == PENDING || this == PROCESSING;
        }
    }

    static final int MAX_RETRY = 3;
    static final String CURRENCY = "VND";

    public static void main(String[] args) {
        TrangThai t = TrangThai.PROCESSING;

        System.out.println("Trang thai: " + t.getMoTa());
        System.out.println("Co the huy: " + t.coTheBHuy());

        // 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);

        // final bien local
        final double thue = 0.1;
        // thue = 0.2;  // COMPILE ERROR

        // final reference -- object van doi duoc
        final java.util.List<String> items = new java.util.ArrayList<>();
        items.add("Laptop");  // OK
        items.add("Mouse");   // OK
        // items = new java.util.ArrayList<>();  // COMPILE ERROR

        System.out.println("So san pham: " + items.size());
        System.out.println("Max retry: " + MAX_RETRY);
    }
}

13. Bảng tổng hợp: final trên từng target

TargetÝ nghĩaVí dụ
Local variableKhông thể gán lạifinal int x = 10;
FieldGán 1 lần tại khai báo hoặc constructorfinal double rate;
ParameterKhông thể gán lại trong methodvoid f(final String s)
MethodSubclass không thể overridepublic final void pay()
ClassKhông thể extend (subclass)public final class Token

14. Pitfall tổng hợp

final reference = immutable object: final List<String> listlist không thể trỏ sang list khác, nhưng list.add(...) vẫn OK.

Constant Interface anti-pattern: interface chứa hằng số và class implements nó — hằng số lộ ra API public. Dùng utility class final class với constructor private thay thế.

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.

Magic number: if (status == 2) thay vì if (status == TrangThai.PROCESSING) — khó đọc, không type-safe.

Quên private AppConstants() {}: utility class hằng số nên có constructor private để tránh bị khởi tạo không cần thiết.

15. 📚 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. ordinal() là thứ tự khai báo (0-indexed) — spec không đảm bảo giá trị ổn định qua refactor. JLS §4.12.4 phân biệt "final variable" (không gán lại) và "compile-time constant" (primitive/String literal + final + gán ngay) — compile-time constant được compiler inline.

16. Tóm tắt

  • final biến: không thể gán lại. final field gán 1 lần tại khai báo hoặc constructor.
  • final method: không thể override trong subclass.
  • final class: không thể extend. Ví dụ: String, Integer.
  • final reference ≠ immutable object: reference cố định, nhưng nội dung object vẫn thay đổi nếu object mutable.
  • static final hằng số cấp class: convention UPPER_SNAKE_CASE. Đặt trong utility class, không dùng constant interface.
  • enum: type-safe constant set. Có field/method. Switch exhaustive. Dùng thay magic number cho tập giá trị liên quan.
  • Không dùng ordinal() cho logic nghiệp vụ — thêm field riêng.

17. Tự kiểm tra

  1. Vì sao final List<String> list = new ArrayList<>(); cho phép list.add("x") nhưng không cho phép list = new ArrayList<>()?
  2. Stringfinal class — điều đó có nghĩa là gì? Tại sao String cần là final?
  3. 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?
  4. Viết utility class chứa hằng số MAX_PAGE_SIZE = 100DEFAULT_CURRENCY = "VND" theo cách đúng (không dùng constant interface).
  5. Vì sao nên dùng enum thay static final int STATUS_ACTIVE = 1; static final int STATUS_INACTIVE = 2;?
  6. final method và final class khác nhau thế nào?

Bài tiếp theo: Mini-challenge: Máy tính BMI