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ĩa | Convention đặt tên |
|---|---|---|---|
final biến | Local var, field, param | Không thể gán lại sau khi khởi tạo | camelCase thường |
static final | Field cấp class | Hằng số dùng chung, compile-time constant | UPPER_SNAKE_CASE |
enum | Kiểu dữ liệu mới | Tập hợp giá trị cố định, type-safe | UPPER_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 finalvới primitive hoặcStringliteral → 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 reference | Immutable object | |
|---|---|---|
| Reference thay đổi? | ❌ Không | Có thể thay đổi |
| Nội dung object thay đổi? | ✅ Vẫn được (nếu object mutable) | ❌ Không được |
| Ví dụ | final List<String> list | String, 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?
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 (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...) | 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) |
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ĩa | Ví dụ |
|---|---|---|
| Local variable | Không thể gán lại | final int x = 10; |
| Field | Gán 1 lần tại khai báo hoặc constructor | final double rate; |
| Parameter | Không thể gán lại trong method | void f(final String s) |
| Method | Subclass không thể override | public final void pay() |
| Class | Không thể extend (subclass) | public final class Token |
14. Pitfall tổng hợp
❌ final reference = immutable object: final List<String> list — list 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:
- 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. - JLS §4.12.4 — final Variables: quy tắc "blank final" (khai báo không gán ngay), compile-time constant definition.
- JLS §8.1.1.2 — final Classes:
finalclass không thể có subclass;abstract finallà compile error. - 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. 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
finalbiến: không thể gán lại.finalfield gán 1 lần tại khai báo hoặc constructor.finalmethod: không thể override trong subclass.finalclass: không thể extend. Ví dụ:String,Integer.finalreference ≠ immutable object: reference cố định, nhưng nội dung object vẫn thay đổi nếu object mutable.static finalhằng số cấp class: conventionUPPER_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
- Vì sao
final List<String> list = new ArrayList<>();cho phéplist.add("x")nhưng không cho phéplist = new ArrayList<>()? Stringlàfinal class— điều đó có nghĩa là gì? Tại saoStringcần làfinal?- Enum
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? - Viết utility class chứa hằng số
MAX_PAGE_SIZE = 100vàDEFAULT_CURRENCY = "VND"theo cách đúng (không dùng constant interface). - Vì sao nên dùng
enumthaystatic final int STATUS_ACTIVE = 1; static final int STATUS_INACTIVE = 2;? finalmethod vàfinalclass khác nhau thế nào?
Bài tiếp theo: Mini-challenge: Máy tính BMI