99 Ngày Java — Ngày 20: Record & lớp bất biến
Giai đoạn OOP khép lại với một câu hỏi thực dụng: phần lớn lớp ta viết hằng ngày chỉ để mang dữ liệu — một DTO, một kết quả truy vấn, một cặp tọa độ. Với lớp truyền thống, mỗi lớp như vậy kéo theo constructor, getter, equals/hashCode/toString — hàng chục dòng không chứa bất kỳ logic nghiệp vụ nào. Record (chuẩn từ Java 16) thu gọn tất cả về một dòng khai báo, và quan trọng hơn cú pháp: record bất biến theo mặc định — nền tảng của thiết kế an toàn.

Record — lớp dữ liệu trong một dòng
// Lớp dữ liệu truyền thống — hàng chục dòng boilerplate:
public final class NhanVienDTO {
private final String ten;
private final double luong;
public NhanVienDTO(String ten, double luong) { ... }
public String getTen() { ... }
public double getLuong() { ... }
@Override public boolean equals(Object o) { ... }
@Override public int hashCode() { ... }
@Override public String toString() { ... }
}
// Record (chuẩn từ Java 16) — MỘT dòng khai báo, cùng kết quả:
public record NhanVienDTO(String ten, double luong) { }
// Compiler tự sinh: field private final, canonical constructor,
// accessor ten() / luong() (không có tiền tố get),
// equals / hashCode / toString theo TOÀN BỘ thành phần
NhanVienDTO nv = new NhanVienDTO("An", 20_000_000);
nv.ten(); // "An"
// nv.setTen(...) ✗ không tồn tại — record là BẤT BIẾNKhai báo
recordliệt kê các thành phần (component) — compiler sinh fieldprivate final, constructor đầy đủ (canonical constructor), accessor trùng tên thành phần, và bộ baequals/hashCode/toString.Accessor của record không có tiền tố
get:nv.ten()thay vìnv.getTen()— khác biệt đáng nhớ khi làm việc với thư viện cũ kỳ vọng chuẩn JavaBean.Record ngầm định là
final(Ngày 18) và không thểextendslớp khác — nhưng implements interface (Ngày 17) thì được.
Compact constructor — validation không lặp lại
public record NhanVienDTO(String ten, double luong) {
public NhanVienDTO { // COMPACT constructor: không lặp lại
if (luong < 0) { // danh sách tham số — dành riêng cho
throw new IllegalArgumentException( // validation & chuẩn hóa
"Lương không được âm: " + luong);
}
ten = ten.trim(); // gán lại tham số TRƯỚC khi
} // compiler gán vào field
public static NhanVienDTO thucTapSinh(String ten) { // static factory
return new NhanVienDTO(ten, 5_000_000); // vẫn dùng được như thường
}
public double luongNam() { // method bổ sung: cho phép —
return luong * 12; // miễn là không thêm TRẠNG THÁI
}
}Compact constructor bỏ danh sách tham số — thân của nó chạy trước khi compiler gán tham số vào field: đúng chỗ cho validation và chuẩn hóa dữ liệu, cùng tinh thần fail-fast của bài abstract class (Ngày 16).
Record vẫn nhận static factory method, method bổ sung và static field — giới hạn duy nhất: không thêm instance field ngoài các thành phần đã khai báo, giữ nguyên tính bất biến.
So với Lombok và JavaBean
// So sánh ba cách tiếp cận cho lớp dữ liệu:
//
// JavaBean thủ công getter/setter viết tay — dài dòng, CÓ THỂ GHI
// Lombok @Data sinh code lúc biên dịch qua annotation processor —
// gọn, nhưng vẫn mutable mặc định + phụ thuộc thư viện
// record cú pháp CHÍNH THỨC của ngôn ngữ — bất biến,
// không phụ thuộc, ngữ nghĩa rõ: "bộ dữ liệu có tên"
//
// Chỗ record KHÔNG phù hợp:
// ✗ JPA entity — Hibernate cần no-arg constructor + setter (Spring Ngày 22)
// ✗ Đối tượng có vòng đời thay đổi trạng thái liên tục
//
// Chỗ record là lựa chọn ĐÚNG:
// ✓ DTO request/response — PageResponse của Spring Ngày 18 chính là record
// ✓ Value object: tọa độ, khoảng tiền, dải ngày
// ✓ Kết quả trung gian nhiều thành phần thay cho Object[]Khác biệt cốt lõi không nằm ở số dòng code mà ở ngữ nghĩa: record tuyên bố với người đọc "đây là bộ dữ liệu có tên, bất biến, so sánh theo giá trị" — một cam kết mà compiler thực thi, không phải quy ước.
Lombok vẫn hữu dụng ở những chỗ record không phù hợp (entity, builder cho đối tượng nhiều field tùy chọn) — hai công cụ bổ trợ, không loại trừ nhau.
Lưu ý cho series Spring: JPA entity không dùng record — Hibernate cần no-arg constructor và khả năng thay đổi trạng thái (chi tiết ở Spring Ngày 22); ranh giới đúng là entity là class, DTO là record.
Value semantics — danh tính nằm ở dữ liệu
NhanVienDTO a = new NhanVienDTO("An", 20_000_000);
NhanVienDTO b = new NhanVienDTO("An", 20_000_000);
a.equals(b); // true — so sánh theo GIÁ TRỊ của từng thành phần
a == b; // false — vẫn là hai đối tượng khác nhau trên heap
// "Value semantics": danh tính của record nằm ở DỮ LIỆU nó mang,
// không phải địa chỉ bộ nhớ — đúng nghĩa một "giá trị".
// Bất biến + equals đúng chuẩn = an toàn làm key trong HashMap (Ngày 24),
// an toàn chia sẻ giữa các luồng (Ngày 69) — không ai thay đổi được nội dung.
// Java 21 — record pattern: phân rã record ngay trong instanceof/switch:
if (obj instanceof NhanVienDTO(String ten, double luong)) {
System.out.println(ten + " — " + luong); // ten, luong sẵn sàng dùng
}Hai record cùng thành phần là bằng nhau theo
equals— value semantics chuẩn mực, trái với lớp thường nơiequalsmặc định so sánh danh tính (địa chỉ); hợp đồngequals/hashCodeđầy đủ hẹn Ngày 24.Bất biến là tính chất vận hành đáng giá nhất: chia sẻ record giữa các luồng không cần đồng bộ hóa, dùng làm key trong
HashMapkhông sợ "key biến dạng" sau khi đưa vào.Record pattern (chuẩn từ Java 21) phân rã record ngay trong
instanceof/switch— mảnh ghép pattern matching thứ ba sau Ngày 06 và Ngày 15.
Bài tập nhỏ
Chuyển một lớp DTO tự viết (đủ constructor, getter,
equals/hashCode/toString) thành record — đếm số dòng trước và sau.Thêm compact constructor từ chối lương âm — viết test khởi tạo hợp lệ và không hợp lệ để kiểm chứng.
Tạo hai record cùng giá trị: in kết quả
equalsvà==— giải thích khác biệt bằng khái niệm value semantics.Dùng record pattern trong một
switchxử lýObjectcó thể làNhanVienDTOhoặcString— đối chiếu cú pháp với pattern matching Ngày 15.
Kết luận
Record hoàn thiện bộ công cụ mô hình hóa của giai đoạn OOP: lớp dữ liệu một dòng, bất biến mặc định, validation qua compact constructor, và value semantics chuẩn mực — ranh giới sử dụng rõ ràng: DTO và value object dùng record, entity và đối tượng có vòng đời dùng class. Mười ngày OOP (11–20) đã đủ bốn trụ cột cùng các công cụ hiện đại. Ngày 21 mở giai đoạn Core API với chủ đề hiệu năng đầu tiên: StringBuilder và xử lý chuỗi nâng cao — vì sao nối chuỗi bằng + trong vòng lặp là vấn đề, và String pool hoạt động ra sao. Hẹn gặp lại!
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


