Java — Từ Zero đến Senior/Kế thừa & Đa hình/Override và dynamic dispatch — đa hình ở runtime
2/7
~18 phútKế thừa & Đa hình

Override và dynamic dispatch — đa hình ở runtime

Override method vs overload, @Override annotation, dynamic dispatch dựa vào kiểu thực của object, covariant return type, và rule 'tightening' cấm thu hẹp visibility.

Bài trước bạn viết class Dog extends Animal và thấy Dog kế thừa eat() từ Animal. Nhưng nếu Dog muốn ăn khác (vd thêm log, kèm uống nước), bạn không muốn xoá eat() — bạn muốn override: giữ chữ ký giống cha, thay body.

Và đây là lúc Java làm điều tuyệt vời: kể cả bạn có reference kiểu Animal đang trỏ tới một Dog object, animal.eat() vẫn gọi phiên bản của Dog. Đó là dynamic dispatch — nền tảng của đa hình (polymorphism). Nhờ nó bạn viết code thao tác "mọi Animal" mà không cần biết loài cụ thể.

Bài này giải thích override đúng cách, phân biệt với overload, cơ chế dispatch ở bytecode, covariant return, và các rule Java quy định để override hợp lệ.

1. Analogy — ca sĩ hát lại bài hit

Bài hát "Yesterday" của Beatles được nhiều ca sĩ cover. Tên bài giống (eat() cùng signature), nhưng mỗi bản thu có phong cách riêng (body khác). Khi bạn bật playlist "Yesterday", app phát bản đúng ca sĩ được gắn vào track đó — không quan tâm bạn đang nhìn tên bài như nhau.

Đời thườngJava
Tên bài "Yesterday"Method signature
Bản gốc BeatlesMethod của class cha
Bản coverMethod override ở class con
Playlist phát đúng bản của ca sĩDynamic dispatch chọn method theo kiểu thực của object

💡 💡 Cách nhớ

Override = giữ signature, đổi body. Dynamic dispatch = JVM chọn phiên bản theo kiểu thực của object, không phải kiểu khai báo của biến.

2. Override cơ bản

class Animal {
    public String sound() {
        return "some sound";
    }
}

class Dog extends Animal {
    @Override
    public String sound() {
        return "woof";
    }
}

class Cat extends Animal {
    @Override
    public String sound() {
        return "meow";
    }
}

Dùng:

Animal a1 = new Dog();
Animal a2 = new Cat();
System.out.println(a1.sound());   // woof
System.out.println(a2.sound());   // meow

a1a2kiểu khai báo Animal nhưng kiểu thựcDog/Cat. Method sound() chọn theo kiểu thực → in đúng woof / meow.

2.1 @Override annotation — dùng luôn

class Dog extends Animal {
    @Override
    public String sound() { return "woof"; }
}

Không bắt buộc, nhưng nên có:

  • Compiler kiểm tra bạn thật sự override method có sẵn. Viết sai tên (Sound thay sound), sai signature (int sound() thay String sound()) → compile error.
  • Không có annotation, Java sẽ coi nó là method mới của class con → không override → bug im lặng.
class Dog extends Animal {
    // gõ sai — không có @Override
    public String soound() { return "woof"; }   // method moi, khong override
}

Animal a = new Dog();
a.sound();   // goi Animal.sound() -> "some sound" — BUG

⚠️ ⚠️ Luôn bật warning 'missing @Override' trong IDE

IntelliJ và Eclipse có inspection "Missing @Override annotation" — bật lên, để IDE nhắc tự động. Nhiều team xem việc thiếu @Override là compile warning nghiêm trọng.

3. Override vs overload — đừng nhầm

OverrideOverload
Quan hệGiữa class cha và class conTrong cùng một class
SignatureGiống chaKhác — khác parameter list
Return typeGiống hoặc subtype (covariant)Tuỳ
DispatchRuntime (dynamic)Compile time (static)
Mục đíchThay behaviour của chaCung cấp nhiều phiên bản cho tên quen
class Calc {
    public int add(int a, int b) { return a + b; }
    public double add(double a, double b) { return a + b; }   // overload
}

class SafeCalc extends Calc {
    @Override
    public int add(int a, int b) {
        return Math.addExact(a, b);   // override — kiem tra overflow
    }
}

