Bắt Đầu Nhanh: Nỗi Đau Cũ và Giải Pháp Mới (5 Phút)
Nếu bạn đã viết Java được một thời gian, chắc hẳn bạn thuộc làu cái nghi lễ này. Tạo một data class, viết constructor, viết getter, override equals(), override hashCode(), override toString(). Lặp đi lặp lại cho mọi DTO, value object, hay response model trong dự án.
Đây là cách một class Point đơn giản trông như thế nào ngày trước:
// Java 8–15: cái giá của boilerplate kiểu cổ điển
public final 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 instanceof Point)) return false;
Point p = (Point) o;
return x == p.x && y == p.y;
}
@Override
public int hashCode() { return Objects.hash(x, y); }
@Override
public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
}
Bây giờ hãy so sánh với Java 16+:
public record Point(int x, int y) {}
Chỉ một dòng. Compiler tự tạo canonical constructor, accessor (x() và y() — không phải getX()), cùng với equals, hashCode và toString chính xác một cách tự động. Record cũng ngầm định là final và immutable.
Thay đổi đơn giản đó cắt đi 30–40 dòng mỗi class. Trong một dự án Spring Boot điển hình với 20+ DTO, bạn có thể loại bỏ 600–800 dòng boilerplate hoàn toàn. Gánh nặng nhận thức khi đọc những thứ đó là có thật — loại bỏ chúng khiến code review thực sự trở nên hữu ích hơn.
Tìm Hiểu Sâu: Hiểu Từng Tính Năng
Records — Không Chỉ Là DTOs Đơn Giản
Records không chỉ là cú pháp gọn hơn (syntactic sugar). Chúng mã hóa một hợp đồng: kiểu này là một carrier minh bạch, bất biến của các thành phần của nó. Sự rõ ràng về ngữ nghĩa đó rất quan trọng khi bạn đọc code lạ lúc 2 giờ sáng trong lúc xử lý sự cố.
Bạn có thể thêm validation trong compact constructor:
public record Range(int min, int max) {
// Compact constructor: không cần lặp lại danh sách tham số
Range {
if (min > max) throw new IllegalArgumentException(
"min %d phải <= max %d".formatted(min, max)
);
}
}
Ngoài validation, records có thể implement interface và định nghĩa static method hoặc instance method bổ sung. Hạn chế duy nhất: không thêm instance field mới ngoài các thành phần đã khai báo.
public interface Measurable {
double length();
}
public record Segment(Point start, Point end) implements Measurable {
@Override
public double length() {
int dx = end.x() - start.x();
int dy = end.y() - start.y();
return Math.sqrt(dx * dx + dy * dy);
}
}
Sealed Classes — Phân Cấp Kiểu Có Kiểm Soát
Kế thừa mở có một vấn đề căn bản: bạn không bao giờ có thể liệt kê hết tất cả các subclass tồn tại. Viết một method nhận Shape, không có gì đảm bảo rằng không ai đó đã thêm một subclass Hexagon mà switch statement của bạn không xử lý.
Sealed class giải quyết điều này ở cấp độ ngôn ngữ:
// Chỉ Circle, Rectangle và Triangle được phép kế thừa
public sealed interface Shape
permits Circle, Rectangle, Triangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double width, double height) implements Shape {}
public record Triangle(double base, double height) implements Shape {}
Mệnh đề permits là tường minh và được kiểm tra tại compile time. Mỗi subclass được phép phải là final, sealed (tiếp tục hạn chế), hoặc non-sealed (mở lại có chủ ý).
Nếu bạn đã làm việc với enum của Rust hay sum type của Haskell, đây chính là tương đương trong Java — algebraic data types, cuối cùng cũng có trong JVM.
Pattern Matching — Switch Expression Thực Sự Có Ý Nghĩa
Pattern matching được giới thiệu theo từng giai đoạn. Phiên bản instanceof ổn định trong Java 16:
// Cách cũ
if (obj instanceof String) {
String s = (String) obj; // cast dư thừa
System.out.println(s.length());
}
// Java 16+ pattern variable
if (obj instanceof String s) {
System.out.println(s.length()); // s đã được định kiểu sẵn
}
Java 21 LTS mang lại switch pattern matching đầy đủ, kết hợp hoàn hảo với sealed class:
double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> 0.5 * t.base() * t.height();
// Không cần default — compiler biết đây là tất cả các case
};
}
Không cần case default. Thêm Pentagon vào sealed interface mà không cập nhật switch này, và compiler sẽ từ chối build. Lỗi được phát hiện tại compile time — không phải lúc 3 giờ sáng khi kỹ sư trực đang căng thẳng.
Nâng Cao: Kết Hợp Cả Ba Tính Năng
Kết hợp cả ba tính năng mới thực sự thú vị. Một expression evaluator đơn giản minh họa rõ ràng pattern này:
// Phân cấp sealed với records
public sealed interface Expr
permits Literal, Add, Multiply, Negate {}
public record Literal(double value) implements Expr {}
public record Add(Expr left, Expr right) implements Expr {}
public record Multiply(Expr left, Expr right) implements Expr {}
public record Negate(Expr expr) implements Expr {}
// Pattern matching để đánh giá — toàn diện và dễ đọc
static double eval(Expr expr) {
return switch (expr) {
case Literal(var v) -> v;
case Add(var l, var r) -> eval(l) + eval(r);
case Multiply(var l, var r) -> eval(l) * eval(r);
case Negate(var e) -> -eval(e);
};
}
Cú pháp Literal(var v) là record pattern deconstruction, có trong Java 21. Nó bind trực tiếp các thành phần của record trong case label — không cần gọi expr.value() bên trong branch.
Cần kiểm soát chi tiết hơn? Thêm guard với when:
String describe(Shape shape) {
return switch (shape) {
case Circle c when c.radius() > 100 -> "Hình tròn lớn";
case Circle c -> "Hình tròn nhỏ";
case Rectangle r when r.width() == r.height() -> "Hình vuông";
case Rectangle r -> "Hình chữ nhật";
case Triangle t -> "Hình tam giác";
};
}
Switch đánh giá từ trên xuống dưới, case khớp đầu tiên thắng. Không có if lồng nhau, không có fall-through bất ngờ.
Mẹo Thực Tế Cho Dự Án Thực Tế
Dùng Records cho API Response Models
Jackson 2.12+ xử lý records ngay lập tức — nó map JSON field trực tiếp vào constructor parameter. Thêm @JsonIgnoreProperties(ignoreUnknown = true) nếu API payload của bạn có các field không cần thiết:
public record UserResponse(
long id,
String username,
String email
) {}
Không cần @JsonProperty, không constructor rỗng, không Lombok. Sạch và bất biến theo mặc định.
Sealed cho Domain Events
Event sourcing và CQRS pattern hưởng lợi rất nhiều từ sealed interface. Liệt kê mọi kiểu event trong mệnh đề permits, và handler của bạn trở nên toàn diện — compiler bắt các case còn thiếu trước khi chúng đến production:
public sealed interface OrderEvent
permits OrderPlaced, OrderShipped, OrderCancelled {}
public record OrderPlaced(String orderId, List<String> items) implements OrderEvent {}
public record OrderShipped(String orderId, String trackingNumber) implements OrderEvent {}
public record OrderCancelled(String orderId, String reason) implements OrderEvent {}
void handleEvent(OrderEvent event) {
switch (event) {
case OrderPlaced e -> processNewOrder(e.orderId(), e.items());
case OrderShipped e -> notifyCustomer(e.orderId(), e.trackingNumber());
case OrderCancelled e -> refundOrder(e.orderId(), e.reason());
}
}
Kiểm Tra Phiên Bản Java Trước Khi Áp Dụng
- Java 16: Records ổn định,
instanceofpattern ổn định - Java 17 LTS: Sealed class ổn định — phiên bản tối thiểu được khuyến nghị cho các tính năng này
- Java 21 LTS: Switch pattern matching ổn định, record deconstruction ổn định — mục tiêu hiện tại
Vẫn đang dùng Java 11? Hãy bắt đầu lên kế hoạch nâng cấp ngay bây giờ. Java 17 LTS có extended support đến 2029; Java 21 LTS đến 2031. Cả hai đều không để bạn mắc kẹt, và đều cung cấp mọi tính năng được đề cập trong bài viết này.
Đừng Áp Dụng Records Ở Mọi Nơi
Records tỏa sáng với data carrier — DTO, value object, response model, configuration snapshot. Chúng không phù hợp với JPA entity (trạng thái có thể thay đổi, cần no-arg constructor) hay các class được xây dựng xung quanh kế thừa phức tạp. Hãy dùng đúng công cụ.
Kết hợp lại, Records, Sealed Classes và Pattern Matching mã hóa các quy tắc domain vào hệ thống kiểu — khiến các trạng thái không hợp lệ thất bại tại compile time thay vì lén lút vào production. Sự đảm bảo đó rất khó bổ sung sau khi codebase đã trưởng thành. Đáng để tích hợp ngay từ ngày đầu.

