Java OO & Functional/Override và dynamic dispatch — đa hình ở runtime
4/38
Bài 4 / 38~18 phútKế thừa & Đa hìnhMiễn phí lượt xem

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 — phân biệt rõ hai loại đa hình

Một trong những nguồn nhầm lẫn lớn nhất của lập trình viên là không phân biệt được rõ ranh giới giữa Method Overriding (Ghi đè phương thức)Method Overloading (Nạp chồng phương thức). Trong Java, đây là hai hiện diện của hai kiểu đa hình hoàn toàn khác nhau:

  • Method Overriding đại diện cho Runtime Polymorphism (Đa hình lúc chạy) hay Dynamic Binding (Liên kết động).
  • Method Overloading đại diện cho Compile-time Polymorphism (Đa hình lúc biên dịch) hay Static Binding (Liên kết tĩnh).

Bảng so sánh toàn diện

Tiêu chíMethod Overriding (Ghi đè)Method Overloading (Nạp chồng)
Cơ chế Đa hìnhRuntime Polymorphism (Đa hình động)Compile-time Polymorphism (Đa hình tĩnh)
Cơ chế Liên kếtDynamic Binding (Quyết định bởi JVM khi chạy)Static Binding (Quyết định bởi Compiler khi biên dịch)
Vị trí khai báoPhải ở hai lớp có quan hệ kế thừa (Cha và Con)Có thể ở trong cùng một lớp hoặc lớp con
Chữ ký (Signature)Bắt buộc giống hệt (Tên + Tham số)Bắt buộc khác tham số (Số lượng, kiểu, hoặc thứ tự)
Kiểu trả vềPhải giống hệt hoặc là lớp con (Covariant Return)Có thể khác nhau tùy ý
Hiệu năngCó một chút chi phí vtable lookup ở RuntimeKhông có chi phí runtime, liên kết cực nhanh
Mục đíchThay đổi hoặc bổ sung hành vi của lớp chaĐịnh nghĩa nhiều cách sử dụng khác nhau cho một hành vi

Ví dụ minh họa

class Calc {
    // 1. Phương thức gốc
    public int add(int a, int b) { return a + b; }
    
    // 2. Overload: Khác tham số truyền vào (double, double)
    public double add(double a, double b) { return a + b; }
}

class SafeCalc extends Calc {
    // 3. Override: Giữ nguyên chữ ký, thay đổi logic ở lớp con
    @Override
    public int add(int a, int b) {
        return Math.addExact(a, b);   // Kiểm tra tràn số (overflow)
    }
}

Trong ví dụ trên:

  • SafeCalc.add(int, int)Override phương thức Calc.add(int, int). Khi chạy, JVM sẽ quyết định gọi hàm này nếu đối tượng thực tế là SafeCalc.
  • Calc.add(double, double)Overload của Calc.add(int, int). Ngay khi biên dịch, Compiler đã xác định được dòng code của bạn đang gọi hàm nào dựa vào kiểu dữ liệu truyền vào.

4. Dynamic dispatch — cơ chế JVM

Bảng so sánh trên rút gọn được thành một câu: một lời gọi đi qua hai lần quyết định, ở hai thời điểm khác nhau.

Hai cột đối chiếu: lúc biên dịch javac chốt chữ ký theo kiểu khai báo, lúc chạy JVM tra vtable theo lớp thật và chốt thân method

Cột trái là nơi overload được giải: javac chỉ nhìn kiểu khai báo của biến để chọn chữ ký, nên đổi kiểu khai báo là đổi luôn hàm được gọi — không cần chạy chương trình cũng biết. Cột phải là nơi override thắng: kể cả khi a khai báo kiểu Animal, nếu object thực là Dog thì JVM tra vtable của Dog, slot sound() trỏ tới Dog.sound(), nên phiên bản của Dog được gọi.

📚 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. Bẫy Constructor Override Trap — hiểm họa khi khởi tạo

Một trong những cạm bẫy tinh vi nhất liên quan đến đa hình là sự kết hợp giữa Constructor và Dynamic Dispatch. Khi bạn gọi một phương thức có thể override bên trong constructor của lớp cha, chương trình sẽ hoạt động theo cách cực kỳ khó lường.

⚠️ Cạm bẫy Constructor Override Trap

Hãy xem đoạn code sau và dự đoán xem new Dog() sẽ in ra gì:


class Animal {
  public Animal() {
      sound();   // Gọi method instance trong constructor của cha!
  }
  
  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);
  }
}

Nhiều lập trình viên nghĩ rằng kết quả sẽ là Dog says woof, name = Rex. Nhưng kết quả thực tế lại là: Dog says woof, name = null!

Giải thích cơ chế (Tại sao name = null?):

  1. Khi chạy new Dog(), constructor của lớp con (Dog()) sẽ ngầm gọi super() trước tiên để chạy constructor của lớp cha (Animal()).
  2. Constructor của Animal() bắt đầu chạy và gọi sound().
  3. Vì đối tượng thực tế đang được tạo là Dog, cơ chế Dynamic Dispatch lập tức kích hoạt vtable của Dog và chuyển hướng cuộc gọi đến phương thức @Override public void sound() của lớp Dog.
  4. Tuy nhiên, lúc này constructor của lớp Dog vẫn chưa hề chạy! Điều đó có nghĩa là tiến trình khởi tạo biến thành viên private String name = "Rex" của lớp con chưa diễn ra. Giá trị của name lúc này vẫn là mặc định: null.

Quy tắc an toàn tuyệt đối:

Không bao giờ gọi phương thức non-final (có thể override) bên trong constructor. Nếu muốn gọi phương thức phụ trợ trong constructor, hãy đánh dấu phương thức đó là private hoặc final để chặn đứng cơ chế Dynamic Dispatch, bảo vệ trạng thái an toàn của đối tượng.

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
0/5 câu đã trả lời
  1. 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());
  2. 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();
  3. 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 { }
    }
  4. 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(); }
    }
  5. 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();

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

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Bài này có giúp bạn hiểu bản chất không?

Ôn phỏng vấn

Bài này trả lời được các câu phỏng vấn sau — tự trả lời thử trước khi mở đáp án.

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên

Bài tiếp theo

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