SafeCalc.add(int, int) override Calc.add(int, int). Nhưng Calc.add(double, double) vẫn là overload ngang hàng.

4. Dynamic dispatch — cơ chế JVM

Khi gọi a.sound() với a kiểu khai báo Animal, compiler không cố định method nào sẽ chạy. Thay vào đó, compiler sinh instruction invokevirtual — JVM runtime lookup method dựa trên kiểu thực của object qua virtual method table (vtable).

flowchart LR
    subgraph Stack
        ref["a (Animal reference)"]
    end
    subgraph Heap
        obj["Dog object<br/>[vtable ptr]"]
    end
    subgraph ClassMetadata
        vtable["Dog vtable<br/>sound() -> Dog.sound"]
    end
    ref -.-> obj
    obj -.-> vtable

Khi bytecode invokevirtual sound()V chạy:

  1. JVM theo reference a → tìm object trên heap.
  2. Từ object header đọc ra class pointer → tìm vtable của class đó.
  3. Trong vtable, chọn slot tương ứng với method sound().
  4. Gọi method ghi ở slot đó.

Kết quả: kể cả a khai báo kiểu Animal, nếu object thực là Dog, vtable của Dog có slot sound()Dog.sound()Dog.sound.

ℹ️ 📚 Static dispatch vs dynamic dispatch

  • Static dispatch (overload, static method, private method, final method) — compiler cố định method tại compile time. Bytecode invokestatic / invokespecial.
  • Dynamic dispatch (instance method non-final) — JVM chọn runtime. Bytecode invokevirtual.

Hiểu điều này giải thích: gọi final method không có overhead vtable lookup — JIT có thể inline thẳng. Gọi non-final method có 1 tầng indirection.

5. Rule override — tightening không được, loosening được

Khi override, Java áp rule về:

5.1 Visibility — không thu hẹp

class Animal {
    public void eat() { }
}

class Dog extends Animal {
    protected void eat() { }   // COMPILE ERROR — thu hep tu public xuong protected
}

Class con không được thu hẹp visibility. public cha → public con. Có thể nới rộng (protectedpublic).

Lý do: Liskov — caller có Animal a; a.eat() phải gọi được. Nếu con thu hẹp thành protected, caller bị refuse → phá substitutability.

5.2 Checked exception — không mở rộng

class A {
    public void run() throws IOException { }
}

class B extends A {
    public void run() throws IOException, SQLException { }   // COMPILE ERROR
    public void run() { }                                     // OK — narrow xuong
    public void run() throws FileNotFoundException { }        // OK — subtype cua IOException
}

Con chỉ được ném subset exception của cha. Ngăn caller bị exception mới không lường trước.

5.3 Return type — covariant (Java 5+)

class Animal {
    public Animal clone() { return new Animal(); }
}

class Dog extends Animal {
    @Override
    public Dog clone() { return new Dog(); }   // OK — Dog la subtype cua Animal
}

Return type con có thể là subtype của return type cha. Gọi là covariant return.

Caller viết Animal a = dog.clone() vẫn work; nhưng caller biết rõ Dog dog có thể lấy trực tiếp Dog d = dog.clone() không cần cast.

5.4 final, static, private — không override được

class A {
    public final void f() { }
    public static void g() { }
    private void h() { }
}

class B extends A {
    public void f() { }        // COMPILE ERROR — cannot override final
    public static void g() { }  // KHONG override — chi shadow (bai sau)
    private void h() { }        // KHONG override — private khong ke thua
}
  • final cấm override — dùng để khoá behaviour quan trọng.
  • static method không có dynamic dispatch — chúng thuộc class, không phải object. Viết cùng tên ở con chỉ là shadow.
  • private không kế thừa nên không có gì để override.

6. Gọi phiên bản cha trong override — super.method()

class Animal {
    public String describe() { return "Animal"; }
}

class Dog extends Animal {
    @Override
    public String describe() {
        return super.describe() + " (Dog)";   // goi Animal.describe
    }
}

System.out.println(new Dog().describe());   // "Animal (Dog)"

