Java — Từ Zero đến Senior/Kế thừa & Đa hình/Abstract class — template buộc subclass hoàn thiện
3/7
~15 phútKế thừa & Đa hình

Abstract class — template buộc subclass hoàn thiện

Khi nào dùng abstract class, abstract method, template method pattern, so sánh với interface, và vì sao abstract class giữ được state nhưng không thể instantiate.

Bạn muốn viết class Shape chung cho Circle, Square, Triangle — cả 3 đều có method area(), perimeter(), nhưng công thức tính khác nhau. Class Shape không có công thức riêng, cũng không nên tồn tại như object cụ thể (ai tạo ra "shape" mà không phải hình gì?). Đây là lúc cần abstract class.

Abstract class là class không instantiate được và có thể chứa abstract method (method không body). Subclass bắt buộc implement các abstract method — compiler đảm bảo. Bài này giải thích khi nào chọn abstract class, pattern template method, và phân biệt với interface.

1. Analogy — bản thiết kế kiến trúc nhà

Bạn có bản thiết kế nhà mẫu — gồm cấu trúc tường, mái, điện, nước. Nhưng "vị trí cửa sổ" bản vẽ ghi "tuỳ chủ nhà". Bản thiết kế này không xây được thành ngôi nhà thật — phải có chủ nhà điền phần còn lại. Mỗi chủ xây cùng bản thiết kế ra nhà khác nhau ở phần "cửa sổ".

Đời thườngJava
Bản thiết kế mẫuabstract class House
Tường, mái đã vẽMethod concrete (có body)
"Vị trí cửa sổ: tuỳ chủ"abstract method (không body)
Ngôi nhà cụ thểSubclass concrete

💡 💡 Cách nhớ

Abstract class = bộ khung buộc subclass điền các lỗ hổng. Không tạo instance trực tiếp; chỉ tạo qua subclass concrete.

2. Cú pháp

public abstract class Shape {
    private final String name;

    protected Shape(String name) {
        this.name = name;
    }

    public String getName() { return name; }

    public abstract double area();          // abstract — khong body
    public abstract double perimeter();

    public String describe() {               // concrete — co body, dung abstract method
        return name + ": area=" + area() + ", perimeter=" + perimeter();
    }
}

Subclass phải implement abstract method:

public class Circle extends Shape {
    private final double radius;

    public Circle(double radius) {
        super("Circle");
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }

    @Override
    public double perimeter() {
        return 2 * Math.PI * radius;
    }
}

public class Square extends Shape {
    private final double side;

    public Square(double side) {
        super("Square");
        this.side = side;
    }

    @Override
    public double area() { return side * side; }

    @Override
    public double perimeter() { return side * 4; }
}

Dùng:

Shape s1 = new Circle(5);
Shape s2 = new Square(4);
System.out.println(s1.describe());   // Circle: area=78.53..., perimeter=31.41...
System.out.println(s2.describe());   // Square: area=16.0, perimeter=16.0

Shape s3 = new Shape("x");   // COMPILE ERROR — cannot instantiate abstract class

3. Quy tắc abstract class

3.1 Class đánh dấu abstract — không new được

Compiler bắt new Shape(...). Class vẫn có constructor — nhưng chỉ gọi được bằng super(...) từ subclass.

3.2 Chứa abstract method → class phải abstract

public class Shape {
    public abstract double area();   // COMPILE ERROR — class khong abstract
}

Fix: thêm abstract trước class.

3.3 Abstract class có thể không có abstract method nào

public abstract class BaseEntity {
    protected Long id;
    protected LocalDateTime createdAt;
    // khong co abstract method — nhung van abstract
}

Dùng khi: bạn muốn cấm instantiate (class "base" thuần tuý), nhưng không muốn buộc subclass override gì cụ thể. Pattern hiếm hơn nhưng hợp lệ.

3.4 Subclass không implement đủ → subclass phải abstract

abstract class Shape {
    public abstract double area();
    public abstract double perimeter();
}

abstract class Ellipse extends Shape {
    private double a, b;

    @Override
    public double area() { return Math.PI * a * b; }
    // khong implement perimeter -> Ellipse van abstract
}

class Circle extends Ellipse {
    private double r;
    @Override
    public double perimeter() { return 2 * Math.PI * r; }
    // lop nay da implement ca 2 -> concrete (co the new)
}

Subclass "truyền gánh nặng" cho cháu — hợp lệ nhưng thường là dấu hiệu nên nhóm lại.

4. Template Method Pattern

Abstract class với mix concrete method + abstract method là nền của template method pattern: cha định nghĩa thuật toán khung, gọi các bước trừu tượng; con cung cấp các bước cụ thể.

public abstract class DataExporter {
    public final void export(List<Record> records) {   // method final — khong override khung
        String header = buildHeader();
        String body = records.stream()
            .map(this::formatRecord)                    // goi abstract method
            .collect(Collectors.joining("\n"));
        String footer = buildFooter();
        writeFile(header + "\n" + body + "\n" + footer);
    }

    // Moi subclass customize 2 buoc
    protected abstract String formatRecord(Record r);
    protected abstract String buildHeader();
    protected String buildFooter() { return ""; }   // mac dinh empty, optional override

