java 21 feature
Tung Bui Thanh (TungDaDev)10 mins read

Java đã bớt rườm rà đi nhiều. Từ những phiên bản đầy boilerplate, Java 17 đến Java 21 đi theo hướng gọn, rõ và an toàn hơn. Nếu bạn quan tâm tới code sạch hay kiến trúc hệ thống (như Clean Architecture hay Event Sourcing), các tính năng mới này giúp bỏ bớt phần "tiếng ồn" để code tập trung vào Domain.
Dưới đây là các tính năng ngôn ngữ đáng chú ý trong Java 17-21 và cách dùng chúng, nhất là trong Spring Boot. (Java 25, bản LTS tiếp theo, ra tháng 9/2025; mọi thứ trong bài vẫn dùng nguyên được, chỉ có String Templates ở cuối bài là đã bị rút lại.)
# records (java 16+, finalized)
Lâu nay ta hay dùng Lombok (@Data, @Value) để đỡ phải viết code rập khuôn. Giờ record là cấu trúc dữ liệu có sẵn của ngôn ngữ, và nó Immutable (bất biến), điều rất quan trọng để giữ dữ liệu toàn vẹn trong hệ thống phân tán và xử lý đồng thời. Có điều đó là bất biến "nông": các field là final, nhưng nếu một field là List có thể sửa được thì nội dung list vẫn bị đổi từ bên ngoài. Muốn chắc thì copy lại trong compact constructor, ví dụ items = List.copyOf(items);.
// Trình biên dịch tự động lo liệu: constructor, getters, equals, hashCode, toString
public record UserDTO(UUID id, String name, String email) {}
// Tích hợp Validation ngay từ vòng gửi xe với Compact Constructor
public record CreateOrderRequest(
@NotNull UUID productId,
@Positive int quantity,
@NotBlank String shippingAddress
) {
// Compact constructor — validation logic
public CreateOrderRequest {
if (quantity > 1000) throw new IllegalArgumentException("Max 1000 per order");
}
}
// Biến Record thành một Value Object trong DDD
public record Money(BigDecimal amount, String currency) {
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);
}
public boolean isPositive() {
return amount.compareTo(BigDecimal.ZERO) > 0;
}
}
// Spring integration
@RestController
public class UserController {
@GetMapping("/users/{id}")
public UserDTO getUser(@PathVariable UUID id) {
return new UserDTO(id, "John", "john@example.com");
}
}Khi nào dùng Records vs Lombok @Data:
| Records | Lombok @Data |
|---|---|
| Immutable (no setters) | Mutable (setters) |
| Simple DTOs, responses, events | JPA entities (cần no-arg constructor) |
| Rất hợp cho Value Objects trong DDD | Hợp với object cần Builder phức tạp (dù Record vẫn có thể custom Builder) |
| Không hỗ trợ extends (chỉ implements) | Hỗ trợ kế thừa hoàn toàn |
# sealed classes (java 17+)
Kiểm soát chặt hệ thống phân cấp class giúp Domain không bị phá bởi những implementation ngoài ý muốn. Trình biên dịch biết chính xác có bao nhiêu subtype, nên rẽ nhánh logic an toàn hơn nhiều. Lưu ý: sealed có từ Java 17, nhưng kiểu switch theo type như ví dụ dưới (pattern matching for switch) phải tới Java 21 mới chính thức.
// Khai báo rõ ràng: Chỉ Circle, Rectangle, Triangle mới được phép kế thừa Shape
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 {}
// Exhaustive Switch: Không cần "default" vì Compiler đã nắm rõ mọi ngả đường
public 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();
};
}
// Domain modeling
public sealed interface PaymentResult permits PaymentSuccess, PaymentFailed, PaymentPending {}
public record PaymentSuccess(String transactionId, Instant timestamp) implements PaymentResult {}
public record PaymentFailed(String errorCode, String message) implements PaymentResult {}
public record PaymentPending(String referenceId) implements PaymentResult {}
// Usage
public ResponseEntity<?> handleResult(PaymentResult result) {
return switch (result) {
case PaymentSuccess s -> ResponseEntity.ok(s);
case PaymentFailed f -> ResponseEntity.badRequest().body(f);
case PaymentPending p -> ResponseEntity.accepted().body(p);
};
}# pattern matching
# instanceof pattern (java 16+)
Hết cảnh ép kiểu (casting) thừa. Code đọc thuận hơn nhiều.
// Ngày xưa: Khai báo rồi ép kiểu
if (obj instanceof String) {
String s = (String) obj;
return s.length();
}
// Java 16+: Binding biến ngay lập tức
if (obj instanceof String s && s.length() > 5) {
return s.substring(0, 5);
}# switch pattern matching (java 21)
Kết hợp switch với instanceof, và cho phép phân tách dữ liệu (destructuring).
// Phân loại kết hợp Guard Clauses (when)
public String classify(Shape shape) {
return switch (shape) {
case Circle c when c.radius() > 100 -> "Vòng tròn cỡ lớn";
case Circle c -> "Vòng tròn tiêu chuẩn";
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";
};
}
// Phân rã Record (Destructuring) - Trực tiếp lấy biến từ bên trong Record
public double calculateArea(Shape shape) {
return switch (shape) {
case Circle(var radius) -> Math.PI * radius * radius;
case Rectangle(var w, var h) -> w * h;
case Triangle(var b, var h) -> 0.5 * b * h;
};
}# practical: exception handling
public APIResponse<?> handleException(Exception ex) {
return switch (ex) {
case NotFoundException e -> APIResponse.error("NOT_FOUND", e.getMessage());
case ValidationException e -> APIResponse.error("VALIDATION", e.getErrors());
case AccessDeniedException e -> APIResponse.error("FORBIDDEN", "Access denied");
case TimeoutException e -> APIResponse.error("TIMEOUT", "Service unavailable");
default -> APIResponse.error("INTERNAL", "Unexpected error");
};
}# text blocks (java 15+)
Không còn phải nối chuỗi bằng + và escape \n làm rối code. Text Blocks giữ nguyên định dạng như bạn viết.
// Multi-line strings
String sql = """
SELECT u.id, u.name, u.email
FROM users u
WHERE u.status = :status
AND u.created_date > :since
ORDER BY u.name
""";
// JSON template
String json = """
{
"name": "%s",
"email": "%s",
"role": "USER"
}
""".formatted(name, email);
// SQL in @Query
@Query(value = """
SELECT p.* FROM products p
JOIN categories c ON c.id = p.category_id
WHERE c.name = :category
AND p.price BETWEEN :min AND :max
ORDER BY p.created_at DESC
""", nativeQuery = true)
List<Product> findByCategoryAndPrice(String category, BigDecimal min, BigDecimal max);# sequenced collections (java 21)
Bao năm nay lấy phần tử cuối của một danh sách phải viết list.get(list.size() - 1) khá vụng. Interface mới này thêm các method nhất quán cho việc đó.
// Interface hierarchy: SequencedCollection, SequencedSet, SequencedMap
List<String> list = List.of("a", "b", "c");
// First/Last access (no more list.get(0), list.get(list.size()-1))
String first = list.getFirst(); // "a"
String last = list.getLast(); // "c"
// Reversed view
List<String> reversed = list.reversed(); // ["c", "b", "a"]
// Works with LinkedHashSet, TreeSet, LinkedHashMap
SequencedMap<String, Integer> map = new LinkedHashMap<>();
map.putFirst("key1", 1);
map.putLast("key2", 2);
Map.Entry<String, Integer> firstEntry = map.firstEntry();
Map.Entry<String, Integer> lastEntry = map.lastEntry();# enhanced switch expressions (java 14+)
// Arrow syntax — no fall-through
String label = switch (status) {
case ACTIVE -> "Active";
case INACTIVE -> "Inactive";
case SUSPENDED -> "Suspended";
};
// Multiple values per case
int numDays = switch (month) {
case JANUARY, MARCH, MAY, JULY, AUGUST, OCTOBER, DECEMBER -> 31;
case APRIL, JUNE, SEPTEMBER, NOVEMBER -> 30;
case FEBRUARY -> isLeapYear ? 29 : 28;
};
// Block with yield
String description = switch (errorCode) {
case 404 -> "Not Found";
case 500 -> {
log.error("Internal server error");
yield "Internal Error";
}
default -> "Unknown Error (%d)".formatted(errorCode);
};# unnamed patterns & variables (preview ở java 21, chính thức từ java 22)
// Underscore for unused variables (Java 22+, hoặc Java 21 với --enable-preview)
try {
process();
} catch (IOException _) {
// don't need the exception variable
return fallback();
}
// In enhanced for
for (var _ : collection) {
count++;
}
// In lambda
map.forEach((_, value) -> process(value));# string templates (preview, java 21)
// STR processor (preview, requires --enable-preview)
String name = "World";
String greeting = STR."Hello, \{name}!";
// Expression interpolation
String info = STR."User \{user.getName()} has \{user.getOrders().size()} orders";
// Multi-line
String html = STR."""
<div class="user">
<span>\{user.getName()}</span>
<span>\{user.getEmail()}</span>
</div>
""";
// Note: preview ở Java 21-22, bị rút khỏi JDK 23 và chưa có bản thay thế
// Hiện tại dùng String.formatted() hoặc MessageFormat
String safe = "User %s has %d orders".formatted(name, count);String Templates là preview ở Java 21 (JEP 430) và 22, rồi bị rút hẳn khỏi JDK 23 để thiết kế lại; tới nay chưa có bản mới. Đừng dùng nó trong code thật, vì code dùng --enable-preview chỉ chạy được trên đúng bản JDK đó.
# practical combinations
Tính năng ngôn ngữ chỉ phát huy khi đặt vào đúng bài toán. Thử xem cách kết hợp Records + Sealed Interfaces + Pattern Matching để giải một bài toán phức tạp hơn.
# domain modeling with records + sealed + pattern matching
// Giới hạn rạch ròi các Event có thể xảy ra trong Domain Order
public sealed interface DomainEvent permits
OrderPlaced, OrderConfirmed, OrderShipped, OrderCancelled {}
public record OrderPlaced(UUID orderId, UUID customerId, List<LineItem> items, Instant at)
implements DomainEvent {}
public record OrderConfirmed(UUID orderId, String paymentRef, Instant at)
implements DomainEvent {}
public record OrderShipped(UUID orderId, String trackingNumber, Instant at)
implements DomainEvent {}
public record OrderCancelled(UUID orderId, String reason, Instant at)
implements DomainEvent {}
// Event handler
public Order apply(Order current, DomainEvent event) {
return switch (event) {
case OrderPlaced e -> Order.create(e.orderId(), e.customerId(), e.items());
case OrderConfirmed e -> current.confirm(e.paymentRef());
case OrderShipped e -> current.ship(e.trackingNumber());
case OrderCancelled e -> current.cancel(e.reason());
};
}# result type (railway-oriented)
Thay vì ném Exception khắp các layer, ta bọc kết quả trả về trong kiểu Result, để luồng Thành công/Thất bại hiện rõ ngay trên method signature.
public sealed interface Result<T> permits Result.Success, Result.Failure {
record Success<T>(T value) implements Result<T> {}
record Failure<T>(String error, String code) implements Result<T> {}
static <T> Result<T> ok(T value) { return new Success<>(value); }
static <T> Result<T> fail(String error, String code) { return new Failure<>(error, code); }
}
// Usage
public Result<User> findUser(UUID id) {
return userRepo.findById(id)
.<Result<User>>map(Result::ok)
.orElse(Result.fail("User not found", "USER_404"));
}
// Handle in controller
public ResponseEntity<?> getUser(UUID id) {
return switch (userService.findUser(id)) {
case Result.Success<User>(var user) -> ResponseEntity.ok(user);
case Result.Failure<User>(var msg, var code) -> ResponseEntity.status(404).body(msg);
};
}# migration tips
| Old Pattern | Java 21 Replacement |
|---|---|
| Lombok @Value DTO | record |
| Visitor pattern | Sealed + switch pattern matching |
instanceof + cast | Pattern binding variable |
| String concatenation | Text blocks + .formatted() |
Collections.unmodifiableList(list) | List.copyOf(list) |
list.get(list.size()-1) | list.getLast() |
| Enum with abstract method | Sealed interface + records |
Vài dòng trong bảng không phải thay thế 1-1. Collections.unmodifiableList trả về một view (list gốc đổi thì view đổi theo), còn List.copyOf tạo bản copy và ném NullPointerException nếu có phần tử null. Enum có abstract method vẫn là lựa chọn tốt khi các giá trị là hằng số không mang dữ liệu riêng; sealed + records chỉ đáng đổi khi mỗi biến thể cần các field khác nhau.
# lời kết
Nhảy từ Java 8, Java 11 lên Java 21 thì lợi ích thấy rõ nhất nằm ở cú pháp: gọn hơn, bỏ bớt phần thừa, để mình tập trung vào Nghiệp Vụ (Business Logic) thay vì loay hoay với boilerplate.
Chuyển từ Class thông thường sang Record, và dùng Sealed Interface cho các hierarchy đóng, sẽ giúp dự án backend dễ bảo trì và mở rộng hơn khá nhiều.
Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!
Happy coding 😎 👍🏻 🚀 🔥.