super.describe() bypass dispatch, gọi chính xác phiên bản của cha. Pattern này gặp khi con muốn bổ sung logic thay vì thay hoàn toàn.

7. Dynamic dispatch trong constructor — bẫy

class Animal {
    public Animal() {
        sound();   // goi method instance trong constructor
    }
    public void sound() { System.out.println("animal sound"); }
}

class Dog extends Animal {
    private String name = "Rex";

    @Override
    public void sound() {
        System.out.println("Dog says woof, name=" + name);
    }
}

new Dog();   // in gi?

Output: Dog says woof, name=null.

Bẫy: khi Animal() chạy sound(), dynamic dispatch chọn Dog.sound() (kiểu thực là Dog). Nhưng Dog.name chưa được gánnew Dog() chưa khởi tạo field của con. name vẫn ở default null.

Rule: không gọi method non-final trong constructor. Rule này cho mọi ngôn ngữ OOP có dynamic dispatch — Java, C#, C++.

8. Pitfall tổng hợp

Nhầm 1: Quên @Override → sai signature → method mới thay vì override.

class Dog extends Animal {
    public void Sound() { ... }   // 'S' viet hoa — method moi
}

✅ Luôn @Override — compiler bắt tự động.

Nhầm 2: Thu hẹp visibility khi override.

class Dog extends Animal {
    protected String sound() { return "woof"; }   // cha la public
}

✅ Giữ public hoặc nới rộng, không thu hẹp.

Nhầm 3: Override static.

class A { public static void f() { ... } }
class B extends A { public static void f() { ... } }   // khong override — shadow

static method không tham gia dispatch. Nếu muốn polymorphism, dùng instance method.

Nhầm 4: Gọi method override trong constructor.

abstract class Animal {
    public Animal() { init(); }   // init() la abstract hoac override-able
    protected abstract void init();
}

✅ Không. Khởi tạo hẳn trong constructor của mỗi class cụ thể, hoặc dùng factory method.

Nhầm 5: Cast rồi gọi để "override" — thực ra là dispatch bình thường.

Animal a = new Dog();
((Dog) a).sound();   // cast khong can thiet — a.sound() da dispatch dung

✅ Chỉ cast khi cần gọi method chỉ có ở con (không có ở cha).

9. 📚 Deep Dive Oracle

ℹ️ 📚 Deep Dive Oracle (optional)

Spec / reference chính thức:

Ghi chú: JVMS §5.4.5 mô tả chính xác method resolution — JVM bắt đầu từ class thực, đi lên cây kế thừa đến khi tìm thấy method có signature match. Hiểu quy trình này giải thích sao override "cứ chạy" mà không cần ai register. JIT sau đó tối ưu bằng inline cachingspeculative inlining — gọi method polymorphic nhưng trong thực tế hầu như toàn 1 kiểu concrete thì JIT inline thẳng.

10. Tóm tắt

  • Override = class con viết lại method của cha với cùng signature, body khác.
  • @Override annotation — luôn dùng; compiler bắt lỗi nếu bạn gõ sai signature.
  • Overload khác override: overload là cùng class, khác signature; override là khác class, cùng signature.
  • Dynamic dispatch — JVM chọn method theo kiểu thực của object, không phải kiểu khai báo của biến. Cơ chế qua vtable + invokevirtual.
  • Rule override:
    • Không thu hẹp visibility (public → public, protected → public OK).
    • Không ném checked exception rộng hơn cha.
    • Return type có thể là subtype (covariant) — Java 5+.
  • final method cấm override; static không tham gia dispatch; private không kế thừa.
  • super.method() gọi phiên bản cha — pattern "làm thêm, vẫn gọi cha".
  • Không gọi method non-final trong constructor — dispatch chọn method con khi con chưa init xong.

11. Tự kiểm tra

Tự kiểm tra
Q1
Đoạn sau in gì?
class Shape {
    public String name() { return "Shape"; }
}
class Circle extends Shape {
    @Override
    public String name() { return "Circle"; }
}

Shape s = new Circle();
Circle c = new Circle();
Shape s2 = new Shape();
System.out.println(s.name() + " / " + c.name() + " / " + s2.name());

In Circle / Circle / Shape.

