Backend

99 Ngày Java — Ngày 16: Abstract class

SSite Admin
12 tháng 08, 2026 6 phút đọc 0 lượt xem
99 Ngày Java — Ngày 16: Abstract class

Ngày 15 khép lại với một câu hỏi thiết kế: làm sao buộc mọi lớp con phải override một method mà lớp cha không thể tự cài đặt? NhanVien xác định được rằng mọi nhân viên đều có lương, nhưng công thức tính lương cụ thể thì chỉ từng lớp con nắm rõ — để lớp cha trả về một giá trị mặc định vô nghĩa là một khiếm khuyết thiết kế tiềm ẩn. Java giải quyết bằng abstract class — lớp trừu tượng: lớp cha khai báo những gì bắt buộc phải có, và ủy quyền phần cài đặt cụ thể cho lớp con.

Sketchnote Ngày 16: abstract class trong Java — abstract method, khi nào dùng, template method, so sánh với interface

Abstract class & abstract method

public abstract class NhanVien {          // abstract: không thể khởi tạo trực tiếp
    protected String ten;
    protected double luongCoBan;

    public NhanVien(String ten, double luongCoBan) {  // vẫn có constructor —
        this.ten = ten;                                // lớp con gọi qua super(...)
        this.luongCoBan = luongCoBan;
    }

    public abstract double tinhLuong();   // abstract method: chỉ khai báo chữ ký,
                                          // không có thân — lớp con BẮT BUỘC override

    public String gioiThieu() {           // method thường: cài đặt sẵn, dùng chung
        return "Tôi là " + ten;
    }
}

NhanVien nv = new NhanVien("An", 10_000_000);
// ✗ lỗi biên dịch: NhanVien is abstract; cannot be instantiated

NhanVien nv = new LapTrinhVien("An", 20_000_000, "Java");   // ✓ mẫu quen thuộc từ Ngày 15
  • Abstract method chỉ có chữ ký, không có thân — compiler buộc lớp con override; lớp con nào chưa cài đặt đủ thì bản thân nó cũng phải khai báo abstract.

  • Abstract class vẫn có đầy đủ constructor, field và method thường — nó là lớp cha chưa hoàn chỉnh một cách có chủ đích, không phải một lớp rỗng.

  • Kết hợp với Ngày 15: dynamic dispatch giờ được compiler bảo chứng — mọi đối tượng thực tế đứng sau biến NhanVien chắc chắn có bản tinhLuong() riêng, không còn khả năng rơi vào cài đặt mặc định vô nghĩa của lớp cha.

Khi nào nên dùng abstract class

// Lớp cha "thường" — override chỉ là khuyến nghị:
public class NhanVien {
    public double tinhLuong() { return 0; }   // giá trị giả — lớp con quên override
}                                              // vẫn biên dịch, sai chỉ lộ ra lúc runtime

// Abstract class — quy ước trở thành ràng buộc biên dịch:
public abstract class NhanVien {
    public abstract double tinhLuong();        // quên override → lỗi biên dịch ngay
}

// Nguyên tắc chung: phát hiện lỗi càng sớm càng tốt —
// lỗi compile-time (lúc build) luôn rẻ hơn lỗi runtime (lúc hệ thống đang chạy)
  • Dùng khi các lớp con cùng một gốc "là một" (Ngày 14) và chia sẻ code lẫn trạng thái chung — constructor, field, các method đã cài đặt — nhưng tồn tại một vài bước mà mỗi lớp bắt buộc phải tự định nghĩa.

  • So với lớp cha thường: lớp cha thường có thể bị khởi tạo một cách vô nghĩa, và override chỉ là quy ước; abstract class chuyển quy ước đó thành ràng buộc do compiler thực thi.

  • JDK áp dụng đúng mô hình này: InputStream, AbstractList, HttpServlet — đều là abstract class đảm nhận phần xử lý chung và để lại một số method cho lớp con cài đặt.

Template method — lớp cha giữ trình tự, lớp con điền chi tiết

public abstract class BaoCao {
    // TEMPLATE METHOD: khung xử lý cố định — final để lớp con không thay đổi trình tự
    public final String xuatBaoCao() {
        return taoTieuDe() + "\n" + taoNoiDung() + "\n" + taoChanTrang();
    }

    protected String taoTieuDe()   { return "=== BÁO CÁO ==="; }  // có cài đặt mặc định,
                                                                  // được phép override
    protected abstract String taoNoiDung();    // bước bắt buộc — mỗi loại báo cáo tự cài đặt
    protected String taoChanTrang() { return "-- Mọt Dev --"; }
}

public class BaoCaoDoanhThu extends BaoCao {
    @Override
    protected String taoNoiDung() { return "Doanh thu tháng này: 120 triệu"; }
}

