Record trong Java: lớp mang dữ liệu bất biến một dòng
Tìm hiểu Record trong Java 16+: cú pháp khai báo, compact constructor, accessor tự sinh, so sánh với class truyền thống và giới hạn kế thừa.
TL;DR: Record (từ Java 16) là cú pháp khai báo lớp mang dữ liệu bất biến (immutable data carrier) chỉ với một dòng code. Trình biên dịch tự động sinh ra các trường private final, canonical constructor, các accessor không có tiền tố get, cùng bộ ba equals, hashCode và toString. Dùng compact constructor để thẩm định dữ liệu đầu vào mà không cần gán thủ công. Ở bài trước về toString, equals và hashCode, ta đã thấy cách viết thủ công các phương thức này, và bài Class và object đã giới thiệu cách định nghĩa lớp đối tượng.
Bài trước bạn viết equals, hashCode, toString cho class Book với hàng chục dòng mã boilerplate chỉ để chứa dữ liệu. Java giải quyết triệt để sự cồng kềnh này bằng record (từ Java 16): một dòng khai báo duy nhất để compiler tự động tạo mọi thành phần thiết yếu.
1. Biểu mẫu khai báo một dòng phản ánh bản chất của record ra sao?
Viết class truyền thống với field, constructor, getter và các method so sánh giống như phải làm một bản báo cáo dài 5 trang chỉ để khai họ tên, tuổi tác. Record giống như một biểu mẫu 1 dòng: điền các ô thông tin cần thiết là hệ thống tự động hoàn thiện toàn bộ thủ tục còn lại.
| Đời thường | Java |
|---|---|
| Biểu mẫu 1 dòng | record Book(String title, int year) {} |
| Các ô trong biểu mẫu | Component (title, year) |
| Máy in tự điền thông tin khác | Compiler sinh constructor, accessor, equals... |
Record là class immutable công khai hình dạng dữ liệu (shape of data) ngay trên header khai báo. Toàn bộ code nhàm chán đều do compiler đảm nhận.
2. Cú pháp khai báo record và những thành phần compiler tự sinh là gì?
public record Book(String title, String author, int year) { }
Một dòng này sinh ra:
- Private final field cho mỗi component:
private final String title;v.v. - Canonical constructor
Book(String, String, int)gán tất cả field. - Accessor
title(),author(),year()— không phảigetTitle(). equals(Object)so dựa trên mọi component.hashCode()hash dựa trên mọi component.toString()trảBook[title=..., author=..., year=...].
Ranh giới ở đây đáng nhớ: bạn gõ đúng dòng khai báo, sáu thứ còn lại là compiler sinh — kể cả cái tên accessor.

