Records
Sau bài này, bạn sẽ:
- Hiểu Records (Java 16+) — bộ chứa dữ liệu bất biến (immutable data carriers) tự động generate constructor, accessors, equals/hashCode/toString
- Biết cách custom Records với compact constructors và thêm methods
- Nắm được restrictions: không thể extend class khác, fields final, không có setter
- Phân biệt Records vs regular classes vs Lombok @Data
- Áp dụng Records cho DTOs, value objects, và multi-return values
Bài trước: Text Blocks — Đã học về multi-line string literals. Bài này sẽ tìm hiểu Records — cách tạo data classes ngắn gọn không boilerplate.
Giới thiệu
Records được giới thiệu như preview feature trong Java 14, và trở thành standard feature trong Java 16 (March 2021). Records là một loại class đặc biệt được thiết kế để làm bộ chứa dữ liệu bất biến (immutable data carriers).
Vấn đề Records giải quyết
Trước Records: Code rập khuôn (boilerplate)
Để tạo một data class đơn giản, bạn cần viết rất nhiều code:
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public String toString() {
return "Point{x=" + x + ", y=" + y + '}';
}
}
// 30+ lines cho một class đơn giản!
Với Records: 1 dòng
public record Point(int x, int y) {}
// Chỉ 1 dòng!
- Canonical constructor:
Point(int x, int y) - Accessor methods:
x(),y()(không phảigetX(),getY()) - equals(): So sánh tất cả fields
- hashCode(): Dựa trên tất cả fields
- toString(): Format
Point[x=1, y=2]
Record vs Class — Compiler tạo gì?
Record Component Lifecycle
Cú pháp Record
Khai báo cơ bản
// Syntax: record ClassName(parameters) {}
public record Person(String name, int age) {}
// Sử dụng
var person = new Person("Alice", 30);
System.out.println(person.name()); // Alice (không phải getName())
System.out.println(person.age()); // 30
System.out.println(person); // Person[name=Alice, age=30]
Record với nhiều fields
public record Customer(
long id,
String name,
String email,
LocalDate registeredDate,
boolean active
) {}
var customer = new Customer(
1L,
"John Doe",
"[email protected]",
LocalDate.now(),
true
);
// Accessor methods
System.out.println(customer.id()); // 1
System.out.println(customer.name()); // John Doe
System.out.println(customer.registeredDate()); // 2024-01-15
Constructor thu gọn (Compact Constructor)
Records cho phép constructor thu gọn (compact constructor) để validate hoặc normalize data:
public record Point(int x, int y) {
// Compact constructor - không cần parameters
public Point {
if (x < 0 || y < 0) {
throw new IllegalArgumentException(
"Coordinates must be non-negative"
);
}
}
}
// Usage
var p1 = new Point(10, 20); // OK
var p2 = new Point(-1, 20); // IllegalArgumentException
Normalize data trong compact constructor
public record Person(String name, int age) {
public Person {
// Normalize name: trim và capitalize
name = name.trim();
if (!name.isEmpty()) {
name = name.substring(0, 1).toUpperCase()
+ name.substring(1).toLowerCase();
}
// Validate age
if (age < 0 || age > 150) {
throw new IllegalArgumentException("Invalid age: " + age);
}
}
}
var person = new Person(" aLiCe ", 30);
System.out.println(person.name()); // "Alice" (normalized)
Các trường hợp đặc biệt của compact constructor
Bản sao phòng thủ (Defensive Copy)
Khi record chứa collection, compact constructor nên tạo bản sao phòng thủ:
public record Team(String name, List<String> members) {
public Team {
// ⚠️ Nếu không copy, caller có thể sửa list sau khi tạo record
members = List.copyOf(members); // Defensive copy → unmodifiable
}
}
var members = new ArrayList<>(List.of("Alice", "Bob"));
var team = new Team("Dev", members);
members.add("Charlie"); // Không ảnh hưởng team.members()!
System.out.println(team.members()); // [Alice, Bob]
Chuẩn hóa dữ liệu (Normalization)
public record Email(String value) {
public Email {
if (value == null || !value.contains("@")) {
throw new IllegalArgumentException("Invalid email: " + value);
}
value = value.toLowerCase().trim(); // Normalize
}
}
var email = new Email(" [email protected] ");
System.out.println(email.value()); // "[email protected]"
Trong compact constructor, bạn KHÔNG ĐƯỢC gán trực tiếp this.field = value. Việc gán xảy ra tự động ở cuối compact constructor. Bạn chỉ gán lại tham số:
public record Point(int x, int y) {
public Point {
// ❌ this.x = x; // COMPILE ERROR trong compact constructor
// ✅ x = Math.abs(x); // Gán lại tham số — OK
}
}
Canonical constructor (explicit)
Nếu cần full control, dùng canonical constructor:
public record Range(int start, int end) {
// Canonical constructor với explicit parameters
public Range(int start, int end) {
if (start > end) {
throw new IllegalArgumentException(
"start must be <= end"
);
}
this.start = start;
this.end = end;
}
}
Custom Methods trong Record
Records có thể có custom methods như class bình thường:
public record Rectangle(double width, double height) {
// Compact constructor cho validation
public Rectangle {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("Dimensions must be positive");
}
}
// Custom methods
public double area() {
return width * height;
}
public double perimeter() {
return 2 * (width + height);
}
public boolean isSquare() {
return width == height;
}
// Static factory method
public static Rectangle square(double side) {
return new Rectangle(side, side);
}
}
// Usage
var rect = new Rectangle(5, 10);
System.out.println(rect.area()); // 50.0
System.out.println(rect.perimeter()); // 30.0
System.out.println(rect.isSquare()); // false
var square = Rectangle.square(5);
System.out.println(square.isSquare()); // true
Record vs Class truyền thống
| Đặc điểm | Record | Class |
|---|---|---|
| Code rập khuôn (boilerplate) | Minimal | Nhiều |
| Tính bất biến (immutability) | Mặc định | Phải tự implement |
| Inheritance | Không thể extend | Có thể extend |
| Fields | Final only | Mutable/Immutable |
| Constructor | Tự động generate | Phải viết |
| Getters | fieldName() | getFieldName() |
| equals/hashCode | Tự động | Phải override |
| toString | Tự động | Phải override |
| Use case | Bộ chứa dữ liệu (data carriers) | Business logic |
Cây quyết định: Khi nào dùng Record vs Class vs Lombok?
💡 Quy tắc ngón tay cái: Nếu đang dùng Java 16+ và cần immutable data class → dùng Record. Nếu cần mutable hoặc inheritance → dùng regular class hoặc Lombok.
Code comparison
// TRADITIONAL CLASS - 40+ lines
public final class PersonClass {
private final String name;
private final int age;
public PersonClass(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof PersonClass)) return false;
PersonClass that = (PersonClass) o;
return age == that.age && Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "PersonClass{name='" + name + "', age=" + age + '}';
}
}
// RECORD - 1 line
public record PersonRecord(String name, int age) {}
Record vs Lombok @Data
Lombok là library phổ biến để giảm boilerplate, nhưng có những khác biệt:
| Đặc điểm | Record | Lombok @Data |
|---|---|---|
| Phụ thuộc (dependency) | Built-in Java | External library |
| Tính bất biến (immutability) | Bắt buộc | Optional (@Value) |
| Getters | name() | getName() |
| Compile-time | Native compiler | Annotation processing |
| IDE support | Built-in | Plugin required |
| Performance | Native | Same (compile-time) |
// LOMBOK @Data (mutable)
@Data
public class Person {
private String name;
private int age;
}
var person = new Person();
person.setName("Alice"); // Mutable
// LOMBOK @Value (immutable)
@Value
public class Person {
String name;
int age;
}
// RECORD (immutable)
public record Person(String name, int age) {}
- Records: Khi dùng Java 16+, cần immutable data
- Lombok @Data: Khi cần mutable objects
- Lombok @Value: Khi stuck với Java 8-15, cần immutable objects
Restrictions (Giới hạn)
1. Không thể extend classes
// ERROR - Records không thể extend classes
public record Point(int x, int y) extends Shape {} // COMPILE ERROR
Records implicitly extend java.lang.Record, nên không thể extend class khác.
2. Tất cả fields phải final
public record Person(String name, int age) {
// ERROR - Không thể có mutable fields
private int counter; // COMPILE ERROR
// ERROR - Không thể thay đổi record fields
public void setAge(int age) {
this.age = age; // COMPILE ERROR - age is final
}
}
3. Không thể khai báo instance fields
public record Person(String name, int age) {
// ERROR - Chỉ được khai báo static fields
private String nickname; // COMPILE ERROR
// OK - Static fields
private static final String DEFAULT_NAME = "Unknown";
}
Tổng hợp các giới hạn và khả năng
Record implements Interface
Records CÓ THỂ implement interfaces:
public interface Drawable {
void draw();
}
public record Point(int x, int y) implements Drawable {
@Override
public void draw() {
System.out.println("Drawing point at (" + x + ", " + y + ")");
}
}
// Polymorphism
Drawable drawable = new Point(10, 20);
drawable.draw(); // Drawing point at (10, 20)
Ví dụ: Comparable
public record Student(String name, double gpa)
implements Comparable<Student> {
@Override
public int compareTo(Student other) {
return Double.compare(this.gpa, other.gpa);
}
}
var students = List.of(
new Student("Alice", 3.8),
new Student("Bob", 3.5),
new Student("Charlie", 3.9)
);
Collections.sort(students);
students.forEach(System.out::println);
// Student[name=Bob, gpa=3.5]
// Student[name=Alice, gpa=3.8]
// Student[name=Charlie, gpa=3.9]
Khi nào dùng Records
1. DTOs (Data Transfer Objects)
// API Response
public record UserResponse(
long id,
String username,
String email,
LocalDateTime createdAt
) {}
// Controller
@GetMapping("/users/{id}")
public UserResponse getUser(@PathVariable long id) {
User user = userService.findById(id);
return new UserResponse(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getCreatedAt()
);
}
2. Value Objects (Domain-Driven Design)
public record Money(BigDecimal amount, String currency) {
public Money {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount must be non-negative");
}
if (currency == null || currency.isBlank()) {
throw new IllegalArgumentException("Currency is required");
}
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(
this.amount.add(other.amount),
this.currency
);
}
}
var price1 = new Money(new BigDecimal("100"), "USD");
var price2 = new Money(new BigDecimal("50"), "USD");
var total = price1.add(price2);
System.out.println(total); // Money[amount=150, currency=USD]
3. Configuration Objects
public record DatabaseConfig(
String host,
int port,
String database,
String username,
String password
) {
public DatabaseConfig {
if (port < 1 || port > 65535) {
throw new IllegalArgumentException("Invalid port: " + port);
}
}
public String jdbcUrl() {
return String.format("jdbc:postgresql://%s:%d/%s",
host, port, database);
}
}
var config = new DatabaseConfig(
"localhost",
5432,
"mydb",
"user",
"pass"
);
4. Query Results (Tuple-like structures)
// Complex query result
public record OrderSummary(
String customerName,
int orderCount,
BigDecimal totalAmount
) {}
// Repository method
public List<OrderSummary> getOrderSummaries() {
String sql = """
SELECT c.name, COUNT(o.id), SUM(o.total)
FROM customers c
JOIN orders o ON c.id = o.customer_id
GROUP BY c.name
""";
// Map ResultSet to Record
return jdbcTemplate.query(sql, (rs, rowNum) ->
new OrderSummary(
rs.getString(1),
rs.getInt(2),
rs.getBigDecimal(3)
)
);
}
Ví dụ thực tế: API Response với Records
Before Records (Java 8-15)
public class ApiResponse<T> {
private final boolean success;
private final T data;
private final String error;
private final LocalDateTime timestamp;
public ApiResponse(boolean success, T data, String error) {
this.success = success;
this.data = data;
this.error = error;
this.timestamp = LocalDateTime.now();
}
// Getters
public boolean isSuccess() { return success; }
public T getData() { return data; }
public String getError() { return error; }
public LocalDateTime getTimestamp() { return timestamp; }
// Static factory methods
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, data, null);
}
public static <T> ApiResponse<T> error(String error) {
return new ApiResponse<>(false, null, error);
}
// equals, hashCode, toString...
}
With Records (Java 16+)
public record ApiResponse<T>(
boolean success,
T data,
String error,
LocalDateTime timestamp
) {
// Static factory methods
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, data, null, LocalDateTime.now());
}
public static <T> ApiResponse<T> error(String error) {
return new ApiResponse<>(false, null, error, LocalDateTime.now());
}
}
// Usage
var successResponse = ApiResponse.success(
new UserResponse(1L, "alice", "[email protected]", LocalDateTime.now())
);
var errorResponse = ApiResponse.<UserResponse>error("User not found");
Nested Records
Records có thể chứa records khác:
public record Address(String street, String city, String country) {}
public record Person(String name, int age, Address address) {}
// Usage
var address = new Address("123 Main St", "New York", "USA");
var person = new Person("Alice", 30, address);
System.out.println(person.address().city()); // New York
Records + Sealed Classes + Pattern Matching
Sự kết hợp Records, Sealed Classes và Pattern Matching tạo nên mô hình dữ liệu đại số (Algebraic Data Types) mạnh mẽ:
// Mô hình kết quả thanh toán
sealed interface PaymentResult permits PaymentSuccess, PaymentFailed, PaymentPending {}
record PaymentSuccess(String transactionId, BigDecimal amount,
LocalDateTime processedAt) implements PaymentResult {}
record PaymentFailed(String errorCode, String message) implements PaymentResult {}
record PaymentPending(String referenceId, Duration estimatedWait) implements PaymentResult {}
// Xử lý kết quả — compiler kiểm tra exhaustiveness
public String handlePayment(PaymentResult result) {
return switch (result) {
case PaymentSuccess(var txId, var amount, var time) ->
"Thanh toán thành công: " + amount + " VND, mã: " + txId;
case PaymentFailed(var code, var msg) ->
"Thanh toán thất bại [" + code + "]: " + msg;
case PaymentPending(var ref, var wait) ->
"Đang xử lý, mã tham chiếu: " + ref + ", dự kiến: " + wait.toMinutes() + " phút";
};
// Không cần default — compiler biết đã xử lý hết!
}
Mô hình này tương đương sealed trait + case class trong Scala, hoặc enum trong Rust. Java đạt được cùng mức biểu đạt nhờ kết hợp 3 tính năng.
Local Records (Java 16+)
Có thể khai báo records bên trong methods:
public void processOrders(List<Order> orders) {
// Local record
record OrderGroup(String status, List<Order> orders) {}
var grouped = orders.stream()
.collect(Collectors.groupingBy(Order::getStatus))
.entrySet().stream()
.map(e -> new OrderGroup(e.getKey(), e.getValue()))
.toList();
grouped.forEach(group ->
System.out.println(group.status() + ": " + group.orders().size())
);
}
Performance
Records có performance giống hệt classes truyền thống vì:
- Compile thành bytecode tương tự
- JIT compiler optimize giống nhau
- Không có magic runtime processing
Memory: Records thậm chí nhẹ hơn vì compiler có thể optimize tốt hơn với immutable objects.
Bên trong hoạt động ra sao
Record biên dịch thành gì?
Khi bạn viết record Point(int x, int y) {}, compiler tạo ra một class tương đương:
💡 Class Diagram giải thích: Record Point kế thừa ngầm từ
java.lang.Recordvà compiler tự động tạo tất cả boilerplate code. Các fields làprivate final, accessor methods tênx()vày()(không phảigetX(),getY()).
Equivalent code:
// Compiler generates:
public final class Point extends java.lang.Record {
private final int x;
private final int y;
public Point(int x, int y) { // Canonical constructor
this.x = x;
this.y = y;
}
public int x() { return x; } // Accessor (KHÔNG phải getX())
public int y() { return y; }
@Override
public boolean equals(Object o) { ... } // Dựa trên TẤT CẢ components
@Override
public int hashCode() { ... } // Dựa trên TẤT CẢ components
@Override
public String toString() { ... } // "Point[x=1, y=2]"
}
Những điểm quan trọng:
- Record luôn là
final— không thể bị kế thừa - Tất cả field là
private final— không thể thay đổi sau khởi tạo - Accessor method tên
x()vày(), KHÔNG PHẢIgetX()vàgetY() - Record kế thừa ngầm từ
java.lang.Record— không thểextendsclass khác equals()so sánh tất cả components,hashCode()dựa trên tất cả components
Record Serialization — An toàn hơn Class
Khi deserialize một record, Java luôn gọi canonical constructor — khác với class bình thường (gọi default constructor hoặc readObject()). Điều này đảm bảo validation trong compact constructor luôn chạy, ngay cả khi data đến từ serialized stream.
public record Age(int value) implements Serializable {
public Age {
if (value < 0 || value > 150) {
throw new IllegalArgumentException("Invalid age: " + value);
}
}
}
// Khi deserialize:
// Class: bypass constructor, gán trực tiếp vào fields → có thể tạo invalid state
// Record: LUÔN gọi canonical constructor → validation chạy → an toàn!
Record là lựa chọn an toàn hơn class cho Serialization vì:
- Canonical constructor luôn chạy → validation đảm bảo
- Không có
readObject(),readResolve()→ không có gadget chain attack - Fields luôn final → không bị modify sau deserialize
RecordComponent Reflection API
Java cung cấp API reflection riêng cho Records:
import java.lang.reflect.RecordComponent;
record Person(String name, int age) {}
// Lấy thông tin components
RecordComponent[] components = Person.class.getRecordComponents();
for (RecordComponent rc : components) {
System.out.printf("Component: %s, Type: %s%n",
rc.getName(), rc.getType().getSimpleName());
}
// Output:
// Component: name, Type: String
// Component: age, Type: int
// Kiểm tra class có phải Record không
System.out.println(Person.class.isRecord()); // true
System.out.println(String.class.isRecord()); // false
Generic Records — Edge Cases
// Generic record
record Pair<A, B>(A first, B second) {}
var pair = new Pair<>("Hello", 42);
// pair.first() trả về String
// pair.second() trả về Integer
// Bounded generic record
record Wrapper<T extends Comparable<T>>(T value)
implements Comparable<Wrapper<T>> {
@Override
public int compareTo(Wrapper<T> other) {
return this.value.compareTo(other.value);
}
}
var w1 = new Wrapper<>(10);
var w2 = new Wrapper<>(20);
System.out.println(w1.compareTo(w2)); // -1
- Record accessor method tên
name(), KHÔNG PHẢIgetName()— đề thi hay đánh lừa bằng getter truyền thống - Record KHÔNG THỂ
extendsclass nào khác (kế thừa ngầmjava.lang.Record) - Record CÓ THỂ
implementsinterface - Canonical constructor phải gán TẤT CẢ components — nếu thiếu sẽ compile error
- Compact constructor KHÔNG ĐƯỢC dùng
this.field =— chỉ gán lại tham số - Record có thể có
staticfields nhưng KHÔNG THỂ có instance fields ngoài components equals()vàhashCode()tự động dựa trên tất cả components — không thể bỏ qua component nào- Local record (khai báo trong method) — hợp lệ từ Java 16
Khi deserialize record, canonical constructor LUÔN chạy. Đề thi có thể cho record với validation trong compact constructor và hỏi: "Deserialize data invalid sẽ xảy ra gì?" → Answer: IllegalArgumentException từ constructor (không phải InvalidObjectException).
equals() của record so sánh TẤT CẢ components. Không thể exclude component nào. Nếu cần custom equals logic, phải override nhưng không nên — vi phạm record semantics.
record Point(int x, int y, String label) {}
var p1 = new Point(1, 2, "A");
var p2 = new Point(1, 2, "B");
System.out.println(p1.equals(p2)); // false! — label khác
// Không thể tạo record equals chỉ so sánh x, y mà bỏ qua label
Record class là restricted form of class declaration. Record body có thể chứa: compact constructor, canonical constructor, other constructors (phải delegate tới canonical), static fields, static methods, instance methods, nested types. KHÔNG THỂ chứa instance fields ngoài components.
Tham khảo: JLS §8.10
Thử thách: Compile hay không?
Câu 1
record Person(String name, int age) {
void setName(String name) {
this.name = name;
}
}
Đáp án
Không compile. Field name là final, không thể gán lại. Record là immutable — không có setter.
Câu 2
record Student(String name, double gpa) {
public Student {
this.name = name.toUpperCase();
}
}
Đáp án
Không compile. Trong compact constructor, không được dùng this.name =. Phải viết: name = name.toUpperCase(); (gán lại tham số, không phải field).
Câu 3
record Point(int x, int y) {}
record ColorPoint(int x, int y, String color) extends Point {}
Đáp án
Không compile. Record không thể extends class hay record khác. Record kế thừa ngầm từ java.lang.Record.
Kết luận
Khi nào dùng Records
✅ NÊN dùng Records khi:
- Cần bộ chứa dữ liệu bất biến (immutable data carriers)
- DTOs, value objects, configuration
- Giảm code rập khuôn (boilerplate)
- Java 16+ available
❌ KHÔNG nên dùng Records khi:
- Cần mutable objects
- Cần inheritance (extend classes)
- Business logic phức tạp
- Stuck với Java < 16
Bài tập thực hành
Bài 1: Tạo Record cho Order System
Tạo records cho một hệ thống đặt hàng:
Product(long id, String name, BigDecimal price)OrderItem(Product product, int quantity)- có methodtotalPrice()Order(long id, String customerId, List<OrderItem> items, LocalDateTime createdAt)- có methodgrandTotal()
Bài 2: Validation với Compact Constructor
Tạo record Email(String value) với validation:
- Không null/empty
- Chứa ký tự @
- Format đúng (có thể dùng regex)
Bài 3: Record implements Interface
Tạo interface Identifiable<T> với method T getId(), và record User(Long id, String name) implement interface này.