Backend

99 Ngày Java — Ngày 20: Record & lớp bất biến

SSite Admin
16 tháng 08, 2026 6 phút đọc 1 lượt xem
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.

Sketchnote Ngày 20: record trong Java — cú pháp một dòng, compact constructor, so với Lombok, value semantics

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ẾN
  • Khai báo record liệt kê các thành phần (component) — compiler sinh field private final, constructor đầy đủ (canonical constructor), accessor trùng tên thành phần, và bộ ba equals/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ể extends lớ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.

Bàn làm việc tối giản — record loại bỏ boilerplate để phần khai báo chỉ còn đúng dữ liệu cần mang

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ơi equals mặc định so sánh danh tính (địa chỉ); hợp đồng equals/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 HashMap khô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ả equals== — giải thích khác biệt bằng khái niệm value semantics.

  • Dùng record pattern trong một switch xử lý Object có thể là NhanVienDTO hoặc String — đố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!

S

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.

Bài viết liên quan

99 Ngày Spring — Ngày 20: CORS

Ngày 20 của 99 Ngày Spring: same-origin policy — luật của trình duyệt, preflight OPTIONS và vì sao một lần gọi sinh hai request, cấu hình @CrossOrigin và global theo profile, cùng các quy tắc an toàn: không mở * ở production, đọc lỗi CORS ở DevTools, và cái bẫy khi thêm Spring Security.

16 thg 8, 20266 phút0
99 Ngày Spring — Ngày 19: Upload & download file

Ngày 19 của 99 Ngày Spring: upload với MultipartFile và tên lưu trữ do server sinh, giới hạn max-file-size/max-request-size với lỗi 413 chuẩn ProblemDetail, download stream qua Resource với Content-Disposition, chặn path traversal bằng normalize + startsWith, và checklist an toàn cho production.

15 thg 8, 20265 phút52
99 Ngày Java — Ngày 19: Inner class & anonymous class

Ngày 19 của 99 Ngày Java: bốn dạng nested class — member inner (tham chiếu ngầm tới đối tượng ngoài), static nested (lựa chọn mặc định), local class trong method, anonymous class dùng một lần — cùng quy tắc capture effectively final và bảng quyết định chọn dạng phù hợp.

15 thg 8, 20266 phút19