Dùng:
Book b = new Book("Java", "Bloch", 2018);
System.out.println(b.title()); // Java — khong phai getTitle()
System.out.println(b); // Book[title=Java, author=Bloch, year=2018]
Book c = new Book("Java", "Bloch", 2018);
System.out.println(b.equals(c)); // true
System.out.println(b.hashCode() == c.hashCode()); // true
Viết class tương đương bằng tay: 30–40 dòng. Record: 1 dòng.
3. Compact constructor hỗ trợ thẩm định dữ liệu như thế nào?
Muốn validate mà không lặp lại danh sách parameter, hãy dùng compact constructor:
public record Book(String title, String author, int year) {
public Book {
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("title must not be blank");
}
if (year < 0) {
throw new IllegalArgumentException("year must be non-negative, got " + year);
}
// khong can gan this.title = title — compiler lam sau body nay
}
}
Đặc điểm khác biệt với constructor thông thường:
- Không có cặp ngoặc tròn
()sau tên:public Book {, không phảipublic Book(...) {. - Không cần gán field: compiler tự làm việc đó sau khi thân compact constructor chạy xong.
- Có thể chuẩn hóa parameter trước khi compiler thực hiện phép gán:
Java
public Book { title = title.trim(); // chuan hoa input }
3.1 Canonical constructor đầy đủ khi cần logic đặc biệt
Nếu compact constructor chưa đáp ứng, bạn có thể viết canonical constructor tường minh:
public record Book(String title, String author, int year) {
public Book(String title, String author, int year) {
this.title = Objects.requireNonNull(title);
this.author = Objects.requireNonNull(author);
this.year = year;
}
}
4. Nạp chồng constructor và static factory trong record áp dụng ra sao?
Record chỉ có duy nhất 1 canonical constructor. Muốn bổ sung thêm constructor khác, bạn phải gọi qua this(...):
public record Book(String title, String author, int year) {
public Book(String title) {
this(title, "Unknown", 0); // chain sang canonical
}
}
Hoặc cách tiếp cận được ưa chuộng hơn là sử dụng static factory method:
public record Book(String title, String author, int year) {
public static Book unknownAuthor(String title) {
return new Book(title, "Unknown", 0);
}
}
Book b = Book.unknownAuthor("Java");
5. Có thể khai báo thêm phương thức và thuộc tính trong record không?
Record hoàn toàn cho phép khai báo thêm các method nghiệp vụ:
public record Book(String title, String author, int year) {
public int yearsOld() {
return java.time.Year.now().getValue() - year;
}
public String displayTitle() {
return title + " (" + year + ")";
}
}
Tuy nhiên, bạn không được phép thêm bất kỳ instance field nào khác ngoài các component đã khai báo trong header (chỉ cho phép static field).
6. Những giới hạn kiến trúc nào khiến record không thể thay thế hoàn toàn class?
- Không hỗ trợ mutable state: Toàn bộ component của record đều là
final. Nếu cần setter để thay đổi dữ liệu theo thời gian, bạn bắt buộc phải dùng class truyền thống. - Không hỗ trợ kế thừa class: Record ngầm kế thừa
java.lang.Recordvà mang thuộc tínhfinal. Record có thể implement interface nhưng không thểextendsclass khác. - Không tương thích hoàn hảo với JPA entity: Các thực thể JPA thường yêu cầu no-arg constructor và field có thể sửa đổi để phục vụ lazy-loading proxy.
- Bất tiện khi có quá nhiều thuộc tính: Record với hơn 10 component sẽ trở nên khó đọc, nên cân nhắc gom nhóm thành các record lồng nhau.
7. Record kết hợp với pattern matching mang lại sức mạnh gì?
Từ Java 21, sự kết hợp giữa sealed interface, record và switch pattern matching tạo nên mô hình Đại số kiểu dữ liệu (ADT) cực kỳ mạnh mẽ:
sealed interface Shape permits Circle, Square, Rectangle {}
record Circle(double radius) implements Shape {}
record Square(double side) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
static double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square sq -> sq.side() * sq.side();
case Rectangle r -> r.width() * r.height();
};
}
Trình biên dịch tự động kiểm tra tính đầy đủ (exhaustive), đảm bảo mọi kiểu dữ liệu con đều được xử lý mà không cần nhánh default.
8. Khi nào nên ưu tiên dùng record thay vì class truyền thống?
✅ Nên dùng record khi:
- Định nghĩa DTO (Data Transfer Object) trong API.
- Đóng gói dữ liệu trả về từ method (tuple-like return).
- Tạo các Value Object bất biến (
Point,Money,DateRange). - Làm key cho
HashMaphoặc phần tử trongHashSet.
❌ Không nên dùng record khi:
- Đối tượng cần biến đổi trạng thái liên tục.
- Cần áp dụng kế thừa phân cấp từ một lớp cha.
- Ánh xạ ORM / JPA Entity.
9. Những cạm bẫy thường gặp khi làm việc với record là gì?
❌ Nhầm 1: Gọi accessor theo phong cách JavaBean (book.getTitle()).
✅ Accessor của record mang tên chính component: book.title().
❌ Nhầm 2: Khai báo thêm instance field bên ngoài header.
public record Book(String title) { private int views; } // COMPILE ERROR
✅ Chỉ static field mới hợp lệ bên trong thân record.
❌ Nhầm 3: Gán this.field = value trong compact constructor.
✅ Trong compact constructor, compiler sẽ tự động gán field sau khi hoàn thành việc validate.
❌ Nhầm 4: Nhầm tưởng record là immutable hoàn toàn khi chứa mutable collection.
public record Cart(List<Item> items) {} // items van co the bi mutate tu ben ngoai
✅ Luôn bọc phòng thủ: items = List.copyOf(items); trong compact constructor.
10. Oracle JEP 395 quy định triết lý cốt lõi của record ra sao?
Spec / reference chính thức:
- JEP 395: Records: triết lý thiết kế lớp mang dữ liệu danh định.
- JLS §8.10 — Record Classes: cú pháp và các quy tắc đặc thù.
- JEP 441: Pattern Matching for switch: tối ưu hóa xử lý record với switch expression.
11. Những điểm cốt lõi nào cần ghi nhớ về record?
- Record là cú pháp cô đọng giúp định nghĩa class mang dữ liệu bất biến: tự sinh
private finalfield, constructor, accessor và bộ ba method Object. - Sử dụng compact constructor để thẩm định hoặc chuẩn hóa tham số đầu vào mà không cần lặp lại danh sách biến hay gán
this.field. - Record ngầm kế thừa
java.lang.Recordvà mang đặc tínhfinal: không thể kế thừa class khác nhưng hoàn toàn có thể implement interface. - Tính bất biến của record phụ thuộc vào component: nếu chứa mutable collection, cần copy phòng thủ qua
List.copyOftrong compact constructor.
12. Tự kiểm tra mức độ nắm vững record trong Java
- Q1Viết lại class sau dưới dạng record (giữ nguyên chức năng):
public class Point { private final double x; private final double y; public Point(double x, double y) { this.x = x; this.y = y; } public double getX() { return x; } public double getY() { return y; } @Override public boolean equals(Object o) { ... } @Override public int hashCode() { ... } @Override public String toString() { ... } } - Q2Đoạn sau compile không?
public record User(String name, int age) { public User { if (age < 0) throw new IllegalArgumentException(...); this.name = name.trim(); this.age = age; } } - Q3Record sau có thật sự immutable không?
public record Cart(String customerId, List<String> items) { } Cart c = new Cart("A1", new ArrayList<>(List.of("apple", "banana"))); c.items().add("cherry"); System.out.println(c.items()); - Q4Khi nào nên chọn class thay vì record?
- Q5Record có thể implement interface không? Extends class không? Ví dụ minh hoạ.
Bài tiếp theo: Mini-challenge: Model hoá Library với record + class
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.
Bộ ba toString, equals và hashCode: ghi đè chuẩn
Bài tiếpMini-challenge: Model hoá thư viện với record + class
Lộ trình Junior Java Backend Developer
Chặng 3/21 · OOP: class, kế thừa, đa hình, abstraction — bài 6/16 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này