- Dùng
extendsvàsuper; hiểu chuỗi constructor chạy từObjectxuống. - Phân biệt dứt điểm override (lúc chạy) và overload (lúc biên dịch).
- Hiểu đa hình và cơ chế dynamic dispatch đứng sau nó.
- Dùng
abstract classkhi có trạng thái và code dùng chung. - Dùng
interface— kể cảdefault,static,privatemethod. - Chọn đúng giữa
abstract classvàinterface. - Dùng
record(Java 16+) cho lớp chỉ mang dữ liệu. - Dùng
sealed(Java 17+) để khoá cây kế thừa và bật kiểm tra đủ nhánh. - Dùng
enumđầy đủ: trường, constructor, phương thức, override theo từng hằng số. - Biết bốn loại nested class và khi nào dùng loại nào.
- Hiểu vì sao "ưu tiên composition hơn inheritance" là lời khuyên quan trọng nhất chương.
1. Kế thừa với extends
// LỚP CHA (superclass / base class)
public class NhanVien {
protected final String hoTen; // protected → class con dùng được
protected long luongCoBan;
public NhanVien(String hoTen, long luongCoBan) {
this.hoTen = hoTen;
this.luongCoBan = luongCoBan;
}
public long tinhLuong() {
return luongCoBan;
}
@Override
public String toString() {
return "%s — %,dđ".formatted(hoTen, tinhLuong());
}
}
// LỚP CON (subclass) — kế thừa MỌI thành viên không private
public class QuanLy extends NhanVien {
private final int soNhanVienQuanLy;
public QuanLy(String hoTen, long luongCoBan, int soNhanVien) {
super(hoTen, luongCoBan); // ⚠️ PHẢI là câu lệnh đầu tiên
this.soNhanVienQuanLy = soNhanVien;
}
@Override
public long tinhLuong() {
return super.tinhLuong() + soNhanVienQuanLy * 500_000L;
// ↑ gọi bản của lớp cha rồi cộng thêm
}
}
public class LapTrinhVien extends NhanVien {
private final String ngonNgu;
public LapTrinhVien(String hoTen, long luong, String ngonNgu) {
super(hoTen, luong);
this.ngonNgu = ngonNgu;
}
@Override
public long tinhLuong() {
return super.tinhLuong() + (ngonNgu.equals("Java") ? 3_000_000L : 0);
}
}
| Class con kế thừa gì | Có | Ghi chú |
|---|---|---|
Trường public/protected | ✅ | Truy cập trực tiếp |
| Trường package-private | ✅ nếu cùng package | |
Trường private | ⚠️ Có trong bộ nhớ nhưng không truy cập được | Phải qua getter/setter của lớp cha |
Phương thức public/protected | ✅ | Có thể override |
| Constructor | ❌ Không | Phải gọi qua super(...) |
Thành viên static | ✅ (dùng chung) | Không override được, chỉ bị "che" |
Java chỉ cho kế thừa đơn
Một class chỉ extends được một class. Không có class C extends A, B
như C++. Lý do là vấn đề kim cương (diamond problem): nếu A và B cùng có phương thức
f(), thì C.f() lấy bản nào? C++ giải quyết bằng cú pháp phức tạp; Java chọn
cách né hẳn.
Bù lại, một class implements được nhiều interface. Vì interface truyền
thống chỉ có chữ ký (không có cài đặt), không có gì để xung đột. Java 8 thêm
default method làm chuyện phức tạp trở lại một chút — sẽ nói ở mục 7.
1.1. super và chuỗi constructor
class A {
A() { System.out.println("A()"); }
}
class B extends A {
B() {
// super(); ← javac TỰ THÊM nếu bạn không viết
System.out.println("B()");
}
}
class C extends B {
C() { System.out.println("C()"); }
}
new C();
// A() ← lớp cha chạy TRƯỚC, luôn luôn
// B()
// C()
super(...)class Cha {
Cha(int x) { } // chỉ có constructor CÓ tham số
}
class Con extends Cha {
Con() { }
// ❌ error: constructor Cha in class Cha cannot be applied to given types;
// required: int, found: no arguments
// → javac cố chèn super() nhưng Cha không có constructor rỗng
}
class ConDung extends Cha {
ConDung() { super(0); } // ✅ gọi tường minh
}
super(...) và this(...) đều phải là câu lệnh đầu tiên của
constructor, và không thể dùng cả hai trong cùng một constructor. Lý do: lớp cha phải được khởi tạo
xong trước khi lớp con chạm vào bất cứ thứ gì.
2. Override và overload — hai thứ hoàn toàn khác nhau
| Override (ghi đè) | Overload (nạp chồng) | |
|---|---|---|
| Ở đâu | Class con ghi đè class cha | Cùng một class (hoặc kế thừa) |
| Tên phương thức | Giống hệt | Giống hệt |
| Danh sách tham số | Giống hệt | Phải khác |
| Kiểu trả về | Giống, hoặc kiểu con (covariant) | Tuỳ ý |
| Mức truy cập | Không được hẹp hơn | Tuỳ ý |
| Quyết định lúc nào | Lúc chạy — theo kiểu thực tế | Lúc biên dịch — theo kiểu khai báo |
| Gọi là | Đa hình động (runtime polymorphism) | Đa hình tĩnh (compile-time) |
class Cha {
void chao() { System.out.println("Cha.chao()"); }
void chao(String t) { System.out.println("Cha.chao(String) — OVERLOAD"); }
}
class Con extends Cha {
@Override
void chao() { System.out.println("Con.chao() — OVERRIDE"); }
}
Cha obj = new Con(); // kiểu KHAI BÁO = Cha, kiểu THỰC TẾ = Con
obj.chao(); // "Con.chao()" ← OVERRIDE: theo kiểu THỰC TẾ
obj.chao("xin chào"); // "Cha.chao(String)" ← OVERLOAD: theo kiểu KHAI BÁO
class Cha {
protected Object lay() { return null; }
public void chay() throws IOException { }
public static void tinh() { System.out.println("Cha"); }
public final void khoa() { }
}
class Con extends Cha {
// ① Kiểu trả về được phép HẸP HƠN (covariant return)
@Override
public String lay() { return "ok"; } // ✅ String là con của Object
// ② Mức truy cập được phép RỘNG HƠN, không được hẹp hơn
// private Object lay() { } // ❌ error: attempting to assign weaker access
// ③ Ngoại lệ checked được phép HẸP HƠN hoặc BỎ HẲN
@Override
public void chay() { } // ✅ bỏ throws
// public void chay() throws Exception { } // ❌ rộng hơn IOException
// ④ static KHÔNG override được — chỉ "che" (hiding)
public static void tinh() { System.out.println("Con"); }
// @Override ở đây sẽ báo lỗi
// ⑤ final KHÔNG override được
// public void khoa() { } // ❌ error: cannot override final method
}
// Chứng minh ④: static chọn theo kiểu KHAI BÁO
Cha c = new Con();
c.tinh(); // "Cha" ⚠️ không phải "Con"! (nên gọi qua tên class: Cha.tinh())
3. Đa hình — sức mạnh thật sự của kế thừa
List<NhanVien> congTy = List.of(
new NhanVien("An", 10_000_000),
new QuanLy("Bình", 20_000_000, 5),
new LapTrinhVien("Chi", 15_000_000, "Java")
);
long tong = 0;
for (NhanVien nv : congTy) {
System.out.println(nv); // gọi toString() ĐÚNG của từng loại
tong += nv.tinhLuong(); // gọi tinhLuong() ĐÚNG của từng loại
}
System.out.printf("Tổng quỹ lương: %,dđ%n", tong);
// An — 10,000,000đ
// Bình — 22,500,000đ ← QuanLy.tinhLuong()
// Chi — 18,000,000đ ← LapTrinhVien.tinhLuong()
// Tổng quỹ lương: 50,500,000đ
Dynamic dispatch — cơ chế đứng sau đa hình
Vòng lặp trên chỉ biết mỗi phần tử là NhanVien. Vậy làm sao nó gọi đúng
QuanLy.tinhLuong()?
JVM lưu cùng mỗi object một con trỏ tới class thực tế của nó. Khi gặp lời gọi
nv.tinhLuong(), bytecode dùng lệnh invokevirtual: JVM tra bảng phương
thức ảo (vtable) của class thực tế để tìm bản cài đặt đúng. Đây gọi là
dynamic dispatch hay late binding, và nó xảy ra lúc chạy chứ không
phải lúc biên dịch.
Bạn kiểm chứng được bằng javap -c: lời gọi tinhLuong() biên dịch thành
invokevirtual, còn lời gọi phương thức static thành
invokestatic — cái sau được cố định ngay lúc biên dịch. Đây chính là lý do
static không override được.
3.1. Ép kiểu object và instanceof
// UPCASTING — lên lớp cha: TỰ ĐỘNG, luôn an toàn
NhanVien nv = new QuanLy("Bình", 20_000_000, 5);
// DOWNCASTING — xuống lớp con: PHẢI khai báo, có thể thất bại
QuanLy ql = (QuanLy) nv; // ✅ vì thực tế nó LÀ QuanLy
NhanVien nv2 = new LapTrinhVien("Chi", 15_000_000, "Java");
// QuanLy ql2 = (QuanLy) nv2;
// 💥 ClassCastException: LapTrinhVien cannot be cast to QuanLy
// ✅ Kiểm tra trước khi ép — cách cũ
if (nv2 instanceof QuanLy) {
QuanLy q = (QuanLy) nv2;
...
}
// ✅ Java 16+: pattern matching — gộp kiểm tra và ép kiểu
if (nv2 instanceof QuanLy q) {
System.out.println(q.getSoNhanVienQuanLy()); // q đã sẵn kiểu QuanLy
}
// Kết hợp điều kiện — q dùng được ngay trong cùng biểu thức
if (nv2 instanceof QuanLy q && q.getSoNhanVienQuanLy() > 3) { ... }
// Java 21: pattern matching trong switch
String loai = switch (nv2) {
case QuanLy q -> "Quản lý " + q.getSoNhanVienQuanLy() + " người";
case LapTrinhVien l -> "Lập trình " + l.getNgonNgu();
default -> "Nhân viên thường";
};
instanceof là dấu hiệu thiết kế chưa tốt
Nếu code của bạn có chuỗi dài if (x instanceof A) … else if (x instanceof B) …, hãy tự hỏi:
tại sao hành vi này không nằm trong chính các class đó? Đa hình sinh ra chính để thay thế mẫu
này. Trường hợp instanceof hợp lý: cài equals(), xử lý dữ liệu từ nguồn ngoài
không kiểm soát được, hoặc kết hợp với sealed để duyệt một tập kiểu đã biết trước và
đóng (mục 9).
4. abstract class
public abstract class HinhHoc {
private final String ten; // abstract class CÓ trạng thái
protected HinhHoc(String ten) { // CÓ constructor (cho lớp con gọi)
this.ten = ten;
}
// PHƯƠNG THỨC TRỪU TƯỢNG — không có thân, lớp con BẮT BUỘC cài đặt
public abstract double dienTich();
public abstract double chuVi();
// PHƯƠNG THỨC THƯỜNG — dùng chung, lớp con kế thừa nguyên vẹn
public String moTa() {
return "%s: diện tích %.2f, chu vi %.2f"
.formatted(ten, dienTich(), chuVi());
// ↑ gọi method trừu tượng — đa hình hoạt động
}
public String getTen() { return ten; }
}
public class HinhTron extends HinhHoc {
private final double banKinh;
public HinhTron(double banKinh) {
super("Hình tròn");
if (banKinh <= 0)
throw new IllegalArgumentException("Bán kính phải dương");
this.banKinh = banKinh;
}
@Override public double dienTich() { return Math.PI * banKinh * banKinh; }
@Override public double chuVi() { return 2 * Math.PI * banKinh; }
}
// ❌ Không tạo instance của abstract class được
// HinhHoc h = new HinhHoc("X");
// error: HinhHoc is abstract; cannot be instantiated
// ✅ Nhưng DÙNG được làm kiểu — đó chính là mục đích
List<HinhHoc> ds = List.of(new HinhTron(3), new HinhChuNhat(2, 4));
ds.forEach(h -> System.out.println(h.moTa()));
5. interface
public interface CoTheBay {
// ① HẰNG SỐ — ngầm định public static final
int DO_CAO_TOI_DA = 10_000;
// ② PHƯƠNG THỨC TRỪU TƯỢNG — ngầm định public abstract
void bay();
int doCaoToiDa();
// ③ DEFAULT METHOD (Java 8+) — có thân, lớp cài đặt kế thừa được
default String moTaBay() {
return "Bay ở độ cao tối đa " + doCaoToiDa() + "m";
}
// ④ STATIC METHOD (Java 8+) — tiện ích, gọi qua tên interface
static boolean hopLe(int doCao) {
return doCao > 0 && doCao <= DO_CAO_TOI_DA;
}
// ⑤ PRIVATE METHOD (Java 9+) — dùng chung cho các default method
private String dinhDang(int d) {
return "%,dm".formatted(d);
}
}
// Một class cài đặt NHIỀU interface
public class Chim implements CoTheBay, CoTheHot {
@Override public void bay() { System.out.println("Vỗ cánh"); }
@Override public int doCaoToiDa() { return 3000; }
@Override public void hot() { System.out.println("Chích chích"); }
}
// Vừa extends vừa implements — extends đứng trước
public class MayBay extends PhuongTien implements CoTheBay { ... }
default methodTrước Java 8, thêm một phương thức vào interface là thay đổi phá vỡ tương thích: mọi class đã cài đặt interface đó lập tức không biên dịch được nữa. Điều này khiến các interface trong thư viện chuẩn bị "đóng băng" — không thể tiến hoá.
Java 8 muốn thêm stream(), forEach(), removeIf() vào
Collection. Nếu làm theo cách cũ, toàn bộ code Java trên thế giới sẽ hỏng.
default method giải quyết: interface cung cấp sẵn một bản cài đặt, class cũ không cần sửa
gì. Đây là lý do duy nhất tính năng này tồn tại — nó không nhằm biến interface thành abstract
class.
default methodinterface A { default String ten() { return "A"; } }
interface B { default String ten() { return "B"; } }
class C implements A, B { }
// ❌ error: class C inherits unrelated defaults for ten() from types A and B
// ✅ Bắt buộc tự giải quyết
class C implements A, B {
@Override
public String ten() {
return A.super.ten(); // cú pháp đặc biệt: Interface.super.method()
}
}
Java xử lý xung đột bằng cách bắt lỗi lúc biên dịch và buộc lập trình viên chọn tường minh — an toàn hơn nhiều so với việc âm thầm chọn một bản theo quy tắc phức tạp như C++.
5.1. abstract class hay interface?
abstract class | interface | |
|---|---|---|
| Số lượng kế thừa được | Một | Nhiều |
| Trường (có trạng thái) | ✅ Bất kỳ | ❌ Chỉ public static final |
| Constructor | ✅ Có | ❌ Không |
| Phương thức có thân | ✅ Có | ✅ default / static / private |
| Mức truy cập thành viên | Bất kỳ | Chỉ public (và private cho helper) |
| Diễn đạt quan hệ | "LÀ MỘT" — cùng bản chất | "CÓ THỂ LÀM" — khả năng |
| Ví dụ | HinhHoc, AbstractList | Comparable, Runnable, AutoCloseable |
interface. Chỉ chuyển sang
abstract class khi bạn cần chia sẻ trạng thái (trường không phải hằng số) hoặc cần
constructor để bảo đảm tính hợp lệ. Interface linh hoạt hơn nhiều vì một class cài đặt được nhiều
interface, còn "một suất extends" thì rất quý.
6. record — lớp dữ liệu bất biến (Java 16+)
// ❌ CÁCH CŨ — 50+ dòng cho một lớp chỉ chứa 3 giá trị
public final class DiemCu {
private final int x, y;
public DiemCu(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) { /* 8 dòng */ }
@Override public int hashCode() { return Objects.hash(x, y); }
@Override public String toString() { /* 3 dòng */ }
}
// ✅ RECORD — MỘT dòng, tương đương hoàn toàn
public record Diem(int x, int y) { }
// Java tự sinh:
// - trường private final x, y
// - constructor Diem(int x, int y)
// - accessor x() và y() ⚠️ KHÔNG phải getX()/getY()
// - equals(), hashCode() dựa trên MỌI thành phần
// - toString(): "Diem[x=1, y=2]"
Diem d = new Diem(1, 2);
System.out.println(d.x()); // 1
System.out.println(d); // Diem[x=1, y=2]
System.out.println(d.equals(new Diem(1, 2))); // true ✅
public record SanPham(String ma, String ten, long gia) {
// ① COMPACT CONSTRUCTOR — kiểm tra hợp lệ, không cần viết lại gán
public SanPham {
Objects.requireNonNull(ma, "Mã không được null");
if (gia < 0)
throw new IllegalArgumentException("Giá không được âm: " + gia);
ten = ten.trim(); // chuẩn hoá được — gán tự động xảy ra SAU khối này
}
// ② Constructor phụ
public SanPham(String ma, String ten) {
this(ma, ten, 0);
}
// ③ Phương thức bổ sung
public long giaSauThue() { return Math.round(gia * 1.08); }
// ④ Override accessor nếu cần (hiếm)
@Override
public String ten() { return ten.toUpperCase(); }
// ⑤ Thành viên static — được phép
public static SanPham mienPhi(String ma, String ten) {
return new SanPham(ma, ten, 0);
}
}
// Record cài đặt được interface
public record Tien(long soTien) implements Comparable<Tien> {
@Override
public int compareTo(Tien khac) { return Long.compare(soTien, khac.soTien); }
}
| Record có thể | Record không thể |
|---|---|
| Cài đặt interface | extends class khác (đã ngầm kế thừa Record) |
| Có phương thức bổ sung | Có trường instance ngoài các thành phần khai báo |
Có trường và phương thức static | Bị kế thừa (record luôn ngầm final) |
| Có constructor phụ và compact constructor | Thay đổi giá trị sau khi tạo (luôn bất biến) |
Là generic: record Cap<A,B>(A a, B b) | Khai báo abstract |
Lồng trong class khác (ngầm static) | — |
record- DTO — object truyền dữ liệu giữa các tầng, ánh xạ JSON/API.
- Value object —
Tien,ToaDo,KhoangThoiGian. - Nhiều giá trị trả về — thay vì mảng
Object[]hoặc lớpPairtự chế. - Khoá của
Map—equals/hashCodeđúng chuẩn và bất biến, hoàn hảo cho vai trò này. - Kết hợp
sealed— mô hình hoá kiểu dữ liệu đại số (mục tiếp theo).
Không dùng khi object cần thay đổi trạng thái theo thời gian, hoặc khi cần kế thừa.
7. sealed — khoá cây kế thừa (Java 17+)
Bình thường, ai cũng có thể kế thừa class public của bạn. sealed cho phép bạn
liệt kê chính xác những class nào được phép — và đổi lại, trình biên dịch có thể kiểm tra
switch đã xử lý đủ mọi trường hợp hay chưa.
// Chỉ ba class này được phép kế thừa HinhHoc
public sealed interface HinhHoc
permits HinhTron, HinhChuNhat, TamGiac { }
public record HinhTron(double banKinh) implements HinhHoc { }
public record HinhChuNhat(double dai, double rong) implements HinhHoc { }
public record TamGiac(double a, double b, double c) implements HinhHoc { }
// ⭐ switch KHÔNG cần default — trình biên dịch biết đủ mọi trường hợp
static double dienTich(HinhHoc h) {
return switch (h) {
case HinhTron t -> Math.PI * t.banKinh() * t.banKinh();
case HinhChuNhat cn -> cn.dai() * cn.rong();
case TamGiac tg -> {
double p = (tg.a() + tg.b() + tg.c()) / 2;
yield Math.sqrt(p * (p-tg.a()) * (p-tg.b()) * (p-tg.c()));
}
};
}
// Thêm record HinhVuong implements HinhHoc → switch trên BÁO LỖI BIÊN DỊCH ✅
// RECORD PATTERN (Java 21) — bung thành phần ngay trong case
static double dienTichGon(HinhHoc h) {
return switch (h) {
case HinhTron(double r) -> Math.PI * r * r;
case HinhChuNhat(double d, double r) -> d * r;
case TamGiac(double a, double b, double c) -> {
double p = (a + b + c) / 2;
yield Math.sqrt(p * (p-a) * (p-b) * (p-c));
}
};
}
| Từ khoá cho class con | Nghĩa |
|---|---|
final | Dừng cây kế thừa ở đây — không ai kế thừa tiếp được |
sealed | Tiếp tục khoá — phải khai báo permits của riêng nó |
non-sealed | Mở lại — ai cũng kế thừa được từ class này trở xuống |
sealed + record = kiểu dữ liệu đại số
Kết hợp này mang một mô hình từ ngôn ngữ hàm (Haskell, Rust, Scala) vào Java: "một giá trị thuộc đúng một trong N dạng, mỗi dạng mang dữ liệu riêng". Nó rất hợp để biểu diễn kết quả có thể thành công hoặc thất bại, trạng thái của một tiến trình, hoặc cây cú pháp.
sealed interface KetQua<T> permits ThanhCong, ThatBai { }
record ThanhCong<T>(T giaTri) implements KetQua<T> { }
record ThatBai<T>(String loi) implements KetQua<T> { }
String hienThi = switch (kq) {
case ThanhCong<String>(String v) -> "OK: " + v;
case ThatBai<String>(String e) -> "Lỗi: " + e;
}; // đủ nhánh, không cần default
8. enum — nhiều hơn một danh sách hằng số
// ① Dạng đơn giản nhất
public enum TrangThai { MOI, DANG_XU_LY, HOAN_THANH, HUY }
// ② Enum CÓ TRƯỜNG, CONSTRUCTOR, PHƯƠNG THỨC — đây mới là sức mạnh thật
public enum HanhTinh {
THUY_TINH(3.303e+23, 2.4397e6),
TRAI_DAT (5.976e+24, 6.37814e6),
SAO_HOA (6.421e+23, 3.3972e6); // ⚠️ dấu chấm phẩy khi có thêm thành viên
private final double khoiLuong; // kg
private final double banKinh; // m
HanhTinh(double khoiLuong, double banKinh) { // constructor ngầm private
this.khoiLuong = khoiLuong;
this.banKinh = banKinh;
}
public double trongLucBeMat() {
return 6.67300E-11 * khoiLuong / (banKinh * banKinh);
}
}
System.out.printf("%.2f m/s²%n", HanhTinh.TRAI_DAT.trongLucBeMat()); // 9.80
// ③ Mỗi hằng số OVERRIDE riêng — "constant-specific body"
public enum PhepToan {
CONG("+") { public double tinh(double a, double b) { return a + b; } },
TRU ("-") { public double tinh(double a, double b) { return a - b; } },
NHAN("×") { public double tinh(double a, double b) { return a * b; } },
CHIA("÷") {
public double tinh(double a, double b) {
if (b == 0) throw new ArithmeticException("Chia cho 0");
return a / b;
}
};
private final String kyHieu;
PhepToan(String kyHieu) { this.kyHieu = kyHieu; }
public abstract double tinh(double a, double b);
@Override public String toString() { return kyHieu; }
}
for (PhepToan p : PhepToan.values()) {
System.out.printf("6 %s 2 = %.1f%n", p, p.tinh(6, 2));
}
TrangThai t = TrangThai.DANG_XU_LY;
t.name(); // "DANG_XU_LY" — tên hằng số
t.ordinal(); // 1 — vị trí (⚠️ đừng lưu vào database!)
TrangThai.values(); // mảng mọi hằng số
TrangThai.valueOf("HUY"); // tra từ chuỗi (ném IllegalArgumentException nếu sai)
t.compareTo(TrangThai.HUY); // so theo ordinal
// ✅ enum so sánh bằng == được (khác mọi object khác!)
if (t == TrangThai.DANG_XU_LY) { ... } // an toàn: mỗi hằng số là singleton
// equals() cũng đúng, nhưng == ngắn hơn và an toàn với null
// Collection chuyên dụng cho enum — nhanh hơn HashMap/HashSet nhiều
EnumMap<TrangThai, Integer> dem = new EnumMap<>(TrangThai.class);
EnumSet<TrangThai> dangHoatDong =
EnumSet.of(TrangThai.MOI, TrangThai.DANG_XU_LY);
ordinal() vào database
ordinal() là vị trí trong danh sách khai báo. Chèn một hằng số mới vào giữa danh
sách sẽ làm dịch chuyển toàn bộ số phía sau — và mọi bản ghi cũ trong database đột nhiên mang ý nghĩa
khác. Đây là bug dữ liệu rất khó phát hiện và gần như không sửa được.
Hãy lưu name() (chuỗi), hoặc tốt hơn: định nghĩa một trường code tường minh
trong enum và lưu trường đó. Với JPA, dùng @Enumerated(EnumType.STRING) chứ không phải
ORDINAL.
Enum là singleton do JVM bảo đảm
Mỗi hằng số enum là một instance duy nhất, được JVM tạo lúc nạp class và không bao giờ có bản thứ hai —
kể cả khi bị serialize/deserialize hay tạo qua phản chiếu. Đây là lý do so sánh bằng == an
toàn (điều mà không kiểu tham chiếu nào khác có), và cũng là cách cài đặt singleton tốt nhất
trong Java:
public enum CauHinh {
INSTANCE;
private final Properties props = nap();
public String lay(String khoa) { return props.getProperty(khoa); }
}
CauHinh.INSTANCE.lay("db.url");
9. Bốn loại class lồng nhau
| Loại | Khai báo | Truy cập thành viên instance của lớp ngoài | Dùng khi |
|---|---|---|---|
| Static nested ⭐ | static class B | ❌ Không | Mặc định — class trợ giúp, Builder, Node |
| Inner (non-static) | class B | ✅ Có | Cần liên kết chặt với instance ngoài — vd Iterator |
| Local | Khai báo trong phương thức | ✅ Có (+ biến cục bộ effectively final) | Rất hiếm |
| Anonymous | new X() { … } | ✅ Có | Cài đặt một lần; nay thường thay bằng lambda |
public class DanhSachLienKet<T> {
private Node<T> dau;
private int kichThuoc;
// ① STATIC NESTED — không cần tham chiếu tới DanhSachLienKet
private static class Node<T> {
T giaTri;
Node<T> ke;
Node(T giaTri) { this.giaTri = giaTri; }
}
// ② INNER — cần đọc `dau` và `kichThuoc` của instance ngoài
public class MyIterator implements Iterator<T> {
private Node<T> hienTai = dau; // truy cập trực tiếp trường ngoài
@Override public boolean hasNext() { return hienTai != null; }
@Override public T next() {
T v = hienTai.giaTri; hienTai = hienTai.ke; return v;
}
}
// ③ ANONYMOUS — cài đặt tại chỗ, dùng một lần
public Comparator<T> soSanhNguoc(Comparator<T> goc) {
return new Comparator<T>() {
@Override
public int compare(T a, T b) { return goc.compare(b, a); }
};
// ✅ Java 8+: lambda gọn hơn hẳn
// return (a, b) -> goc.compare(b, a);
}
}
// Tạo instance:
DanhSachLienKet<String> ds = new DanhSachLienKet<>();
DanhSachLienKet<String>.MyIterator it = ds.new MyIterator(); // cú pháp lạ của inner class
Mỗi instance của inner class (non-static) giữ một tham chiếu ẩn tới instance của lớp ngoài. Nếu inner class sống lâu hơn lớp ngoài — ví dụ được đăng ký làm listener, hoặc đưa vào một collection tĩnh — nó sẽ giữ lớp ngoài sống mãi, dù lớp ngoài đã hết vai trò. Đây là nguyên nhân rò rỉ bộ nhớ kinh điển trong Android (Activity bị giữ bởi một AsyncTask nội tại).
Quy tắc: luôn khai báo static cho nested class, trừ khi bạn thực
sự cần truy cập thành viên instance của lớp ngoài. Bản thân đội ngũ JDK cũng theo quy tắc này —
Map.Entry, HashMap.Node đều là static nested.
10. final và "ưu tiên composition hơn inheritance"
// final class — không ai kế thừa được
public final class Tien { ... }
// String, Integer, LocalDate đều là final — vì tính bất biến của chúng
// sẽ bị phá nếu ai đó kế thừa và override method
// final method — lớp con không override được
public class DichVu {
public final void chay() { // khung xử lý cố định (template method)
chuanBi();
xuLy(); // ← chỉ bước này cho lớp con tuỳ biến
donDep();
}
protected abstract void xuLy();
private void chuanBi() { }
private void donDep() { }
}
// ❌ Ví dụ kinh điển từ sách "Effective Java"
public class DemPhanTu<E> extends ArrayList<E> {
private int soLanThem = 0;
@Override
public boolean add(E e) {
soLanThem++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
soLanThem += c.size();
return super.addAll(c);
}
}
DemPhanTu<String> d = new DemPhanTu<>();
d.addAll(List.of("a", "b", "c"));
System.out.println(d.soLanThem); // 6 ⚠️ đáng lẽ là 3!
// Nguyên nhân: ArrayList.addAll() bên trong GỌI add() cho từng phần tử.
// Mà add() đã bị override → đếm hai lần.
// Bạn không thể biết điều này nếu không đọc mã nguồn của ArrayList —
// và nó có thể thay đổi ở phiên bản Java sau!
// ✅ COMPOSITION — bọc thay vì kế thừa
public class DemPhanTu<E> {
private final List<E> benTrong = new ArrayList<>();
private int soLanThem = 0;
public boolean them(E e) {
soLanThem++;
return benTrong.add(e);
}
public boolean themTatCa(Collection<? extends E> c) {
soLanThem += c.size();
return benTrong.addAll(c);
}
public int getSoLanThem() { return soLanThem; }
}
// Chỉ mở ra API bạn kiểm soát; không phụ thuộc chi tiết cài đặt của ArrayList
11. Cheat sheet chương 5
| Tình huống | Dùng |
|---|---|
| Lớp chỉ chứa dữ liệu, bất biến | record |
| Khai báo khả năng, không có trạng thái | interface |
| Chia sẻ trạng thái + code giữa các lớp con | abstract class |
| Tập hằng số cố định | enum |
| Khoá tập lớp con + kiểm tra đủ nhánh | sealed + record |
| Class trợ giúp bên trong | static nested class |
| Ngăn kế thừa | final class |
| Ngăn override một method | final method |
| Thêm hành vi cho lớp có sẵn | Composition, không phải kế thừa |
| Gọi bản của lớp cha | super.method() |
| Gọi default method của interface | Interface.super.method() |
| Kiểm tra + ép kiểu | if (o instanceof X x) (Java 16+) |
| Phân nhánh theo kiểu | switch (o) { case X x -> … } (Java 21) |
| Bung thành phần của record | case Diem(int x, int y) -> … (Java 21) |
| Singleton | enum một hằng số |
| Lưu enum vào database | name() hoặc mã riêng — không ordinal() |
Bài tập
Hệ thống hình học với sealed + record
Thiết kế lại hệ thống hình học theo phong cách Java hiện đại:
sealed interface Hinh permits Tron, ChuNhat, TamGiac, DaGiac { }
record Tron(double r) implements Hinh { }
record ChuNhat(double dai, double rong) implements Hinh { }
record TamGiac(double a, double b, double c) implements Hinh { }
record DaGiac(List<Diem> dinh) implements Hinh { }
- Compact constructor kiểm tra: bán kính/cạnh phải dương; ba cạnh tam giác phải thoả bất đẳng thức tam giác; đa giác phải có ≥3 đỉnh.
- Hàm
static double dienTich(Hinh h)dùngswitchvới record pattern, không códefault. - Tương tự cho
chuVi(Hinh)vàString moTa(Hinh). DaGiacdùng công thức Shoelace để tính diện tích.- Thêm
record HinhVuongvàopermitsmà chưa sửa các hàm — quan sát lỗi biên dịch, ghi lại thông điệp.
Gợi ý
Công thức Shoelace: A = |Σ(xᵢ·yᵢ₊₁ − xᵢ₊₁·yᵢ)| / 2, với chỉ số cuối nối về đầu.
Compact constructor của DaGiac nên tạo bản sao bất biến:
dinh = List.copyOf(dinh); — nếu không, người gọi vẫn sửa được list sau khi tạo
record, phá vỡ tính bất biến (đây là điều record không tự bảo đảm cho
thành phần kiểu tham chiếu).
Enum máy trạng thái đơn hàng
Viết enum TrangThaiDon mô hình hoá vòng đời đơn hàng, với quy tắc chuyển trạng thái nằm ngay trong enum:
MOI → DA_XAC_NHAN → DANG_GIAO → DA_GIAO
↓ ↓ ↓
DA_HUY DA_HUY DA_HUY
DA_GIAO → HOAN_TRA
- Mỗi hằng số có:
String moTa,boolean laTrangThaiCuoi. Set<TrangThaiDon> chuyenDuocSang()— dùngEnumSet.boolean coTheChuyenSang(TrangThaiDon dich).void kiemTraChuyen(TrangThaiDon dich)— némIllegalStateExceptionvới thông điệp rõ ràng nếu không hợp lệ.- Class
DonHangdùng enum này, lưu lịch sử chuyển trạng thái kèm thời gian.
Gợi ý
Vấn đề tham chiếu vòng: hằng số enum không thể tham chiếu hằng số khai báo sau nó trong
constructor. Hai cách giải: (a) khai báo chuyenDuocSang() là phương thức trừu tượng
rồi override ở từng hằng số — lúc gọi thì mọi hằng số đã tồn tại; (b) dùng khối
static { } để điền một EnumMap sau khi tất cả hằng số đã khởi tạo.
Cách (a) sạch hơn.
Interface với default method
Thiết kế hệ thống thông báo đa kênh:
interface KenhThongBao {
void gui(String nguoiNhan, String noiDung); // trừu tượng
String tenKenh();
default boolean guiAnToan(String nn, String nd) { ... } // bắt lỗi, trả false
default void guiHangLoat(List<String> ds, String nd) { ... }
default KenhThongBao hoacDung(KenhThongBao duPhong) { ... } // fallback
static KenhThongBao gopChung(KenhThongBao... cac) { ... } // gửi qua MỌI kênh
}
- Cài đặt ba kênh:
Email,Sms,GhiLog. hoacDungtrả về một kênh mới: thử kênh chính, thất bại thì dùng kênh dự phòng.- Thêm interface thứ hai
CoTheLenLichcũng códefault void gui(...). Cho một class cài đặt cả hai và giải quyết xung đột bằngKenhThongBao.super.gui(...).
Gợi ý
hoacDung và gopChung trả về interface — dễ nhất là dùng lambda hoặc
anonymous class tại chỗ. Đây chính là mẫu decorator, và bạn sẽ thấy nó lặp lại ở
Stream (chương 10). Lưu ý: KenhThongBao có hai
phương thức trừu tượng nên không phải functional interface — không viết được bằng
lambda trần, phải dùng anonymous class hoặc tách nhỏ interface.
Tái hiện bug kế thừa và sửa bằng composition
Gõ lại đúng ví dụ DemPhanTu extends ArrayList trong bài học và:
- Chạy và xác nhận
addAllđếm sai gấp đôi. - Dùng
⌘Btrong IntelliJ để đọc mã nguồnArrayList.addAll()— chỉ ra chính xác dòng nào gây ra vấn đề. - Sửa bằng cách bỏ override
addAll. Hỏi: cách này có bền vững không, nếu bản Java sau đổi cách cài đặt? - Sửa lại bằng composition.
- Nâng cấp: viết
DemPhanTu<E> implements List<E>uỷ quyền mọi phương thức cho list bên trong (dùng tính năng sinh code uỷ quyền của IDE). So sánh ba cách về số dòng, tính an toàn, và mức độ tương thích API.
Gợi ý
Câu 3 là mấu chốt: sửa như vậy chỉ đúng với bản cài đặt hiện tại. Nếu Java 26 đổi
addAll để không gọi add nữa, code lại sai theo hướng ngược lại. Đây là
định nghĩa của lớp cơ sở dễ vỡ (fragile base class problem) — bạn phụ thuộc vào thứ
không nằm trong hợp đồng công khai.
Câu 5 tương ứng với lớp ForwardingList trong sách "Effective Java", mục 18.
Bộ nhớ đệm LRU với Comparable và nested class
Cài đặt LruCache<K, V> dùng danh sách liên kết đôi + HashMap:
public class LruCache<K, V> implements Iterable<Map.Entry<K, V>> {
public LruCache(int sucChua);
public V get(K khoa); // đưa lên đầu; null nếu không có
public void put(K khoa, V giaTri); // đầy thì loại phần tử ít dùng nhất
public int size();
public Iterator<Map.Entry<K, V>> iterator(); // duyệt từ mới nhất
}
Nodephải làprivate staticnested class — giải thích vì saostatic.Iteratorlà inner class (non-static) — giải thích vì sao lần này lại cần non-static.- Cả
getvàputphải là O(1). - Thêm kiểm tra sửa đổi đồng thời: sửa cache khi đang duyệt phải ném
ConcurrentModificationException.
Gợi ý
Node là static vì nó chỉ chứa dữ liệu, không cần biết cache nào sở hữu
nó — để non-static sẽ lãng phí một tham chiếu ẩn cho mỗi phần tử.
Iterator cần non-static vì nó phải đọc dau và biến đếm sửa đổi của
instance cache cụ thể.
Kiểm tra sửa đổi đồng thời: giữ biến modCount, tăng mỗi lần cấu trúc thay đổi;
iterator lưu ảnh chụp lúc tạo và so sánh ở mỗi next(). Đây đúng là cách
ArrayList làm — bạn sẽ gặp lại ở Chương 7.
Ghi chú: LinkedHashMap với accessOrder=true làm sẵn việc này, nhưng
tự cài đặt là bài tập tốt nhất để hiểu chương 7-8.
Quiz
Override và overload khác nhau thế nào? Cái nào quyết định lúc chạy?
Xem đáp án
Override (ghi đè): class con định nghĩa lại một phương thức của class cha với
chữ ký giống hệt. Được quyết định lúc chạy theo kiểu thực tế của
object — đây chính là đa hình động, cài đặt bằng lệnh invokevirtual và bảng phương thức
ảo.
Overload (nạp chồng): nhiều phương thức cùng tên nhưng khác danh sách tham số, trong cùng một class. Được quyết định lúc biên dịch theo kiểu khai báo.
Cha o = new Con();
o.chao(); // Con.chao() ← override, theo kiểu THỰC TẾ
o.chao("hi"); // Cha.chao(String) ← overload, theo kiểu KHAI BÁO
Hệ quả: phương thức static không override được (chúng dùng
invokestatic, cố định lúc biên dịch) — chỉ bị "che" (hiding).
Vì sao Java không cho kế thừa nhiều class nhưng cho cài đặt nhiều interface?
Xem đáp án
Vì vấn đề kim cương. Nếu class C extends A, B mà cả A và B đều có
phương thức f() với thân khác nhau — hoặc cùng có trường x — thì
C lấy bản nào? C++ giải quyết bằng cú pháp phức tạp (virtual inheritance); Java chọn
cách né hẳn để giữ ngôn ngữ đơn giản và dự đoán được.
Interface truyền thống chỉ có chữ ký, không có thân và không có trạng thái — nên cài đặt nhiều interface không tạo xung đột nào: class cụ thể tự cung cấp bản cài đặt duy nhất.
Java 8 thêm default method làm vấn đề quay lại một phần. Java xử lý bằng cách
báo lỗi biên dịch khi có xung đột và buộc lập trình viên chọn tường minh bằng
A.super.f() — an toàn hơn hẳn việc âm thầm chọn theo quy tắc phức tạp. Và vì interface
vẫn không có trạng thái, xung đột trường không bao giờ xảy ra.
Khi nào dùng abstract class, khi nào dùng interface?
Xem đáp án
Interface — mặc định. Chọn nó khi:
- Bạn khai báo một khả năng ("CÓ THỂ LÀM"):
Comparable,Runnable,AutoCloseable. - Class cài đặt có thể thuộc nhiều cây kế thừa khác nhau.
- Không cần chia sẻ trạng thái.
Abstract class — chỉ khi:
- Cần trường không phải hằng số dùng chung (interface chỉ có
public static final). - Cần constructor để bảo đảm tính hợp lệ.
- Quan hệ thật sự là "LÀ MỘT" cùng bản chất:
HinhTronLÀ MỘTHinhHoc.
Lý do ưu tiên interface: một class chỉ có một suất extends — rất quý.
Dùng nó cho abstract class là bạn đã khoá class đó khỏi mọi cây kế thừa khác. Cách kết hợp phổ biến
nhất trong thư viện chuẩn: định nghĩa interface List + cung cấp
abstract class AbstractList làm bộ khung tiện lợi cho ai muốn dùng.
record tự sinh những gì? Nó không làm được gì?
Xem đáp án
Tự sinh:
- Trường
private finalcho mọi thành phần. - Constructor chính (canonical) nhận đủ thành phần.
- Accessor
x(),y()— không phảigetX(). equals()vàhashCode()dựa trên mọi thành phần.toString()dạngDiem[x=1, y=2].
Không làm được:
extendsclass khác — record đã ngầm kế thừajava.lang.Record.- Bị kế thừa — record luôn ngầm
final. - Thêm trường instance ngoài các thành phần khai báo (trường
staticthì được). - Thay đổi giá trị sau khi tạo.
Cảnh báo quan trọng: record chỉ bất biến nông. Nếu một thành phần là
List, người gọi vẫn sửa được list đó. Muốn bất biến thật, hãy sao chép trong compact
constructor: dinh = List.copyOf(dinh);.
sealed giải quyết vấn đề gì?
Xem đáp án
Nó cho phép tác giả liệt kê chính xác những class được phép kế thừa
(permits), tạo ra một cây kế thừa đóng nhưng vẫn công khai.
Hai lợi ích:
- Kiểm soát API — trước
sealed, cách duy nhất để ngăn kế thừa làfinal(cấm hẳn) hoặc để constructor package-private (cấm cả bên ngoài dùng).sealedcho phép "chỉ ba class này, nhưng ai cũng dùng được". - Kiểm tra đủ nhánh — trình biên dịch biết trọn vẹn tập kiểu con, nên
switchpattern matching không cầndefault. Thêm một class con mới sẽ làm mọiswitchliên quan báo lỗi biên dịch ngay.
Kết hợp với record, nó mang kiểu dữ liệu đại số từ ngôn ngữ hàm vào
Java: một giá trị thuộc đúng một trong N dạng, mỗi dạng mang dữ liệu riêng. Rất hợp cho kết quả
thành công/thất bại, trạng thái tiến trình, hoặc cây cú pháp.
Class con phải khai báo một trong ba: final (dừng), sealed (khoá tiếp),
hoặc non-sealed (mở lại).
Vì sao không lưu enum.ordinal() vào database?
Xem đáp án
Vì ordinal() là vị trí trong danh sách khai báo, không phải một định danh ổn
định. Chèn một hằng số mới vào giữa danh sách sẽ dịch chuyển toàn bộ số phía sau:
// Phiên bản 1
enum TrangThai { MOI, DANG_XU_LY, HOAN_THANH }
// MOI=0, DANG_XU_LY=1, HOAN_THANH=2 → database lưu số 2 cho "hoàn thành"
// Phiên bản 2 — thêm CHO_DUYET vào giữa
enum TrangThai { MOI, CHO_DUYET, DANG_XU_LY, HOAN_THANH }
// HOAN_THANH giờ là 3. Mọi bản ghi cũ có số 2 nay đọc thành DANG_XU_LY 💥
Dữ liệu bị hỏng âm thầm — không có lỗi nào được ném ra, chỉ là ý nghĩa bị đổi. Rất khó phát hiện và gần như không sửa được nếu đã chạy một thời gian.
Giải pháp: lưu name() (chuỗi), hoặc tốt hơn — định nghĩa một trường
code tường minh trong enum và lưu trường đó (tách rời hoàn toàn khỏi thứ tự khai báo).
Với JPA: @Enumerated(EnumType.STRING), không bao giờ dùng ORDINAL.
ordinal() chỉ nên dùng nội bộ cho EnumMap/EnumSet.
"Ưu tiên composition hơn inheritance" nghĩa là gì? Cho ví dụ kế thừa gây bug.
Xem đáp án
Kế thừa tạo ra phụ thuộc vào chi tiết cài đặt của lớp cha — thứ không nằm trong hợp đồng công khai và có thể đổi bất cứ lúc nào. Composition (bọc một object vào bên trong) chỉ phụ thuộc vào API công khai, ổn định hơn nhiều.
Ví dụ kinh điển:
class Dem<E> extends ArrayList<E> {
int soLan = 0;
@Override public boolean add(E e) { soLan++; return super.add(e); }
@Override public boolean addAll(Collection<? extends E> c) {
soLan += c.size(); return super.addAll(c);
}
}
new Dem<>().addAll(List.of("a","b","c")).soLan; // 6, không phải 3!
Vì ArrayList.addAll() bên trong gọi add() cho từng phần tử — mà
add() đã bị override nên đếm hai lần. Bạn không thể biết điều này nếu không đọc mã
nguồn JDK, và nó có thể thay đổi ở phiên bản sau. Đây gọi là vấn đề lớp cơ sở dễ vỡ.
Khi nào kế thừa vẫn đúng: quan hệ thật sự là "LÀ MỘT", và lớp cha được
thiết kế có chủ đích cho việc kế thừa — có tài liệu nói rõ method nào gọi method nào (như
AbstractList làm).
Vì sao nested class nên khai báo static?
Xem đáp án
Vì inner class (non-static) giữ một tham chiếu ẩn tới instance của lớp ngoài. Điều đó gây ba vấn đề:
- Rò rỉ bộ nhớ — nếu inner class sống lâu hơn lớp ngoài (được đăng ký làm listener, đưa vào collection tĩnh, truyền sang luồng khác), nó giữ lớp ngoài sống mãi. Đây là nguyên nhân rò rỉ kinh điển trong Android: một Activity bị giữ bởi task nội tại đã bị bỏ quên.
- Tốn bộ nhớ — mỗi instance tốn thêm một tham chiếu. Với một node trong danh sách liên kết có hàng triệu phần tử, con số đó rất đáng kể.
- Khó serialize — serialize inner class kéo theo cả lớp ngoài.
Quy tắc: mặc định static. Chỉ bỏ static khi nested class
thực sự cần đọc thành viên instance của lớp ngoài — trường hợp điển hình là một
Iterator phải duyệt dữ liệu của instance cụ thể.
Chính JDK cũng theo quy tắc này: Map.Entry, HashMap.Node,
ArrayList.Itr — cái đầu là static, cái cuối là inner vì nó cần duyệt danh sách cụ thể.
Tổng kết
Sau chương 5, bạn đã nắm:
- Kế thừa:
extends,super()chạy trước, kế thừa đơn và lý do. - Override vs overload: lúc chạy theo kiểu thực tế vs lúc biên dịch theo kiểu khai báo; bốn quy tắc override.
- Đa hình và dynamic dispatch — nền tảng của nguyên tắc mở-đóng.
- Ép kiểu object: upcasting tự động, downcasting phải khai báo, pattern matching (Java 16/21).
abstract classkhi cần trạng thái và code chung;interfacecho khả năng, vớidefault/static/privatemethod.recordcho lớp dữ liệu bất biến — một dòng thay 50 dòng.sealedđể khoá cây kế thừa và bật kiểm tra đủ nhánh; kết hợp record thành kiểu dữ liệu đại số.enumđầy đủ: trường, constructor, override theo hằng số,EnumMap/EnumSet, singleton — và cấm dùngordinal()để lưu trữ.- Bốn loại nested class và quy tắc "mặc định
static". - Composition hơn inheritance — bài học thiết kế quan trọng nhất chương.
Kết nối
- Chương 6 (Ngoại lệ) — cây
Throwablelà ví dụ kế thừa lớn nhất trong JDK; override không được mở rộng ngoại lệ checked. - Chương 7-8 (Collections) —
List/Set/Maplà interface,ArrayList/HashMaplà cài đặt: minh hoạ hoàn hảo cho "lập trình theo interface". - Chương 9 (Generics) —
Comparable<T>, kế thừa với generic, wildcard. - Chương 10 (Lambda & Stream) — functional interface là interface có đúng một phương thức trừu tượng; lambda thay anonymous class.
- Chương 12 (Đa luồng) —
Runnable,Callable; bất biến (record) là chiến lược an toàn luồng tốt nhất. - Chương 13 (JDBC) — mẫu DAO dựa trên interface;
recordlàm lớp thực thể. - Trụ cột OOP (số 6) — bốn tính chất: đóng gói (chương 4), kế thừa, đa hình, trừu tượng hoá (chương này).
- Python Chương 8 — đối chiếu: Python cho kế thừa nhiều class và giải quyết bằng MRO C3;
@dataclass≈record;Protocol≈ interface nhưng theo cấu trúc chứ không cần khai báo.