Cả 3 call đều dispatch theo kiểu thực:

  • s khai báo Shape nhưng kiểu thực CircleCircle.name()"Circle".
  • c khai báo Circle, kiểu thực Circle"Circle".
  • s2 khai báo Shape, kiểu thực ShapeShape.name()"Shape".

Kiểu khai báo (Shape s vs Circle c) chỉ ảnh hưởng: (1) compile-time check — method nào gọi được, (2) overload resolution. Không ảnh hưởng instance method dispatch.

Q2
Vì sao đoạn sau in "name=null"name đã khai báo private String name = "Rex";?
class Animal {
    public Animal() { greet(); }
    public void greet() { System.out.println("hi"); }
}
class Dog extends Animal {
    private String name = "Rex";
    @Override
    public void greet() { System.out.println("name=" + name); }
}

new Dog();

Thứ tự khởi tạo object Dog:

  1. JVM cấp memory, zero-out mọi field → name = null.
  2. Constructor Dog() chạy. Ngầm gọi super()Animal().
  3. Animal() gọi greet(). Dynamic dispatch chọn method theo kiểu thực DogDog.greet().
  4. Dog.greet() đọc name — nhưng name = "Rex" chưa được gán (bước này nằm sau khi super() return). name vẫn là null.
  5. Animal() return. Giờ Java mới chạy field initializer của Dog: name = "Rex".
  6. Body Dog() (không có gì) chạy.

Bài học: không gọi method non-final trong constructor. Nếu bạn kế thừa class framework có constructor gọi method của bạn override, bạn sẽ gặp bug này — rất khó debug vì khác với mental model "object đã init xong khi constructor chạy".

Q3
Đoạn sau compile không?
class A {
    public void run() throws java.io.IOException { }
}
class B extends A {
    @Override
    public void run() throws java.sql.SQLException { }
}

Không compile. JLS §8.4.8.3 yêu cầu override không ném checked exception rộng hơn cha. SQLException không phải subtype của IOException — nó là một exception mới hoàn toàn. Caller có A a; try { a.run(); } catch (IOException e) { ... } sẽ không catch được SQLException khi a thực ra là B.

Fix (chọn 1):

  • Không ném exception: public void run() { ... }.
  • Ném subset: throws java.io.FileNotFoundException (subtype của IOException).
  • Ném cùng kiểu: throws IOException.
  • Ném unchecked exception (subclass RuntimeException) — rule này không áp dụng cho unchecked.
Q4
Covariant return — đoạn sau OK hay lỗi?
class Shape {
    public Shape clone() { return new Shape(); }
}
class Circle extends Shape {
    @Override
    public Circle clone() { return new Circle(); }
}

OK. Từ Java 5+, override được phép return subtype của return type cha — gọi là covariant return. Circle là subtype của Shape → hợp lệ.

Lợi ích: caller Circle c = new Circle(); Circle c2 = c.clone(); không cần cast. Viết chung Shape s = someCircle; Shape cloned = s.clone(); cũng vẫn work — chỉ là bạn mất loại kiểu ở compile time.

Nếu đảo lại — cha return Circle, con return Shapecompile errorShape không phải subtype của Circle, caller kỳ vọng Circle sẽ bị nhận về Shape rộng hơn.

Q5
Java dispatch static method thế nào?
class A {
    public static void greet() { System.out.println("A"); }
}
class B extends A {
    public static void greet() { System.out.println("B"); }
}

A a = new B();
a.greet();

In A.

static method không tham gia dynamic dispatch. Compiler resolve method tại compile time dựa vào kiểu khai báo của reference (A a) — chọn A.greet(). Bytecode là invokestatic, không invokevirtual.

Khi B khai public static void greet(), nó không override — nó hide (ẩn) method của cha. Gọi qua B.greet() hoặc a thực tế là B nhưng khai báo B b, lúc đó mới chọn B.greet().

Bài học:

  • static không có polymorphism — đừng thiết kế như thể có.
  • Tránh gọi static qua instance reference (a.greet()). Gọi qua class (A.greet(), B.greet()) cho rõ.
  • Muốn polymorphism cho "hành vi của class" → dùng instance method + factory, hoặc enum với method override.

Bài tiếp theo: Abstract class — class không tạo được nhưng định hình subtype