    private void writeFile(String content) { ... }   // private — khung quan ly
}

public class CsvExporter extends DataExporter {
    @Override
    protected String buildHeader() { return "id,name,email"; }

    @Override
    protected String formatRecord(Record r) {
        return r.id + "," + r.name + "," + r.email;
    }
}

public class JsonExporter extends DataExporter {
    @Override
    protected String buildHeader() { return "["; }

    @Override
    protected String formatRecord(Record r) {
        return "{\"id\":" + r.id + ", \"name\":\"" + r.name + "\"}";
    }

    @Override
    protected String buildFooter() { return "]"; }
}

Ưu điểm: luồng chung ở cha, biến thể ở con. Framework Java đầy ví dụ: javax.servlet.HttpServlet, AbstractList, InputStream.

5. Abstract class vs Interface — so sánh

Abstract classInterface
State (instance field)❌ (chỉ static final)
Constructor
Kế thừaĐơn (1 cha)Đa (nhiều interface)
VisibilityBất kỳMethod mặc định public
Abstract method
Default methodMethod concrete bình thườngdefault method (Java 8+)
static method✅ (Java 8+)
Mục đích"Is-a" với share state"Can-do" capability

Khi nào chọn abstract class?

Abstract class khi:

  • state chung subclass dùng (name, createdAt).
  • implementation chung không muốn lặp (template method).
  • Quan hệ "is-a" rõ: Circle extends Shape (hình tròn là một loại hình).

Interface khi:

  • Muốn nhóm class không cùng cây kế thừa có chung capability: Comparable, Serializable, Closeable.
  • Không cần state — chỉ hợp đồng method.
  • Muốn class implement nhiều "vai trò".

Modern Java (8+): interface cũng có default method, nên đôi khi ranh giới mờ. Rule đơn giản: cần state → abstract class; chỉ method → interface. Nhiều team hiện ưu tiên interface trừ khi thực sự cần abstract class.

6. protected constructor — pattern thường gặp

Abstract class thường dùng protected cho constructor:

public abstract class Shape {
    protected Shape(String name) { ... }
}

Lý do:

  • Không cần public — không ai bên ngoài new được class abstract.
  • protected vừa đủ để subclass gọi super(...).
  • Tín hiệu rõ ý đồ cho reader: "class này chỉ extends".

7. Abstract class kèm sealed (Java 17+)

Hạn chế subclass chỉ trong tập xác định:

public sealed abstract class Shape permits Circle, Square, Triangle {
    public abstract double area();
}

public final class Circle extends Shape { ... }
public final class Square extends Shape { ... }
public final class Triangle extends Shape { ... }

// class Hexagon extends Shape { ... }   // COMPILE ERROR — khong trong permits

Kết hợp với switch pattern matching: compiler biết exhaustive, không cần default:

double area = switch (s) {
    case Circle c -> Math.PI * c.radius() * c.radius();
    case Square sq -> sq.side() * sq.side();
    case Triangle t -> 0.5 * t.base() * t.height();
    // khong can default — compiler biet 3 case la day du
};

Sẽ đào sâu ở bài 5.

8. Pitfall tổng hợp

Nhầm 1: new abstract class.

Shape s = new Shape("abc");   // COMPILE ERROR

✅ Chỉ new subclass concrete.

Nhầm 2: Có abstract method mà quên khai class abstract.

public class Shape {          // COMPILE ERROR
    public abstract double area();
}

✅ Thêm abstract trước class.

Nhầm 3: Abstract method có body.

public abstract double area() { return 0; }   // COMPILE ERROR

✅ Abstract method không có { }.

Nhầm 4: Subclass concrete không implement đủ abstract method.

abstract class Shape { public abstract double area(); }
class Circle extends Shape { }   // COMPILE ERROR — chua implement area()

✅ Implement hoặc khai Circle cũng abstract.

Nhầm 5: Dùng abstract class cho "capability" thuần — vì bỏ qua sức mạnh của multi-interface.

abstract class Printable { public abstract void print(); }
// class Book extends Printable — OK, nhung class Book da extends PaperItem -> COMPILE ERROR

interface Printable { void print(); } — class implement được nhiều interface.

9. 📚 Deep Dive Oracle

ℹ️ 📚 Deep Dive Oracle (optional)

Spec / reference chính thức:

Ghi chú: Item 20 của Effective Java (Bloch) đề nghị mạnh: "ưu tiên interface hơn abstract class". Lý do chính: Java đơn kế thừa class → abstract class khoá class con khỏi kế thừa class khác. Interface không hạn chế đó. Với Java 8+ có default method, interface đã đủ mạnh cho hầu hết use case. Abstract class vẫn cần khi bạn thực sự muốn share state — interface không cho.

