1. So sánh record và class: khác biệt chính là gì?
Trong Java, chúng ta có hai cách chính để mô tả kiểu dữ liệu của mình: qua lớp thông thường (class) và qua lớp record (record). Nhìn qua thì cả hai cách đều cho phép lưu trữ và xử lý dữ liệu. Nhưng nếu đào sâu hơn, có nhiều khác biệt hơn bạn tưởng!
Bảng so sánh: class vs record
| Đặc điểm | Lớp thông thường (class) | Lớp record (record) |
|---|---|---|
| Khả năng thay đổi | Tùy ý: có thể cho trường final hoặc không | Bất biến: mọi trường đều final |
| Kế thừa | Có thể kế thừa (extends), mặc định không phải final | Luôn là final, không thể là siêu lớp |
| Trường | Bất kỳ: static, non-static, final hoặc không final, mọi kiểu | Chỉ các thành phần của record (private final), cộng thêm các trường static |
| Getter/setter | Tự viết (hoặc tạo bằng Lombok) | Tự động tạo getter (tên phương thức trùng tên trường), không có setter |
| equals/hashCode/toString | Thường tự viết/tạo (equals, hashCode, toString) | Được tạo tự động dựa trên tất cả thành phần |
| Constructor | Tùy ý, bao nhiêu cũng được | Một constructor chính (cho tất cả thành phần), có thể thêm constructor rút gọn |
| Interface | Có thể triển khai | Có thể triển khai |
| Phương thức bổ sung | Tùy ý | Có thể thêm, nhưng chỉ phương thức (không phải trường) |
| Sử dụng trong collection | Có thể, nhưng cần triển khai đúng equals/hashCode | Rất phù hợp cho khóa/giá trị, mọi thứ đã được triển khai sẵn |
Ví dụ minh họa
Lớp thông thường:
public class Person {
private final String name;
private final int age;
public Person(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) { /* ... */ }
@Override
public int hashCode() { /* ... */ }
@Override
public String toString() { /* ... */ }
}
Lớp record:
public record Person(String name, int age) { }
Thế là xong! Chỉ một dòng mã — và ta có được điều tương tự (thậm chí tốt hơn). Và không còn rủi ro quên triển khai thứ quan trọng.
2. Các hạn chế của lớp record
Các lớp record không chỉ là “cú pháp ngắn gọn” mà là một khái niệm riêng với các quy tắc nghiêm ngặt. Hãy cùng xem chi tiết.
Record luôn là final
Theo định nghĩa, lớp record luôn final. Điều đó có nghĩa là bạn không thể tạo lớp con từ record:
public record Point(int x, int y) { }
// public class ColoredPoint extends Point { } // Lỗi biên dịch!
Nếu cần mở rộng hành vi, hãy dùng lớp thông thường hoặc composition (nhúng record vào lớp).
Record không thể là siêu lớp
Lớp record không thể là cha của các lớp khác, nó luôn final. Điều này hợp lý: nếu có thể, ai đó có thể thêm một trường có thể thay đổi — và toàn bộ khái niệm “dữ liệu bất biến” sẽ sụp đổ.
Chỉ có trường final (thành phần)
Mọi thành phần của record được khai báo trong phần đầu và mặc định là private final. Bạn không thể thêm trường non-static vào thân của record:
public record User(String login, String email) {
// int counter; // Lỗi! Không được thêm trường non-static
static int totalUsers = 0; // Được phép, đây là trường static
}
Không có setter
Lớp record không thể có setter cho các thành phần. Bất kỳ nỗ lực thêm phương thức như setX(int x) đều vô nghĩa: bạn không thể thay đổi giá trị trường sau khi tạo đối tượng.
public record Point(int x, int y) {
// public void setX(int x) { this.x = x; } // Lỗi: không thể thay đổi trường final
}
Không có constructor rỗng
Lớp record luôn chỉ có constructor chính nhận giá trị cho tất cả các thành phần. Không thể tạo record mà không cung cấp đủ dữ liệu:
Point p = new Point(1, 2); // OK
// Point p = new Point(); // Lỗi: không có constructor không tham số
Không có initializer non-static
Lớp record không thể chứa các initializer non-static (loại viết trong ngoặc nhọn ngoài các phương thức):
public record User(String login) {
// { /* ... */ } // Lỗi: cấm initializer non-static
}
Hạn chế về kế thừa
Lớp record không thể kế thừa tường minh lớp khác (ngoại trừ java.lang.Record, vốn ẩn như lớp cơ sở cho mọi record). Nhưng triển khai interface thì thoải mái!
public interface Printable {
void print();
}
public record Book(String title) implements Printable {
@Override
public void print() {
System.out.println("In sách: " + title);
}
}
Không phù hợp cho business logic phức tạp
record là về dữ liệu, không phải hành vi. Nếu đối tượng của bạn có logic phức tạp, trạng thái có thể thay đổi, “vòng đời” hay nhiều dependency — record sẽ không giúp ích. Hãy dùng lớp thông thường.
3. Khi nào nên dùng lớp record?
- DTO (Data Transfer Object): để truyền dữ liệu bất biến giữa các tầng ứng dụng, dịch vụ, microservice hoặc REST controller (ví dụ, trong phản hồi JSON).
- Value Object: các đối tượng chỉ được xác định bởi giá trị của chúng.
- Khóa và giá trị trong collection: khi việc triển khai đúng equals và hashCode là quan trọng (ví dụ, để dùng trong HashMap hoặc Set).
- Kết quả tính toán: khi cần trả về từ phương thức nhiều giá trị cùng lúc (ví dụ, record Pair<T, U>(T first, U second)).
Ví dụ: DTO cho REST controller
public record UserDto(String login, String email) { }
Giờ bạn có thể trả về đối tượng kiểu này từ controller mà không lo ai đó thay đổi các trường của nó.
Ví dụ: Khóa cho HashMap
public record Point(int x, int y) { }
Map<Point, String> pointNames = new HashMap<>();
pointNames.put(new Point(1, 2), "A");
pointNames.put(new Point(3, 4), "B");
// Mọi thứ hoạt động chính xác: equals và hashCode đã được triển khai sẵn!
4. Khi KHÔNG nên dùng lớp record
- Trạng thái có thể thay đổi: nếu ít nhất một trường cần thay đổi sau khi tạo đối tượng.
- Logic phức tạp: nếu đối tượng có hành vi phức tạp, nhiều phương thức, các đối tượng lồng nhau với trạng thái có thể thay đổi.
- Kế thừa: nếu cần hệ phân cấp lớp, lớp cơ sở trừu tượng, ghi đè phương thức.
- Thực thể business: ví dụ, các đối tượng sống trong cơ sở dữ liệu và có định danh duy nhất.
Ví dụ: khi cần lớp thông thường
public class Account {
private String id;
private int balance;
public Account(String id, int balance) {
this.id = id;
this.balance = balance;
}
public void deposit(int amount) { balance += amount; }
public void withdraw(int amount) { balance -= amount; }
// getters, setters, equals, hashCode, toString...
}
Ở đây có thể thấy rõ trạng thái của đối tượng thay đổi — record không phù hợp.
5. Ví dụ thực tế: chọn giữa record và class
Ví dụ 1: record — lựa chọn lý tưởng
public record Rectangle(int width, int height) {
public int area() {
return width * height;
}
}
- Hình chữ nhật được xác định chỉ bởi chiều rộng và chiều cao.
- Không cần thay đổi các giá trị này sau khi tạo.
- Có thể thêm phương thức hữu ích area().
- Những phần còn lại Java sẽ lo cho bạn.
Ví dụ 2: class — lựa chọn tốt hơn
public class MutableRectangle {
private int width;
private int height;
public MutableRectangle(int width, int height) {
this.width = width;
this.height = height;
}
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int area() { return width * height; }
}
Cần thay đổi kích thước hình chữ nhật sau khi tạo? Hãy dùng lớp thông thường.
6. Các lỗi thường gặp khi làm việc với lớp record
Lỗi số 1: cố gắng thêm trường non-static.
Lớp record không cho phép khai báo trường non-static ngoài danh sách thành phần. Nếu thử — trình biên dịch sẽ báo lỗi. Ví dụ:
public record City(String name) {
// int population; // Lỗi!
}
Lỗi số 2: muốn thêm setter.
Record không hỗ trợ setter cho các thành phần. Bất kỳ nỗ lực thay đổi giá trị trường sau khi tạo đối tượng — là lỗi biên dịch.
Lỗi số 3: cố gắng kế thừa record hoặc từ record.
Record luôn final. Không thể kế thừa từ record, và record không thể kế thừa lớp khác (ngoại trừ java.lang.Record ẩn).
Lỗi số 4: dùng record cho đối tượng mutable.
Nếu bạn dự định thay đổi trạng thái của đối tượng sau khi tạo — record không dành cho bạn! Hãy dùng lớp thông thường.
Lỗi số 5: quên các ràng buộc của constructor.
Lớp record bắt buộc phải có constructor nhận giá trị cho tất cả các thành phần. Không có constructor không tham số!
GO TO FULL VERSION