- 1 1. Những Điều Bạn Sẽ Học Trong Bài Viết Này
- 2 2. Đa Hình Có Nghĩa Gì Trong Java
- 2.1 2.1 Xử Lý Đối Tượng Thông Qua Kiểu Cha
- 2.2 2.2 Cùng Một Lời Gọi Phương Thức, Hành Vi Khác Nhau
- 2.3 2.3 Tại Sao Sử Dụng Kiểu Cha Lại Quan Trọng Đến Vậy
- 2.4 2.4 Mối Quan Hệ Giữa Đa Hình và Ghi Đè Phương Thức
- 2.5 2.5 Đây Không Phải Cú Pháp Mới — Đó Là Một Khái Niệm Thiết Kế
- 2.6 2.6 Kết Luận Chính Từ Phần Này
- 3 3. Cơ Chế Cốt Lõi: Ghi Đè Phương Thức và Ràng Buộc Động
- 3.1 3.1 Những gì được quyết định ở Thời Gian Biên Dịch so với Thời Gian Chạy
- 3.2 3.2 Kiểm Tra Tính Sẵn Có Của Phương Thức ở Thời Gian Biên Dịch
- 3.3 3.3 Phương Thức Thực Sự Được Chọn ở Thời Gian Chạy
- 3.4 3.4 Tại sao Ràng Buộc Động Cho Phép Polymorphism
- 3.5 3.5 Tại sao Các Phương Thức và Trường static Khác Biệt
- 3.6 3.6 Nhầm Lẫn Thông Thường của Người Mới Bắt Đầu — Được Làm Rõ
- 3.7 3.7 Tóm Tắt Phần
- 4 4. Viết Mã Polymorphic Bằng Kế Thừa (extends)
- 5 5. Viết Mã Đa Hình Sử Dụng Giao Diện (implements)
- 5.1 5.1 Giao Diện Đại Diện Cho “Những Gì Một Đối Tượng Có Thể Làm”
- 5.2 5.2 Định Nghĩa Hành Vi Trong Các Lớp Triển Khai
- 5.3 5.3 Sử dụng Kiểu Giao Diện trong Mã Gọi
- 5.4 5.4 Tại Sao Giao Diện Được Ưa Chuộng Trong Thực Tế
- 5.5 5.5 Ví Dụ Thực Tế: Hành Vi Có Thể Hoán Đổi
- 5.6 5.6 Giao Diện so Với Lớp Trừu Tượng — Cách Chọn
- 5.7 5.7 Tóm Tắt Phần
- 6 6. Các Lỗi Phổ Biến: instanceof và Ép Kiểu Xuống
- 7 7. Replacing if / switch Statements With Polymorphism
- 8 8. Practical Guidelines: When to Use Polymorphism — and When Not To
- 9 9. Tóm Tắt: Những Điểm Chính Về Đa Hình Trong Java
- 10 10. FAQ: Các Câu Hỏi Thường Gặp Về Đa Hình Trong Java
- 10.1 10.1 Sự khác biệt giữa đa hình và ghi đè phương thức là gì?
- 10.2 10.2 Ghi quá tải phương thức có được coi là đa hình trong Java không?
- 10.3 10.3 Tại sao tôi nên sử dụng giao diện hoặc kiểu cha?
- 10.4 10.4 Việc sử dụng instanceof luôn xấu sao?
- 10.5 10.5 Khi nào tôi nên chọn lớp trừu tượng thay vì giao diện?
- 10.6 10.6 Đa hình có ảnh hưởng đến hiệu suất không?
- 10.7 10.7 Tôi có nên thay thế mọi if hoặc switch bằng đa hình không?
- 10.8 10.8 Những ví dụ thực hành tốt là gì?
1. Những Điều Bạn Sẽ Học Trong Bài Viết Này
1.1 Đa hình trong Java — Giải thích trong một câu
Trong Java, đa hình có nghĩa là:
“Xử lý các đối tượng khác nhau thông qua cùng một kiểu, trong khi hành vi thực tế của chúng thay đổi tùy thuộc vào đối tượng cụ thể.”
Nói một cách đơn giản, bạn có thể viết mã sử dụng lớp cha hoặc giao diện, và sau đó thay đổi triển khai thực tế mà không cần thay đổi mã gọi.
Ý tưởng này là nền tảng của lập trình hướng đối tượng trong Java.
1.2 Tại Sao Đa Hình Quan Trọng
Đa hình không chỉ là một khái niệm lý thuyết.
Nó trực tiếp giúp bạn viết mã mà:
- Dễ mở rộng hơn
- Dễ bảo trì hơn
- Ít dễ bị hỏng khi yêu cầu thay đổi
Các tình huống điển hình mà đa hình tỏa sáng bao gồm:
- Các tính năng sẽ có nhiều biến thể hơn theo thời gian
- Mã chứa ngày càng nhiều câu lệnh
if/switch - Logic nghiệp vụ thay đổi độc lập với các bên gọi
Trong phát triển Java thực tế, đa hình là một trong những công cụ hiệu quả nhất để kiểm soát độ phức tạp.
1.3 Tại Sao Người Mới Bắt Đầu Thường Gặp Khó Khăn Với Đa Hình
Nhiều người mới bắt đầu cảm thấy đa hình khó hiểu lúc đầu, chủ yếu vì:
- Khái niệm này trừu tượng và không gắn liền với cú pháp mới
- Nó thường được giải thích cùng với kế thừa và giao diện
- Nó tập trung vào tư duy thiết kế, không chỉ là cơ chế mã
Do đó, người học có thể “biết thuật ngữ” nhưng vẫn không chắc khi nào và tại sao nên sử dụng nó.
1.4 Mục Tiêu Của Bài Viết Này
Khi kết thúc bài viết này, bạn sẽ hiểu:
- Đa hình thực sự có nghĩa gì trong Java
- Cách ghi đè phương thức và hành vi thời gian chạy hoạt động cùng nhau
- Khi nào đa hình cải thiện thiết kế — và khi nào không
- Cách nó thay thế logic điều kiện trong các ứng dụng thực tế
Mục tiêu là giúp bạn nhìn thấy đa hình không phải là một khái niệm khó khăn, mà là một công cụ thiết kế tự nhiên và thực tiễn.
2. Đa Hình Có Nghĩa Gì Trong Java
2.1 Xử Lý Đối Tượng Thông Qua Kiểu Cha
Cốt lõi của đa hình trong Java là một ý tưởng đơn giản:
Bạn có thể xử lý một đối tượng cụ thể thông qua lớp cha hoặc kiểu giao diện của nó.
Xem xét ví dụ sau:
Animal animal = new Dog();
Đây là những gì đang xảy ra:
- Kiểu biến là
Animal - Đối tượng thực tế là một
Dog
Mặc dù biến được khai báo là Animal, chương trình vẫn hoạt động đúng.
Đây không phải là một mánh khóe — nó là một tính năng cơ bản của hệ thống kiểu của Java.
2.2 Cùng Một Lời Gọi Phương Thức, Hành Vi Khác Nhau
Bây giờ hãy nhìn vào lời gọi phương thức này:
animal.speak();
Mã nguồn tự nó không bao giờ thay đổi.
Tuy nhiên, hành vi phụ thuộc vào đối tượng thực tế được lưu trong animal.
- Nếu
animaltham chiếu tới mộtDog→ triển khai của chó sẽ chạy - Nếu
animaltham chiếu tới mộtCat→ triển khai của mèo sẽ chạy
Đây là lý do tại sao nó được gọi là đa hình —
một giao diện, nhiều hình thức hành vi.
2.3 Tại Sao Sử Dụng Kiểu Cha Lại Quan Trọng Đến Vậy
Bạn có thể tự hỏi:
“Tại sao không chỉ dùng
Dogthay vìAnimalở mọi nơi?”
Sử dụng kiểu cha mang lại cho bạn những lợi thế mạnh mẽ:
- Mã gọi không phụ thuộc vào các lớp cụ thể
- Các triển khai mới có thể được thêm vào mà không cần sửa đổi mã hiện có
- Mã trở nên dễ tái sử dụng và kiểm thử hơn
Ví dụ:
public void makeAnimalSpeak(Animal animal) {
animal.speak();
}
Phương thức này hoạt động cho:
DogCat- Bất kỳ lớp động vật nào trong tương lai
Người gọi chỉ quan tâm đến những gì đối tượng có thể làm, không phải nó là gì.
2.4 Mối Quan Hệ Giữa Đa Hình và Ghi Đè Phương Thức
Đa hình thường bị nhầm lẫn với ghi đè phương thức, vì vậy hãy làm rõ.
- Ghi đè phương thức → Một lớp con cung cấp triển khai riêng cho phương thức của lớp cha
- Đa hình → Gọi phương thức đã được ghi đè thông qua một tham chiếu kiểu cha
Ghi đè cho phép đa hình, nhưng đa hình là nguyên tắc thiết kế sử dụng nó.
2.5 Đây Không Phải Cú Pháp Mới — Đó Là Một Khái Niệm Thiết Kế
.
Polymorphism không giới thiệu các từ khóa Java mới hay cú pháp đặc biệt nào.
- Bạn đã sử dụng
class,extendsvàimplements - Bạn đã gọi các phương thức theo cùng một cách
Điều thay đổi là cách bạn suy nghĩ về tương tác giữa các đối tượng.
Thay vì viết mã phụ thuộc vào các lớp cụ thể,
bạn thiết kế mã phụ thuộc vào các trừu tượng.
2.6 Kết Luận Chính Từ Phần Này
Tóm tắt lại:
- Polymorphism cho phép các đối tượng được sử dụng thông qua một kiểu chung
- Hành vi thực tế được xác định tại thời gian chạy
- Người gọi không cần biết chi tiết triển khai
Trong phần tiếp theo, chúng ta sẽ khám phá tại sao Java có thể làm được điều này,
bằng cách xem xét việc ghi đè phương thức và ràng buộc động một cách chi tiết.
3. Cơ Chế Cốt Lõi: Ghi Đè Phương Thức và Ràng Buộc Động
3.1 Những gì được quyết định ở Thời Gian Biên Dịch so với Thời Gian Chạy
Để thực sự hiểu polymorphism trong Java, bạn phải tách biệt hành vi thời gian biên dịch và hành vi thời gian chạy.
Java thực hiện hai quyết định khác nhau ở hai giai đoạn khác nhau:
- Thời gian biên dịch → Kiểm tra xem một lời gọi phương thức có hợp lệ đối với kiểu của biến hay không
- Thời gian chạy → Quyết định phương thức triển khai nào thực sự được thực thi
Sự tách biệt này là nền tảng của polymorphism.
3.2 Kiểm Tra Tính Sẵn Có Của Phương Thức ở Thời Gian Biên Dịch
Xem lại đoạn mã này một lần nữa:
Animal animal = new Dog();
animal.speak();
Ở thời gian biên dịch, trình biên dịch Java chỉ nhìn vào:
- Kiểu khai báo:
Animal
Nếu Animal định nghĩa phương thức speak(), lời gọi được coi là hợp lệ.
Trình biên dịch không quan tâm đối tượng cụ thể nào sẽ được gán sau này.
Điều này có nghĩa là:
- Bạn chỉ có thể gọi các phương thức tồn tại trong kiểu cha
- Trình biên dịch không “đoán” hành vi của lớp con
3.3 Phương Thức Thực Sự Được Chọn ở Thời Gian Chạy
Khi chương trình chạy, Java đánh giá:
- Đối tượng
animalthực sự tham chiếu tới gì - Liệu lớp đó có ghi đè phương thức được gọi hay không
Nếu Dog ghi đè speak(), thì cài đặt của Dog sẽ được thực thi, không phải của Animal.
Việc lựa chọn phương thức ở thời gian chạy này được gọi là ràng buộc động (hoặc dynamic dispatch).
3.4 Tại sao Ràng Buộc Động Cho Phép Polymorphism
Nếu không có ràng buộc động, polymorphism sẽ không tồn tại.
Nếu Java luôn gọi phương thức dựa trên kiểu khai báo của biến,
đoạn mã này sẽ vô nghĩa:
Animal animal = new Dog();
Ràng buộc động cho phép Java:
- Hoãn quyết định phương thức cho đến thời gian chạy
- Phù hợp hành vi với đối tượng thực tế
Tóm lại:
- Ghi đè tạo ra sự đa dạng
- Ràng buộc động kích hoạt nó
Cùng nhau, chúng làm cho polymorphism trở nên khả thi.
3.5 Tại sao Các Phương Thức và Trường static Khác Biệt
Một nguồn gây nhầm lẫn phổ biến là các thành viên static.
Quy tắc quan trọng:
- Các phương thức và trường static KHÔNG tham gia vào polymorphism
Tại sao?
- Chúng thuộc về lớp, không phải đối tượng
- Chúng được giải quyết ở thời gian biên dịch, không phải thời gian chạy
Điều này có nghĩa là:
Animal animal = new Dog();
animal.staticMethod(); // resolved using Animal, not Dog
Việc lựa chọn phương thức được cố định và không thay đổi dựa trên đối tượng thực tế.
3.6 Nhầm Lẫn Thông Thường của Người Mới Bắt Đầu — Được Làm Rõ
Hãy tóm tắt các quy tắc chính một cách rõ ràng:
- Tôi có thể gọi phương thức này không? → Được kiểm tra bằng kiểu khai báo (thời gian biên dịch)
- Phiên bản triển khai nào sẽ chạy? → Được quyết định bởi đối tượng thực tế (thời gian chạy)
- Điều gì hỗ trợ polymorphism? → Chỉ các phương thức instance đã được ghi đè
Khi sự phân biệt này rõ ràng, polymorphism sẽ không còn cảm giác bí ẩn nữa.
3.7 Tóm Tắt Phần
- Java xác thực các lời gọi phương thức dựa trên kiểu của biến
- Thời gian chạy chọn phương thức đã được ghi đè dựa trên đối tượng
- Cơ chế này được gọi là ràng buộc động
- Các thành viên static không phải là polymorphic
Trong phần tiếp theo, chúng ta sẽ xem cách viết mã polymorphic bằng kế thừa (extends), kèm theo các ví dụ cụ thể.
4. Viết Mã Polymorphic Bằng Kế Thừa (extends)
4.1 Mẫu Kế Thừa Cơ Bản
Hãy bắt đầu với cách đơn giản nhất để triển khai đa hình trong Java: kế thừa lớp.
class Animal {
public void speak() {
System.out.println("Some sound");
}
}
class Dog extends Animal {
@Override
public void speak() {
System.out.println("Woof");
}
}
class Cat extends Animal {
@Override
public void speak() {
System.out.println("Meow");
}
}
Ở đây:
Animalđịnh nghĩa một hành vi chung- Mỗi lớp con ghi đè hành vi đó bằng triển khai riêng của nó
Cấu trúc này là nền tảng của đa hình qua kế thừa.
4.2 Sử dụng Kiểu Cha Là Chìa Khóa
Bây giờ hãy xem mã gọi:
Animal a1 = new Dog();
Animal a2 = new Cat();
a1.speak();
a2.speak();
Mặc dù cả hai biến đều có kiểu Animal,
Java thực thi triển khai đúng dựa trên đối tượng thực tế.
Dog→"Woof"Cat→"Meow"
Mã gọi không cần biết — hoặc quan tâm — về lớp cụ thể.
4.3 Tại Sao Không Sử Dụng Kiểu Lớp Con Trực Tiếp?
Người mới bắt đầu thường viết mã như thế này:
Dog dog = new Dog();
dog.speak();
Điều này hoạt động, nhưng nó hạn chế tính linh hoạt.
Nếu sau này bạn giới thiệu một loại động vật khác, bạn phải:
- Thay đổi khai báo biến
- Cập nhật tham số phương thức
- Sửa đổi bộ sưu tập
Sử dụng kiểu cha tránh những thay đổi này:
List<Animal> animals = List.of(new Dog(), new Cat());
Cấu trúc giữ nguyên ngay cả khi thêm lớp con mới.
4.4 Những Gì Thuộc Về Lớp Cha?
Khi thiết kế đa hình dựa trên kế thừa, lớp cha nên chứa:
- Hành vi được chia sẻ bởi tất cả lớp con
- Các phương thức có ý nghĩa bất kể kiểu cụ thể
Tránh đặt hành vi vào lớp cha chỉ áp dụng cho một số lớp con.
Điều đó thường chỉ ra một vấn đề thiết kế.
Một quy tắc ngón tay cái tốt:
Nếu việc coi một đối tượng như kiểu cha cảm thấy “sai”, thì sự trừu tượng là không đúng.
4.5 Sử dụng Lớp Trừu Tượng
Đôi khi lớp cha không nên có triển khai mặc định nào cả.
Trong những trường hợp như vậy, sử dụng lớp trừu tượng.
abstract class Animal {
public abstract void speak();
}
Điều này thực thi các quy tắc:
- Lớp con phải triển khai
speak() - Lớp cha không thể được khởi tạo
Lớp trừu tượng hữu ích khi bạn muốn ép buộc một hợp đồng, không phải cung cấp hành vi.
4.6 Những Nhược Điểm Của Kế Thừa
Kế thừa mạnh mẽ, nhưng nó có những đánh đổi:
- Ghép nối mạnh giữa cha và con
- Hệ thống phân cấp lớp cứng nhắc
- Khó tái cấu trúc sau này
Vì những lý do này, nhiều thiết kế Java hiện đại ưu tiên giao diện hơn kế thừa.
4.7 Tóm Tắt Phần
- Kế thừa cho phép đa hình qua ghi đè phương thức
- Luôn tương tác qua kiểu cha
- Lớp trừu tượng thực thi hành vi bắt buộc
- Kế thừa nên được sử dụng cẩn thận
Tiếp theo, chúng ta sẽ khám phá đa hình sử dụng giao diện, đây thường là cách tiếp cận ưu tiên trong các dự án Java thực tế.
5. Viết Mã Đa Hình Sử Dụng Giao Diện (implements)
5.1 Giao Diện Đại Diện Cho “Những Gì Một Đối Tượng Có Thể Làm”
Trong phát triển Java thực tế, giao diện là cách phổ biến nhất để triển khai đa hình.
Một giao diện đại diện cho một khả năng hoặc vai trò, không phải danh tính.
interface Speaker {
void speak();
}
Ở giai đoạn này, không có triển khai — chỉ có một hợp đồng.
Bất kỳ lớp nào triển khai giao diện này đều hứa cung cấp hành vi này.
5.2 Định Nghĩa Hành Vi Trong Các Lớp Triển Khai
Bây giờ hãy triển khai giao diện:
class Dog implements Speaker {
@Override
public void speak() {
System.out.println("Woof");
}
}
class Cat implements Speaker {
@Override
public void speak() {
System.out.println("Meow");
}
}
Những lớp này không chia sẻ mối quan hệ cha-con.
Tuy nhiên, chúng có thể được xử lý thống nhất qua giao diện Speaker.
5.3 Sử dụng Kiểu Giao Diện trong Mã Gọi
Sức mạnh của các giao diện trở nên rõ ràng ở phía gọi:
Speaker s1 = new Dog();
Speaker s2 = new Cat();
s1.speak();
s2.speak();
Mã gọi:
- Chỉ phụ thuộc vào giao diện
- Không biết gì về các triển khai cụ thể
- Hoạt động không thay đổi khi các triển khai mới được thêm vào
Đây là đa hình thực sự trong thực tế.
5.4 Tại Sao Giao Diện Được Ưa Chuộng Trong Thực Tế
Các giao diện thường được ưu tiên hơn kế thừa vì chúng cung cấp:
- Kết nối lỏng lẻo
- Linh hoạt qua các lớp không liên quan
- Hỗ trợ cho nhiều triển khai
Một lớp có thể triển khai nhiều giao diện, nhưng chỉ có thể mở rộng một lớp.
Điều này làm cho các giao diện lý tưởng để thiết kế các hệ thống có thể mở rộng.
5.5 Ví Dụ Thực Tế: Hành Vi Có Thể Hoán Đổi
Các giao diện tỏa sáng trong các tình huống mà hành vi có thể thay đổi hoặc mở rộng:
- Phương thức thanh toán
- Kênh thông báo
- Chiến lược lưu trữ dữ liệu
- Cơ chế ghi log
Ví dụ:
public void notifyUser(Notifier notifier) {
notifier.send();
}
Bạn có thể thêm các phương thức thông báo mới mà không cần sửa đổi phương thức này.