10. Tóm tắt

  • abstract class không instantiate được trực tiếp. new Shape() → compile error.
  • abstract method không có body, buộc subclass concrete implement.
  • Class có abstract method → class phải abstract. Class abstract có thể không có abstract method nào.
  • Abstract class có state (field), constructor, concrete method — khác với interface ở điểm này.
  • Template method pattern: cha định nghĩa final khung, gọi abstract method; con cung cấp bước cụ thể.
  • Constructor abstract class thường protected — tín hiệu "chỉ extends".
  • So với interface: abstract class khi cần state + đơn kế thừa; interface khi nhóm capability không cùng cây.
  • Modern Java ưu tiên interface; chỉ dùng abstract class khi thực sự cần state chung.
  • sealed abstract class (Java 17+) cho phép kiểm soát subclass chính xác + pattern matching exhaustive.

11. Tự kiểm tra

Tự kiểm tra
Q1
Đoạn sau compile không?
public abstract class Vehicle {
    public abstract int maxSpeed();
}
public class Bike extends Vehicle { }

Vehicle v = new Bike();

Không compile. Bike extends Vehicle nhưng không implement abstract method maxSpeed(). Compiler báo: "Bike is not abstract and does not override abstract method maxSpeed() in Vehicle".

Fix (chọn 1):

  • Implement trong Bike: public int maxSpeed() { return 25; }.
  • Đánh dấu Bike cũng abstract: public abstract class Bike extends Vehicle {}. Sau đó một subclass concrete (vd MountainBike) phải implement.
Q2
Khi nào chọn abstract class, khi nào chọn interface?

Abstract class khi:

  • Bạn cần state chung — field chia sẻ mà mọi subclass inherit: id, name, createdAt.
  • Bạn muốn template method pattern — khung thuật toán ở cha, subclass chỉ cung cấp bước cụ thể.
  • Quan hệ "is-a" rõ và subclass thực sự là loại con cụ thể: Circle extends Shape.
  • Bạn muốn cấm instantiate class base.

Interface khi:

  • Chỉ cần hợp đồng method, không có state.
  • Nhóm các class không cùng cây kế thừa nhưng chia sẻ một capability: Comparable, Iterable, Serializable, AutoCloseable.
  • Cần đa kế thừa — class implement được nhiều interface.
  • Muốn API mở mạnh cho nhiều implement, không ép cây kế thừa.

Modern Java rule: ưu tiên interface. Chỉ chọn abstract class khi thực sự cần share state hoặc khung template-method phức tạp.

Q3
Đoạn sau in gì?
abstract class Base {
    public Base() { step(); }
    public abstract void step();
}
class Sub extends Base {
    private int x = 10;
    @Override public void step() { System.out.println("x = " + x); }
}

new Sub();

In x = 0.

Thứ tự khởi tạo:

  1. JVM cấp memory, zero-out field Sub.x = 0.
  2. new Sub() chạy → ngầm gọi super()Base().
  3. Base() gọi step(). Dynamic dispatch chọn Sub.step() (kiểu thực là Sub).
  4. Sub.step() đọc xx vẫn 0 (chưa gán 10).
  5. Base() return. Giờ Java gán field initializer: x = 10.

Bài học: không gọi abstract method trong constructor — bẫy khó debug. Pattern này phổ biến đến mức có tên riêng: "partially constructed object". Fix: gọi method từ factory sau khi object đã khởi tạo xong.

Q4
Viết abstract class Shape với describe() dùng abstract area(), perimeter(). Sau đó Circle concrete.
public abstract class Shape {
    private final String name;

    protected Shape(String name) { this.name = name; }

    public abstract double area();
    public abstract double perimeter();

    public String describe() {
        return String.format("%s: area=%.2f, perimeter=%.2f",
            name, area(), perimeter());
    }
}

public class Circle extends Shape {
    private final double radius;

    public Circle(double radius) {
        super("Circle");
        if (radius <= 0) throw new IllegalArgumentException("radius must be positive");
        this.radius = radius;
    }

    @Override
    public double area() { return Math.PI * radius * radius; }

    @Override
    public double perimeter() { return 2 * Math.PI * radius; }
}

Các điểm đáng nhớ:

  • Constructor Shape protected: subclass gọi qua super("Circle"); không ai bên ngoài new Shape(...).
  • Field name private final: chỉ set qua constructor, subclass không access trực tiếp — getter nếu cần.
  • describe()template method — implementation cha, gọi abstract method của con. Nếu muốn cấm override describe, thêm final.
  • Validate radius trong constructor con, throw IllegalArgumentException.
  • area() / perimeter()@Override.
Q5
Abstract class có constructor — ai gọi nó và khi nào?

Abstract class có constructor như class thường, nhưng không ai new abstract class trực tiếp. Constructor được gọi chỉ qua super(...) từ subclass concrete.

Flow khi new Circle(5):

  1. JVM cấp memory cho object Circle.
  2. Circle(5) chạy, gọi super("Circle")Shape("Circle").
  3. Shape("Circle") gọi super()Object().
  4. Object() return → Shape gán this.name = "Circle".
  5. Shape return → Circle gán this.radius = 5.
  6. Reference object Circle hoàn chỉnh trả về caller.

Convention: constructor abstract class thường protected — không cần public vì không ai call trực tiếp, protected đủ cho subclass.


Bài tiếp theo: Interface — hợp đồng capability, default method, và đa kế thừa có kiểm soát