Java cơ bản #8: record, enum và equals, hashCode, toString
Ba phương thức mọi đối tượng đều có — toString, equals, hashCode — vì sao phải ghi đè chúng cùng nhau và cái bẫy khi quên; record (Java 16+) viết lớp dữ liệu bất biến trong một dòng; enum cho tập giá trị cố định, có thuộc tính, phương thức và dùng trong switch.
11 phút đọc
Mục lục
Bài 7 nói mọi lớp đều ngầm kế thừa Object. Ba phương thức thừa hưởng từ Object — toString, equals, hashCode — trông vô hại, nhưng hiểu sai chúng gây ra những lỗi rất khó tìm: đối tượng "biến mất" khỏi HashSet, hai sản phẩm giống hệt nhau mà so sánh ra khác.
Bài này giải thích ba phương thức đó, rồi giới thiệu hai công cụ giúp bạn gần như không phải tự viết chúng: record cho dữ liệu và enum cho tập giá trị cố định.
toString
Mặc định, in một đối tượng ra màn hình cho chuỗi vô nghĩa kiểu SanPham@1b6d3586 (tên lớp + mã băm). Ghi đè toString để in thông tin hữu ích — rất có ích khi gỡ lỗi và ghi log:
class SanPham {
private final String ma;
private final String ten;
private final long gia;
SanPham(String ma, String ten, long gia) {
this.ma = ma;
this.ten = ten;
this.gia = gia;
}
@Override
public String toString() {
return "SanPham[ma=%s, ten=%s, gia=%,d]".formatted(ma, ten, gia);
}
}
void main() {
var sp = new SanPham("SP01", "Bàn phím", 850_000);
IO.println(sp); // println tự gọi toString()
IO.println("Vừa thêm: " + sp); // nối chuỗi cũng gọi toString()
}equals: hai đối tượng có "bằng nhau" không
== so sánh địa chỉ. equals mặc định (của Object) cũng so sánh địa chỉ. Muốn hai sản phẩm cùng mã được coi là bằng nhau, phải ghi đè equals theo đúng hợp đồng:
import java.util.HashSet;
import java.util.Objects;
class Diem {
private final int x, y;
Diem(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (this == o) return true; // cùng đối tượng
if (!(o instanceof Diem khac)) return false; // khác kiểu hoặc null
return x == khac.x && y == khac.y; // so sánh từng trường quan trọng
}
@Override
public int hashCode() {
return Objects.hash(x, y); // ĐỒNG BỘ với equals
}
@Override
public String toString() { return "(" + x + ", " + y + ")"; }
}
class DiemQuenHashCode {
private final int x, y;
DiemQuenHashCode(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
return o instanceof DiemQuenHashCode k && x == k.x && y == k.y;
}
// quên hashCode!
}
void main() {
var a = new Diem(1, 2);
var b = new Diem(1, 2);
IO.println(a == b); // false — hai đối tượng khác nhau
IO.println(a.equals(b)); // true — cùng nội dung
var tapHop = new HashSet<Diem>();
tapHop.add(a);
IO.println(tapHop.contains(b)); // true
var tapLoi = new HashSet<DiemQuenHashCode>();
tapLoi.add(new DiemQuenHashCode(1, 2));
IO.println(tapLoi.contains(new DiemQuenHashCode(1, 2))); // false — "biến mất"!
}Hợp đồng equals–hashCode
- Nếu
a.equals(b)thìa.hashCode() == b.hashCode()— bắt buộc. - Hai đối tượng khác nhau có thể trùng hashCode (va chạm), không sao.
equalsphải phản xạ, đối xứng, bắc cầu, nhất quán, vàx.equals(null)làfalse.
HashSet, HashMap (bài 9) tìm phần tử bằng cách tính hashCode để chọn "ngăn", rồi mới dùng equals trong ngăn đó. Quên ghi đè hashCode → hai đối tượng "bằng nhau" rơi vào hai ngăn khác nhau → không tìm thấy. Quy tắc: ghi đè equals thì luôn ghi đè hashCode.
Lưu ý: đừng dùng trường có thể thay đổi trong
equals/hashCodecủa đối tượng nằm trongHashSet/HashMap. Sửa trường đó sau khi thêm vào tập hợp → đối tượng nằm sai "ngăn", không tìm lại được. Đối tượng bất biến tránh được hoàn toàn vấn đề này.
Viết tay ba phương thức này vừa dài vừa dễ sai. May là có record.
record: lớp dữ liệu trong một dòng
Từ Java 16, record là cách khai báo lớp chỉ để mang dữ liệu bất biến:
import java.util.HashSet;
record Diem(int x, int y) {}
record SanPham(String ma, String ten, long gia) {
SanPham { // constructor rút gọn: chỉ để kiểm tra/chuẩn hoá
if (gia < 0) throw new IllegalArgumentException("Giá không được âm");
ten = ten.strip();
}
SanPham giamGia(int phanTram) { // thêm phương thức bình thường được
return new SanPham(ma, ten, gia * (100 - phanTram) / 100); // "sửa" = tạo bản mới
}
static SanPham mienPhi(String ma, String ten) { // phương thức static (factory) cũng được
return new SanPham(ma, ten, 0);
}
}
void main() {
var p = new Diem(3, 4);
IO.println(p); // Diem[x=3, y=4] — toString có sẵn
IO.println(p.x() + p.y()); // accessor tên x(), không phải getX()
IO.println(p.equals(new Diem(3, 4))); // true — equals/hashCode có sẵn
var tap = new HashSet<Diem>();
tap.add(new Diem(1, 1));
IO.println(tap.contains(new Diem(1, 1))); // true
var sp = new SanPham("SP01", " Chuột không dây ", 400_000);
var spSale = sp.giamGia(25);
IO.println(sp + " → " + spSale);
IO.println(SanPham.mienPhi("QT", "Móc khoá"));
}Một dòng record Diem(int x, int y) {} cho bạn: các trường private final, constructor đầy đủ, accessor x()/y(), equals, hashCode, toString.
| Record làm được | Record không làm được |
|---|---|
| Thêm phương thức, phương thức static, constructor phụ | Kế thừa lớp khác (extends) |
Cài đặt interface (như bài 7 với sealed) | Thêm trường instance ngoài danh sách thành phần |
| Kiểm tra dữ liệu trong constructor rút gọn | Sửa trường sau khi tạo (luôn bất biến) |
Dùng record cho: kết quả trả về nhiều giá trị, DTO nhận/gửi API, khoá của HashMap, sự kiện, toạ độ, tiền tệ… Dùng lớp thường khi đối tượng có trạng thái thay đổi theo thời gian và hành vi phức tạp (như TaiKhoan ở bài 6).
Lưu ý: record bất biến nông:
record Lop(String ten, List<String> hocVien)vẫn cho sửa nội dung danh sách bên trong. Sao chép phòng thủ trong constructor:hocVien = List.copyOf(hocVien);.
enum: tập giá trị cố định
Trạng thái đơn hàng, ngày trong tuần, mức độ ưu tiên — những thứ có số giá trị cố định, biết trước. Dùng int (0, 1, 2) hay String ("DANG_GIAO") đều dễ gõ sai và không được trình biên dịch kiểm tra. enum giải quyết:
enum TrangThai { CHO_XAC_NHAN, DANG_GIAO, DA_GIAO, DA_HUY }
void main() {
TrangThai t = TrangThai.DANG_GIAO;
IO.println(t); // DANG_GIAO
IO.println(t.name() + " " + t.ordinal()); // DANG_GIAO 1 — thứ tự khai báo (đừng lưu ordinal vào DB)
IO.println(t == TrangThai.DANG_GIAO); // so sánh enum bằng == là đúng và an toàn
IO.println(TrangThai.valueOf("DA_GIAO")); // từ chuỗi → enum (sai tên → IllegalArgumentException)
for (TrangThai tt : TrangThai.values()) IO.print(tt + " ");
IO.println("");
String moTa = switch (t) { // switch trên enum: không cần default nếu đủ mọi giá trị
case CHO_XAC_NHAN -> "Đang chờ cửa hàng xác nhận";
case DANG_GIAO -> "Shipper đang giao";
case DA_GIAO -> "Giao thành công";
case DA_HUY -> "Đơn đã huỷ";
};
IO.println(moTa);
}enum có thuộc tính và phương thức
Mỗi hằng enum là một đối tượng; enum có thể có constructor, thuộc tính, phương thức:
enum HangThanhVien {
DONG("Đồng", 0, 0),
BAC("Bạc", 5_000_000, 3),
VANG("Vàng", 20_000_000, 5),
KIM_CUONG("Kim cương", 50_000_000, 10);
private final String tenHienThi;
private final long chiTieuToiThieu;
private final int phanTramGiam;
HangThanhVien(String tenHienThi, long chiTieuToiThieu, int phanTramGiam) { // constructor luôn private
this.tenHienThi = tenHienThi;
this.chiTieuToiThieu = chiTieuToiThieu;
this.phanTramGiam = phanTramGiam;
}
long giaSauGiam(long gia) {
return gia * (100 - phanTramGiam) / 100;
}
static HangThanhVien theoChiTieu(long tongChiTieu) {
HangThanhVien ketQua = DONG;
for (HangThanhVien h : values()) {
if (tongChiTieu >= h.chiTieuToiThieu) ketQua = h; // values() theo thứ tự khai báo tăng dần
}
return ketQua;
}
String tenHienThi() { return tenHienThi; }
}
void main() {
for (long chiTieu : new long[]{1_000_000, 8_000_000, 70_000_000}) {
var hang = HangThanhVien.theoChiTieu(chiTieu);
IO.println("Chi %,d → hạng %s, áo 500.000đ còn %,d".formatted(chiTieu, hang.tenHienThi(), hang.giaSauGiam(500_000)));
}
}Quy tắc nghiệp vụ (ngưỡng hạng, phần trăm giảm) nằm gọn trong enum; thêm hạng mới chỉ cần thêm một dòng. EnumMap và EnumSet (trong java.util) là các cấu trúc dữ liệu tối ưu riêng cho khoá enum.
Bất biến (immutability)
Record, enum, String, Integer, LocalDate đều bất biến. Đây không phải tình cờ:
- An toàn khi chia sẻ: truyền cho phương thức khác, lưu vào tập hợp, dùng giữa nhiều luồng — không ai sửa được.
- Dễ suy luận: giá trị lúc tạo là giá trị mãi mãi; không có lỗi "ai đó sửa mất rồi".
- Khoá an toàn cho
HashMap/HashSet.
Cách "sửa" đối tượng bất biến là tạo bản mới (sp.giamGia(25), chuoi.toUpperCase(), ngay.plusDays(1)). Mặc định hãy làm lớp bất biến; chỉ cho phép thay đổi khi thật sự cần.
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
void main() {
LocalDate homNay = LocalDate.of(2026, 10, 28);
LocalDate hanNop = homNay.plusWeeks(2); // bản mới; homNay không đổi
IO.println(homNay + " → hạn nộp " + hanNop + " (" + hanNop.getDayOfWeek() + ")");
IO.println("Còn " + ChronoUnit.DAYS.between(homNay, LocalDate.of(2027, 1, 1)) + " ngày tới năm mới");
}(java.time — LocalDate, LocalDateTime, Duration — là thư viện ngày giờ chuẩn từ Java 8; đừng dùng java.util.Date cũ trong code mới.)
Bài tập
- Viết record
PhanSo(long tu, long mau)tự rút gọn và đưa dấu âm lên tử trong constructor rút gọn; kiểm tranew PhanSo(2, 4).equals(new PhanSo(1, 2))làtrue. - Viết record
Tien(long soDong)vớicong,tru(không cho âm),nhan(int),toStringdạng1.250.000 ₫. - Viết enum
PhuongTien(XE_MAY, O_TO, XE_TAI) có phí gửi xe theo giờ; viếtlong tinhPhi(PhuongTien p, int soGio). - Viết lớp thường
NhanVien(mã, tên, lương), ghi đèequals/hashCodetheo mã; thử thêm hai nhân viên cùng mã vàoHashSetvà giải thích kết quả.
Tổng kết
- Ghi đè
toStringđể in đối tượng có nghĩa. equalsso sánh nội dung theo hợp đồng; ghi đèequalsthì phải ghi đèhashCode, nếu khôngHashSet/HashMaphoạt động sai.- record: lớp dữ liệu bất biến, có sẵn constructor, accessor,
equals,hashCode,toString; constructor rút gọn để kiểm tra dữ liệu. - enum: tập giá trị cố định, an toàn kiểu, so sánh bằng
==, có thuộc tính/phương thức, dùng tốt với switch. - Ưu tiên đối tượng bất biến; "sửa" bằng cách tạo bản mới.
- Bài tiếp theo: Collections — List, Set, Map — nơi
equalsvàhashCodephát huy tác dụng.
Nguồn tham khảo: dev.java — "Records", "Enums"; JEP 395: Records; tài liệu API Java 25 — java.lang.Object (hợp đồng equals/hashCode), java.time; Bloch — Effective Java, mục 10–12 (equals, hashCode, toString), 17 (bất biến), 34 (enum thay cho hằng int).
Bài này thế nào?
Bài viết có hữu ích với bạn?
Đánh giá giúp mình biết nên viết thêm về chủ đề nào.