new BaoCaoDoanhThu().xuatBaoCao();   // khung của lớp cha + nội dung của lớp con
// Lớp cha kiểm soát TRÌNH TỰ, lớp con cung cấp CHI TIẾT — tài liệu gọi đây là
// Hollywood Principle: "don't call us, we'll call you"
  • Đây là design pattern đầu tiên của series — về bản chất chỉ là abstract class được vận dụng có kỷ luật: một method final định nghĩa khung xử lý, các bước abstract dành cho lớp con.

  • final trên xuatBaoCao() là quyết định có chủ đích: lớp con được thay thế từng bước, không được thay đổi trình tự — cấu trúc báo cáo vì vậy luôn thống nhất trên toàn hệ thống.

  • Bạn đã sử dụng mẫu này ở series Spring mà chưa gọi tên: vòng đời bean (Ngày 07) hay JdbcTemplate — framework giữ khung xử lý, code của bạn cài đặt các bước cụ thể.

Bản vẽ kiến trúc — template method là bản thiết kế khung: trình tự cố định, chi tiết do từng lớp con hoàn thiện

Abstract class và interface — định vị trước Ngày 17

// Tiêu chí                Abstract class              Interface (Ngày 17)
// ----------------------------------------------------------------------------
// Quan hệ mô hình hóa     "là một" (is-a)             "có khả năng" (capability)
// Field trạng thái        ✓ có, kèm constructor       ✗ chỉ có hằng số
// Code cài đặt sẵn        ✓ method thường             ✓ default method (có giới hạn)
// Giới hạn kế thừa        extends 1 lớp cha duy nhất  implements nhiều interface
//
// Quy tắc chọn nhanh:
//   Cần CHIA SẺ code + trạng thái cho một họ lớp cùng gốc  → abstract class
//   Chỉ cần MÔ TẢ hợp đồng cho các lớp không cùng gốc      → interface
  • Abstract class mô hình hóa quan hệ "là một" và mang được field, constructor, code dùng chung — nhưng mỗi lớp chỉ extends được một lớp cha duy nhất.

  • Interface mô tả năng lực (hợp đồng thuần túy) — một lớp implements được nhiều interface cùng lúc. Chi tiết về default method, functional interface sẽ được trình bày ở Ngày 17.

Bài tập nhỏ

  • Chuyển NhanVien của Ngày 14–15 thành abstract class với tinhLuong() abstract — xác nhận new NhanVien(...) không còn biên dịch được, trong khi vòng lặp tính quỹ lương của Ngày 15 vẫn chạy nguyên vẹn.

  • Thêm lớp ThucTapSinh nhưng không override tinhLuong() — đọc và ghi nhớ thông báo lỗi biên dịch.

  • Cài đặt BaoCao theo mẫu template method và thêm BaoCaoNhanSu — sau đó thử bỏ final khỏi xuatBaoCao() và override nó ở lớp con để thấy rủi ro khi trình tự bị thay đổi.

  • Đọc tài liệu InputStream trong JDK: method nào abstract, method nào là khung dùng chung? Đối chiếu với nội dung hôm nay.

Kết luận

Abstract class hoàn thiện bức tranh kế thừa: lớp cha vừa chia sẻ code chung vừa ép buộc lớp con cài đặt phần việc riêng, và template method nâng điều đó thành khung xử lý cố định — trình tự thuộc về lớp cha, chi tiết thuộc về lớp con. Tuy nhiên abstract class vẫn chịu một giới hạn cốt lõi: mỗi lớp chỉ có một lớp cha. Khi một lớp cần cam kết nhiều hợp đồng cùng lúc, ta cần công cụ khác — Ngày 17: interface, thành phần linh hoạt nhất của hệ thống kiểu trong Java. Hẹn gặp lại!

S

Site Admin

Engineer and writer. Building things with TypeScript and distributed systems.

Bình luận (0)

Bạn cần đăng nhập bằng Google để bình luận.

Hãy là người bình luận đầu tiên.

Bài viết liên quan

99 Ngày Spring — Ngày 16: Xử lý lỗi toàn cục

Ngày 16 của 99 Ngày Spring: tập trung xử lý lỗi về một điểm với @RestControllerAdvice + @ExceptionHandler, chuẩn hóa body lỗi theo ProblemDetail (RFC 7807), kiểm soát response cho lỗi validation của @Valid, và handler dự phòng cho 500 — log chi tiết ở server, trả thông điệp chung cho client.

12 thg 8, 20266 phút0
99 Ngày Spring — Ngày 15: ResponseEntity & status code

Ngày 15 của 99 Ngày Spring: bảng status code tối thiểu (200/201/204/400/404/409/500) với ranh giới 4xx vs 5xx, ResponseEntity điều khiển status–header–body, nghi thức 201 Created + Location cho POST, 204 cho DELETE, và nguyên tắc "lỗi thì ném, đừng tự build".

11 thg 8, 20265 phút18
99 Ngày Java — Ngày 15: Đa hình (Polymorphism)

Ngày 15 của 99 Ngày Java: upcasting và dynamic dispatch — kiểu biến vs kiểu đối tượng thật, một vòng lặp tính lương cho mọi loại nhân viên, downcasting với instanceof & pattern matching, lập trình theo hợp đồng (nền móng của DI), và toString — đa hình bạn dùng mỗi ngày.

11 thg 8, 20266 phút15