Chương 05 · OOP nâng cao

Kế thừa, đa hình và interface

Ba tính chất còn lại của lập trình hướng đối tượng nằm gọn trong chương này: kế thừa, đa hình, trừu tượng hoá. Nhưng thông điệp quan trọng nhất không phải "hãy kế thừa nhiều vào" — mà ngược lại: kế thừa là công cụ mạnh và dễ dùng sai. Chương này dạy cả cách dùng lẫn cách không dùng, cùng những công cụ hiện đại (record, sealed) khiến nhiều trường hợp kế thừa truyền thống trở nên không cần thiết.

Độ dài: ~1700 dòng Bài tập: 5 Quiz: 8 Prerequisites: Chương 1-4
🎯 Mục tiêu chương
  • Dùng extends và super; hiểu chuỗi constructor chạy từ Object xuố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 class khi có trạng thái và code dùng chung.
  • Dùng interface — kể cả default, static, private method.
  • Chọn đúng giữa abstract class và 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

KeThua.java
// 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 đượcPhải qua getter/setter của lớp cha
Phương thức public/protected✅Có thể override
Constructor❌ KhôngPhả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

ChuoiConstructor.java
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()
⚠️ Lớp cha không có constructor rỗng → lớp con bắt buộc gọi super(...)
SuperBatBuoc.java
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)
Ở đâuClass con ghi đè class chaCùng một class (hoặc kế thừa)
Tên phương thứcGiống hệtGiống hệt
Danh sách tham sốGiống hệtPhải khác
Kiểu trả vềGiống, hoặc kiểu con (covariant)Tuỳ ý
Mức truy cậpKhông được hẹp hơnTuỳ ý
Quyết định lúc nàoLú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)
OverrideVsOverload.java
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
🕳️ Bốn quy tắc khi override
QuyTacOverride.java
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

DaHinh.java
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.

Giá trị thật của đa hình: code viết cho lớp cha chạy được với mọi lớp con — kể cả lớp con được viết sau đó nhiều năm. Thêm một loại nhân viên mới không cần sửa một dòng nào của vòng lặp tính lương. Đây là nguyên tắc mở-đóng: mở để mở rộng, đóng để sửa đổi.

3.1. Ép kiểu object và instanceof

EpKieuObject.java
// 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";
};
💡 Nhiều 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

Abstract.java
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

Interface.java
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 { ... }
📌 Vì sao Java 8 thêm default method

Trướ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.

🕳️ Vấn đề kim cương quay lại với default method
KimCuong.java
interface 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 classinterface
Số lượng kế thừa đượcMộtNhiề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ênBấ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, AbstractListComparable, Runnable, AutoCloseable
Quy tắc chọn: mặc định dùng 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+)

Record.java
// ❌ 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 ✅
RecordNangCao.java
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 interfaceextends class khác (đã ngầm kế thừa Record)
Có phương thức bổ sungCó trường instance ngoài các thành phần khai báo
Có trường và phương thức staticBị kế thừa (record luôn ngầm final)
Có constructor phụ và compact constructorThay đổ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)—
💡 Khi nào dùng 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ớp Pair tự 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.

Sealed.java
// 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 conNghĩa
finalDừng cây kế thừa ở đây — không ai kế thừa tiếp được
sealedTiếp tục khoá — phải khai báo permits của riêng nó
non-sealedMở 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.

KetQua.java
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ố

Enum.java
// ① 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));
}
EnumApi.java — phương thức có sẵn
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);
⚠️ Không bao giờ lưu 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:

Singleton.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ạiKhai báoTruy cập thành viên instance của lớp ngoàiDùng khi
Static nested ⭐static class B❌ KhôngMặ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
LocalKhai báo trong phương thức✅ Có (+ biến cục bộ effectively final)Rất hiếm
Anonymousnew X() { … }✅ CóCài đặt một lần; nay thường thay bằng lambda
NestedClass.java
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
⚠️ Inner class giữ tham chiếu ngầm — nguồn rò rỉ bộ nhớ

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.java
// 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()  { }
}
KeThuaSai.java — vì sao kế thừa dễ dùng sai
// ❌ 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
Ưu tiên composition hơn inheritance. Kế thừa tạo phụ thuộc vào chi tiết cài đặt của lớp cha — thứ có thể thay đổi bất cứ lúc nào. Chỉ kế thừa khi 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 rõ method nào gọi method nào). Còn lại, hãy bọc (composition) hoặc dùng interface.

