Chuyển tới nội dung chính

Records

Mục tiêu bài học

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!
Records tự động generate
  • Canonical constructor: Point(int x, int y)
  • Accessor methods: x(), y() (không phải getX(), 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]"
Compact constructor và gán field

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ểmRecordClass
Code rập khuôn (boilerplate)MinimalNhiều
Tính bất biến (immutability)Mặc địnhPhải tự implement
InheritanceKhông thể extendCó thể extend
FieldsFinal onlyMutable/Immutable
ConstructorTự động generatePhải viết
GettersfieldName()getFieldName()
equals/hashCodeTự độngPhải override
toStringTự độngPhải override
Use caseBộ 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ểmRecordLombok @Data
Phụ thuộc (dependency)Built-in JavaExternal library
Tính bất biến (immutability)Bắt buộcOptional (@Value)
Gettersname()getName()
Compile-timeNative compilerAnnotation processing
IDE supportBuilt-inPlugin required
PerformanceNativeSame (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) {}
Khi nào dùng Records vs Lombok
  • 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

Performance của Records

Records có performance giống hệt classes truyền thống vì:

  1. Compile thành bytecode tương tự
  2. JIT compiler optimize giống nhau
  3. 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.Record và compiler tự động tạo tất cả boilerplate code. Các fields là private final, accessor methods tên x()y() (không phải getX(), 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()y(), KHÔNG PHẢI getX()getY()
  • Record kế thừa ngầm từ java.lang.Record — không thể extends class 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 Serialization Best Practice

Record là lựa chọn an toàn hơn class cho Serialization vì:

  1. Canonical constructor luôn chạy → validation đảm bảo
  2. Không có readObject(), readResolve() → không có gadget chain attack
  3. 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
OCP Exam Tips
  • Record accessor method tên name(), KHÔNG PHẢI getName() — đề thi hay đánh lừa bằng getter truyền thống
  • Record KHÔNG THỂ extends class nào khác (kế thừa ngầm java.lang.Record)
  • Record CÓ THỂ implements interface
  • 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ó static fields nhưng KHÔNG THỂ có instance fields ngoài components
  • equals()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
OCP Trap — Record Serialization

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).

OCP Trap — Record equals() dùng ALL components

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
📖 JLS §8.10 Record Declarations

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 namefinal, 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ó method totalPrice()
  • Order(long id, String customerId, List<OrderItem> items, LocalDateTime createdAt) - có method grandTotal()

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.

Tài liệu tham khảo

Đọc thêm