5.6 Giao Diện so Với Lớp Trừu Tượng — Cách Chọn
Nếu bạn không chắc chắn nên sử dụng cái nào, hãy tuân theo hướng dẫn này:
- Sử dụng giao diện khi bạn quan tâm đến hành vi
- Sử dụng lớp trừu tượng khi bạn muốn trạng thái chia sẻ hoặc triển khai
Trong hầu hết các thiết kế Java hiện đại, bắt đầu với giao diện là lựa chọn an toàn hơn.
5.7 Tóm Tắt Phần
- Các giao diện định nghĩa hợp đồng hành vi
- Chúng cho phép đa hình linh hoạt, kết nối lỏng lẻo
- Mã gọi phụ thuộc vào các trừu tượng, không phải triển khai
- Các giao diện là lựa chọn mặc định trong thiết kế Java chuyên nghiệp
Tiếp theo, chúng ta sẽ xem xét một lỗi phổ biến: sử dụng instanceof và ép kiểu xuống, và lý do tại sao chúng thường chỉ ra vấn đề thiết kế.
6. Các Lỗi Phổ Biến: instanceof và Ép Kiểu Xuống
6.1 Tại Sao Nhà Phát Triển Sử Dụng instanceof
Khi học đa hình, nhiều nhà phát triển cuối cùng viết mã như thế này:
if (speaker instanceof Dog) {
Dog dog = (Dog) speaker;
dog.fetch();
}
Điều này thường xảy ra vì:
- Một lớp con có phương thức không được khai báo trong giao diện
- Hành vi cần khác nhau dựa trên lớp cụ thể
- Yêu cầu được thêm vào sau thiết kế ban đầu
Mong muốn “kiểm tra kiểu thực sự” là bản năng tự nhiên — nhưng nó thường báo hiệu vấn đề sâu sắc hơn.
6.2 Điều Gì Xảy Ra Sai Khi instanceof Lan Rộng
Sử dụng instanceof thỉnh thoảng không phải là sai lầm cố hữu.
Vấn đề phát sinh khi nó trở thành cơ chế kiểm soát chính.
if (speaker instanceof Dog) {
...
} else if (speaker instanceof Cat) {
...
} else if (speaker instanceof Bird) {
...
}
Mẫu này dẫn đến:
- Mã phải thay đổi mỗi khi một lớp mới được thêm vào
- Logic được tập trung ở người gọi thay vì đối tượng
- Mất lợi ích cốt lõi của đa hình
Lúc đó, đa hình bị bỏ qua một cách hiệu quả.
6.3 Rủi Ro Của Ép Kiểu Xuống
Ép kiểu xuống chuyển đổi kiểu cha thành kiểu con cụ thể:
Animal animal = new Dog();
Dog dog = (Dog) animal;
Điều này chỉ hoạt động nếu giả định là đúng.
Nếu đối tượng thực sự không phải là Dog, mã sẽ thất bại lúc chạy với ClassCastException.
Ép kiểu xuống:
- Đẩy lỗi từ thời gian biên dịch sang thời gian chạy
- Đưa ra giả định về bản sắc đối tượng
- Tăng tính mong manh
6.4 Có Thể Giải Quyết Điều Này Bằng Đa Hình Không?
Trước khi sử dụng instanceof, hãy tự hỏi:
- Hành vi này có thể được biểu diễn như một phương thức không?
- Giao diện có thể được mở rộng thay vì không?
- Trách nhiệm có thể được di chuyển vào chính lớp không?
Ví dụ, thay vì kiểm tra kiểu:
speaker.performAction();
Hãy để mỗi lớp quyết định cách thực hiện hành động đó.
6.5 Khi Nào instanceof Là Chấp Nhận Được
Có những trường hợp mà instanceof là hợp lý:
- Tích hợp với thư viện bên ngoài
- Mã di sản mà bạn không thể thiết kế lại
- Các lớp biên (bộ điều hợp, trình nối chuỗi)
The key rule:
Keep
instanceofat the edges, not at the core logic.
6.6 Practical Guideline
- Avoid
instanceofin business logic - Avoid designs that require frequent downcasting
- If you feel forced to use them, reconsider the abstraction
Next, we will see how polymorphism can replace conditional logic (if / switch) in a clean and scalable way.
7. Replacing if / switch Statements With Polymorphism
7.1 A Common Conditional Code Smell
Consider this typical example:
public void processPayment(String type) {
if ("credit".equals(type)) {
// Credit card payment
} else if ("bank".equals(type)) {
// Bank transfer
} else if ("paypal".equals(type)) {
// PayPal payment
}
}
At first glance, this code seems fine.
However, as the number of payment types grows, so does the complexity.
7.2 Applying Polymorphism Instead
We can refactor this using polymorphism.
interface Payment {
void pay();
}
class CreditPayment implements Payment {
@Override
public void pay() {
// Credit card payment
}
}
class BankPayment implements Payment {
@Override
public void pay() {
// Bank transfer
}
}
Calling code:
public void processPayment(Payment payment) {
payment.pay();
}
Now, adding a new payment type requires no changes to this method.
7.3 Why This Approach Is Better
This design provides several benefits:
- Conditional logic disappears
- Each class owns its own behavior
- New implementations can be added safely
The system becomes open for extension, closed for modification.
7.4 When Not to Replace Conditionals
Polymorphism is not always the right choice.
Avoid overusing it when:
- The number of cases is small and fixed
- Behavior differences are trivial
- Additional classes reduce clarity
Simple conditionals are often clearer for simple logic.
7.5 How to Decide in Practice
Ask yourself:
- Will this branch grow over time?
- Will new cases be added by others?
- Do changes affect many places?
If the answer is “yes,” polymorphism is likely the better choice.
7.6 Incremental Refactoring Is Best
You do not need perfect design from the start.
- Start with conditionals
- Refactor when complexity increases
- Let the code evolve naturally
This approach keeps development practical and maintainable.
Next, we will discuss when polymorphism should be used — and when it should not — in real-world projects.
8. Practical Guidelines: When to Use Polymorphism — and When Not To
8.1 Signs That Polymorphism Is a Good Fit
Polymorphism is most valuable when change is expected.
You should strongly consider it when:
- The number of variations is likely to increase
- Behavior changes independently of the caller
- You want to keep calling code stable
- Different implementations share the same role
In these cases, polymorphism helps you localize change and reduce ripple effects.
8.2 Signs That Polymorphism Is Overkill
Polymorphism is not free. It introduces more types and indirection.
Avoid it when:
- The number of cases is fixed and small
- The logic is short and unlikely to change
- Extra classes would hurt readability
Forcing polymorphism “just in case” often leads to unnecessary complexity.
8.3 Avoid Designing for an Imaginary Future
A common beginner mistake is adding polymorphism preemptively:
“We might need this later.”
In practice:
- Requirements often change in unexpected ways
- Many predicted extensions never happen
It is usually better to start simple and refactor when real needs appear.
8.4 A Practical View of the Liskov Substitution Principle (LSP)
You may encounter the Liskov Substitution Principle (LSP) when studying OOP.
A practical way to understand it is:
“Nếu tôi thay thế một đối tượng bằng một trong các kiểu con của nó, không có gì nên bị hỏng.”
Nếu việc sử dụng kiểu con gây ra bất ngờ, ngoại lệ hoặc xử lý đặc biệt,
abstraction có lẽ là sai.
8.5 Đặt Câu Hỏi Thiết Kế Đúng
Khi không chắc chắn, hãy hỏi:
- Người gọi có cần biết đây là implementation nào không?
- Hay chỉ hành vi mà nó cung cấp?
Nếu chỉ hành vi là đủ, đa hình thường là lựa chọn đúng.
8.6 Tóm Tắt Phần
- Đa hình là công cụ để quản lý thay đổi
- Sử dụng nó nơi biến đổi được mong đợi
- Tránh abstraction sớm
- Refactor hướng tới đa hình khi cần
Tiếp theo, chúng ta sẽ kết thúc bài viết với một tóm tắt rõ ràng và một phần FAQ.
9. Tóm Tắt: Những Điểm Chính Về Đa Hình Trong Java
9.1 Ý Tưởng Cốt Lõi
Về bản chất, đa hình trong Java xoay quanh một nguyên tắc đơn giản:
Mã nên phụ thuộc vào abstraction, không phải implementation cụ thể.
Bằng cách tương tác với đối tượng qua lớp cha hoặc giao diện,
bạn cho phép hành vi thay đổi mà không cần viết lại mã gọi.
9.2 Những Gì Bạn Nên Nhớ
Đây là những điểm quan trọng nhất từ bài viết này:
- Đa hình là một khái niệm thiết kế, không phải syntax mới
- Nó được thực hiện qua ghi đè phương thức và binding động
- Kiểu cha định nghĩa những gì có thể được gọi
- Hành vi thực tế được quyết định tại runtime
- Giao diện thường là cách tiếp cận ưu tiên
instanceofvà ép kiểu xuống nên được sử dụng tiết kiệm- Đa hình giúp thay thế logic điều kiện đang phát triển
9.3 Con Đường Học Tập Cho Người Mới Bắt Đầu
Nếu bạn vẫn đang xây dựng trực giác, hãy theo tiến trình này:
- Làm quen với việc sử dụng kiểu giao diện
- Quan sát cách các phương thức bị ghi đè hoạt động tại runtime
- Hiểu tại sao các câu lệnh điều kiện trở nên khó bảo trì hơn
- Refactor hướng tới đa hình khi độ phức tạp tăng
Với thực hành, đa hình trở thành lựa chọn thiết kế tự nhiên thay vì một “khái niệm khó khăn.”
10. FAQ: Các Câu Hỏi Thường Gặp Về Đa Hình Trong Java
10.1 Sự khác biệt giữa đa hình và ghi đè phương thức là gì?
Ghi đè phương thức là một cơ chế — định nghĩa lại một phương thức trong lớp con.
Đa hình là nguyên tắc cho phép các phương thức bị ghi đè được gọi qua tham chiếu kiểu cha.
10.2 Ghi quá tải phương thức có được coi là đa hình trong Java không?
Trong hầu hết các ngữ cảnh Java, không.
Ghi quá tải được giải quyết tại compile time, trong khi đa hình phụ thuộc vào hành vi runtime.
10.3 Tại sao tôi nên sử dụng giao diện hoặc kiểu cha?
Vì chúng:
- Giảm coupling
- Cải thiện khả năng mở rộng
- Ổn định mã gọi
Mã của bạn trở nên dễ bảo trì hơn khi yêu cầu phát triển.
10.4 Việc sử dụng instanceof luôn xấu sao?
Không, nhưng nó nên được giới hạn.
Nó chấp nhận được trong:
- Các lớp biên
- Hệ thống legacy
- Điểm tích hợp
Tránh sử dụng nó trong logic kinh doanh cốt lõi.
10.5 Khi nào tôi nên chọn lớp trừu tượng thay vì giao diện?
Sử dụng lớp trừu tượng khi:
- Bạn cần trạng thái chia sẻ hoặc implementation
- Có mối quan hệ “is-a” mạnh mẽ
Sử dụng giao diện khi hành vi và tính linh hoạt quan trọng hơn.
10.6 Đa hình có ảnh hưởng đến hiệu suất không?
Trong các ứng dụng kinh doanh điển hình, sự khác biệt hiệu suất là không đáng kể.
Khả năng đọc, khả năng bảo trì và tính đúng đắn quan trọng hơn nhiều.
10.7 Tôi có nên thay thế mọi if hoặc switch bằng đa hình không?
Không.
Sử dụng đa hình khi biến đổi được mong đợi và đang phát triển.
Giữ các câu lệnh điều kiện khi logic đơn giản và ổn định.
10.8 Những ví dụ thực hành tốt là gì?
Các kịch bản thực hành tuyệt vời bao gồm:
- Xử lý thanh toán
- Hệ thống thông báo
- Trình xuất định dạng file
- Chiến lược ghi log
Bất cứ nơi nào hành vi cần được thay thế, đa hình phù hợp tự nhiên

