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ường | Java |
|---|---|
| Tên bài "Yesterday" | Method signature |
| Bản gốc Beatles | Method của class cha |
| Bản cover | Method 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
a1 và a2 có kiểu khai báo Animal nhưng kiểu thực là Dog/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 (
Soundthaysound), sai signature (int sound()thayString 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
| Override | Overload | |
|---|---|---|
| Quan hệ | Giữa class cha và class con | Trong cùng một class |
| Signature | Giống cha | Khác — khác parameter list |
| Return type | Giống hoặc subtype (covariant) | Tuỳ |
| Dispatch | Runtime (dynamic) | Compile time (static) |
| Mục đích | Thay behaviour của cha | Cung 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 -.-> vtableKhi bytecode invokevirtual sound()V chạy:
- JVM theo reference
a→ tìm object trên heap. - Từ object header đọc ra class pointer → tìm vtable của class đó.
- Trong vtable, chọn slot tương ứng với method
sound(). - 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,
staticmethod,privatemethod,finalmethod) — compiler cố định method tại compile time. Bytecodeinvokestatic/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 (protected → public).
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
}
finalcấm override — dùng để khoá behaviour quan trọng.staticmethod 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.privatekhô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án — new 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:
- JLS §8.4.8 — Inheritance, Overriding, and Hiding — rule đầy đủ cho override.
- JLS §8.4.8.1 — Overriding (by Instance Methods) — quy tắc signature + return type.
- JLS §8.4.8.3 — Requirements in Overriding and Hiding — visibility, checked exception.
- JVMS §5.4.5 — Method Overriding — rule cấp bytecode.
- JVMS §6.5 invokevirtual — dispatch thực tế.
- Effective Java Item 40: "Consistently use the Override annotation".
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 caching và speculative 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.
@Overrideannotation — 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+.
finalmethod cấm override;statickhông tham gia dispatch;privatekhô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
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());
▸
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:
skhai báoShapenhưng kiểu thựcCircle→Circle.name()→"Circle".ckhai báoCircle, kiểu thựcCircle→"Circle".s2khai báoShape, kiểu thựcShape→Shape.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.
Q2Vì sao đoạn sau in "name=null" dù 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();
▸
"name=null" dù 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:
- JVM cấp memory, zero-out mọi field →
name = null. - Constructor
Dog()chạy. Ngầm gọisuper()→Animal(). Animal()gọigreet(). Dynamic dispatch chọn method theo kiểu thựcDog→Dog.greet().Dog.greet()đọcname— nhưngname = "Rex"chưa được gán (bước này nằm sau khisuper()return).namevẫn lànull.Animal()return. Giờ Java mới chạy field initializer củaDog:name = "Rex".- 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 { }
}
▸
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ủaIOException). - Ném cùng kiểu:
throws IOException. - Ném unchecked exception (subclass
RuntimeException) — rule này không áp dụng cho unchecked.
Q4Covariant 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(); }
}
▸
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 Shape — compile error vì Shape không phải subtype của Circle, caller kỳ vọng Circle sẽ bị nhận về Shape rộng hơn.
Q5Java 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();
▸
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:
statickhông có polymorphism — đừng thiết kế như thể có.- Tránh gọi
staticqua 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