11. Cheat sheet chương 5

Tình huốngDùng
Lớp chỉ chứa dữ liệu, bất biếnrecord
Khai báo khả năng, không có trạng tháiinterface
Chia sẻ trạng thái + code giữa các lớp conabstract class
Tập hằng số cố địnhenum
Khoá tập lớp con + kiểm tra đủ nhánhsealed + record
Class trợ giúp bên trongstatic nested class
Ngăn kế thừafinal class
Ngăn override một methodfinal method
Thêm hành vi cho lớp có sẵnComposition, không phải kế thừa
Gọi bản của lớp chasuper.method()
Gọi default method của interfaceInterface.super.method()
Kiểm tra + ép kiểuif (o instanceof X x) (Java 16+)
Phân nhánh theo kiểuswitch (o) { case X x -> … } (Java 21)
Bung thành phần của recordcase Diem(int x, int y) -> … (Java 21)
Singletonenum một hằng số
Lưu enum vào databasename() 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:

Khung
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ùng switch với record pattern, không có default.
  • Tương tự cho chuVi(Hinh) và String moTa(Hinh).
  • DaGiac dùng công thức Shoelace để tính diện tích.
  • Thêm record HinhVuong vào permits mà 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:

Luồng hợp lệ
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ùng EnumSet.
  • boolean coTheChuyenSang(TrangThaiDon dich).
  • void kiemTraChuyen(TrangThaiDon dich) — ném IllegalStateException với thông điệp rõ ràng nếu không hợp lệ.
  • Class DonHang dù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:

Khung
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.
  • hoacDung trả 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 CoTheLenLich cũng có default void gui(...). Cho một class cài đặt cả hai và giải quyết xung đột bằng KenhThongBao.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à:

  1. Chạy và xác nhận addAll đếm sai gấp đôi.
  2. Dùng ⌘B trong IntelliJ để đọc mã nguồn ArrayList.addAll() — chỉ ra chính xác dòng nào gây ra vấn đề.
  3. 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?
  4. Sửa lại bằng composition.
  5. 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:

API
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
}
  • Node phải là private static nested class — giải thích vì sao static.
  • Iterator là inner class (non-static) — giải thích vì sao lần này lại cần non-static.
  • Cả get và put phả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

Q1

Override và overload khác nhau thế nào? Cái nào quyết định lúc chạy?

Xem đáp án
✓ Đá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.

SoSanh.java
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).

Q2

Vì sao Java không cho kế thừa nhiều class nhưng cho cài đặt nhiều interface?

Xem đáp án
✓ Đá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.

Q3

Khi nào dùng abstract class, khi nào dùng interface?

Xem đáp án
✓ Đá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: HinhTron LÀ MỘT HinhHoc.

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.

Q4

record tự sinh những gì? Nó không làm được gì?

Xem đáp án
✓ Đáp án

Tự sinh:

  • Trường private final cho mọi thành phần.
  • Constructor chính (canonical) nhận đủ thành phần.
  • Accessor x(), y() — không phải getX().
  • equals() và hashCode() dựa trên mọi thành phần.
  • toString() dạng Diem[x=1, y=2].

Không làm được:

  • extends class khác — record đã ngầm kế thừa java.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 static thì đượ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);.

Q5

sealed giải quyết vấn đề gì?

Xem đáp án
✓ Đá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). sealed cho 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 switch pattern matching không cần default. Thêm một class con mới sẽ làm mọi switch liê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).

Q6

Vì sao không lưu enum.ordinal() vào database?

Xem đáp án
✓ Đá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:

Ordinal.java
// 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.

Q7

"Ưu tiên composition hơn inheritance" nghĩa là gì? Cho ví dụ kế thừa gây bug.

Xem đáp án
✓ Đá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:

Bug.java
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).

Q8

Vì sao nested class nên khai báo static?

Xem đáp án
✓ Đá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 class khi cần trạng thái và code chung; interface cho khả năng, với default/static/private method.
  • record cho 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ùng ordinal() để 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 Throwable là 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/Map là interface, ArrayList/HashMap là 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; record là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.