Tính Đa hình (Polymorphism) trong Java - Java OOP
Thu, 17 Apr 2025

Khám phá các công cụ và kỹ thuật hữu ich cho Lập trình viên
Từ "Polymorphism" có nguồn gốc từ tiếng Hy Lạp, với "poly" nghĩa là "nhiều" và "morph" nghĩa là "hình dạng". Do đó, Polymorphism có nghĩa là "nhiều hình dạng". Trong ngữ cảnh OOP, nó thể hiện khả năng của một đối tượng, một biến tham chiếu, hoặc một phương thức có thể tồn tại dưới nhiều hình thức khác nhau hoặc thực hiện một hành động theo nhiều cách khác nhau tùy thuộc vào ngữ cảnh.
Hãy tưởng tượng hành động "phát ra âm thanh". Con chó sẽ "gâu gâu", con mèo sẽ "meo meo", con vịt sẽ "quạc quạc". Cùng là hành động "phát ra âm thanh" nhưng mỗi loài vật lại thực hiện theo cách riêng của nó. Đó chính là bản chất của tính đa hình – một hành động chung (phương thức) có thể có nhiều cách triển khai (hình dạng) khác nhau.
Trong Java, tính đa hình cho phép chúng ta:
if-else
hoặc switch-case
để kiểm tra kiểu đối tượng trước khi gọi hành động tương ứng.Các loại Đa hình trong Java
Java hỗ trợ hai loại đa hình chính:
Đây là loại đa hình được giải quyết bởi trình biên dịch (compiler) dựa trên "chữ ký" của phương thức. Nó đạt được thông qua Nạp chồng phương thức (Method Overloading).
Nạp chồng phương thức (Method Overloading):
Mục đích: Cho phép thực hiện các tác vụ tương tự nhưng với các loại hoặc số lượng dữ liệu đầu vào khác nhau, giúp code dễ đọc và dễ sử dụng hơn.
Ví dụ về Nạp chồng phương thức:
class Calculator {
// Nạp chồng phương thức add
public int add(int a, int b) {
System.out.println("Gọi add(int, int)");
return a + b;
}
public double add(double a, double b) {
System.out.println("Gọi add(double, double)");
return a + b;
}
public int add(int a, int b, int c) {
System.out.println("Gọi add(int, int, int)");
return a + b + c;
}
public String add(String s1, String s2) {
System.out.println("Gọi add(String, String)");
return s1 + s2;
}
// Ví dụ: Thay đổi thứ tự tham số với kiểu khác nhau
public void process(int id, String name) {
System.out.println("Processing ID: " + id + ", Name: " + name);
}
public void process(String name, int id) {
System.out.println("Processing Name: " + name + ", ID: " + id);
}
// Lỗi biên dịch! Chỉ khác kiểu trả về là không đủ để nạp chồng
/*
public String add(int x, int y) {
return "Sum: " + (x + y);
}
*/
}
public class OverloadingDemo {
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println("Tổng 1: " + calc.add(5, 10)); // Gọi add(int, int)
System.out.println("Tổng 2: " + calc.add(3.5, 2.5)); // Gọi add(double, double)
System.out.println("Tổng 3: " + calc.add(1, 2, 3)); // Gọi add(int, int, int)
System.out.println("Chuỗi nối: " + calc.add("Hello", " World")); // Gọi add(String, String)
calc.process(101, "Alice"); // Gọi process(int, String)
calc.process("Bob", 202); // Gọi process(String, int)
}
}
Trong ví dụ này, trình biên dịch biết chính xác phiên bản nào của phương thức add
hoặc process
cần gọi dựa vào các đối số được cung cấp tại thời điểm viết code.
Đây là dạng đa hình mạnh mẽ và thường được nhắc đến nhiều nhất khi nói về Polymorphism trong OOP. Nó liên quan đến kế thừa (inheritance) và giao diện (interface), và được thực hiện thông qua Ghi đè phương thức (Method Overriding).
Ghi đè phương thức (Method Overriding):
protected
, thì ở lớp con có thể là protected
hoặc public
, nhưng không thể là private
hoặc default
).final
và static
không thể bị ghi đè.@Override
được khuyến khích sử dụng để trình biên dịch kiểm tra xem phương thức có thực sự ghi đè một phương thức từ lớp cha/interface hay không.Cách hoạt động (Điều phối phương thức động - Dynamic Method Dispatch):
SuperClass ref = new SubClass();
// Hoặc
Interface ref = new ImplementingClass();
ref.overriddenMethod();
), Máy ảo Java (JVM) sẽ xác định phiên bản nào của phương thức (của lớp cha hay lớp con) sẽ được thực thi tại thời điểm chạy (runtime), dựa trên kiểu đối tượng thực tế mà biến tham chiếu đang trỏ tới. Quá trình này gọi là ràng buộc động (dynamic binding) hay điều phối phương thức động (dynamic method dispatch).Mục đích: Cho phép các lớp con cung cấp hành vi chuyên biệt cho các phương thức được kế thừa, trong khi vẫn giữ được "hợp đồng" chung được định nghĩa bởi lớp cha hoặc interface. Điều này tạo ra sự linh hoạt lớn, cho phép xử lý các đối tượng khác nhau một cách thống nhất.
Ví dụ về Ghi đè phương thức và Đa hình lúc chạy:
// Sử dụng lại ví dụ Shape từ bài Abstraction (hoặc định nghĩa mới)
abstract class Animal {
String name;
public Animal(String name) {
this.name = name;
}
// Phương thức sẽ được ghi đè bởi các lớp con
public abstract void makeSound(); // Hoặc là phương thức cụ thể nếu có âm thanh mặc định
public void eat() {
System.out.println(name + " is eating."); // Hành vi chung
}
}
class Dog extends Animal {
public Dog(String name) {
super(name);
}
// Ghi đè phương thức makeSound
@Override
public void makeSound() {
System.out.println(name + " says: Woof woof!");
}
}
class Cat extends Animal {
public Cat(String name) {
super(name);
}
// Ghi đè phương thức makeSound
@Override
public void makeSound() {
System.out.println(name + " says: Meow!");
}
}
class Duck extends Animal {
public Duck(String name) {
super(name);
}
// Ghi đè phương thức makeSound
@Override
public void makeSound() {
System.out.println(name + " says: Quack quack!");
}
}
public class RuntimePolymorphismDemo {
public static void main(String[] args) {
// Upcasting: Tham chiếu kiểu Animal trỏ đến đối tượng cụ thể
Animal myPet1 = new Dog("Buddy");
Animal myPet2 = new Cat("Whiskers");
Animal myPet3 = new Duck("Donald");
// Mảng chứa các đối tượng Animal khác nhau
Animal[] pets = { myPet1, myPet2, myPet3, new Dog("Lucy") };
System.out.println("--- Making sounds ---");
// Gọi cùng một phương thức makeSound() trên các đối tượng khác nhau
// JVM sẽ quyết định phiên bản nào được thực thi tại runtime
myPet1.makeSound(); // Gọi makeSound() của Dog
myPet2.makeSound(); // Gọi makeSound() của Cat
myPet3.makeSound(); // Gọi makeSound() của Duck
System.out.println("\n--- Pets in array ---");
for (Animal pet : pets) {
pet.makeSound(); // Đa hình lúc chạy!
pet.eat(); // Gọi phương thức kế thừa từ Animal
System.out.println("-----");
}
}
}
Trong ví dụ này, mặc dù myPet1
, myPet2
, myPet3
đều là biến tham chiếu kiểu Animal
, nhưng khi gọi makeSound()
, JVM đã gọi đúng phương thức được ghi đè trong các lớp Dog
, Cat
, Duck
tương ứng. Vòng lặp for
xử lý mảng pets
thể hiện rõ sức mạnh của đa hình: cùng một lời gọi pet.makeSound()
nhưng kết quả lại khác nhau tùy thuộc vào đối tượng thực tế mà pet
đang tham chiếu tới tại thời điểm đó.
Hai khái niệm này liên quan chặt chẽ đến đa hình lúc chạy:
Dog myDog = new Dog("Rex");
Animal myAnimal = myDog; // Upcasting ngầm định (từ Dog lên Animal)
instanceof
để kiểm tra kiểu trước khi ép kiểu xuống để tránh lỗi ClassCastException
.
Animal anotherPet = new Cat("Tom"); // Upcasting
// Kiểm tra trước khi ép kiểu xuống
if (anotherPet instanceof Cat) {
Cat myCat = (Cat) anotherPet; // Downcasting tường minh và an toàn
myCat.makeSound();
// myCat.climbTree(); // Giả sử Cat có phương thức riêng
} else if (anotherPet instanceof Dog) {
Dog myDog = (Dog) anotherPet; // Downcasting
myDog.makeSound();
// myDog.fetchStick(); // Giả sử Dog có phương thức riêng
}
if-else
hoặc switch
dựa trên kiểu đối tượng, làm code ngắn gọn và logic mạch lạc hơn.Kết luận
Tính đa hình là một khái niệm cực kỳ mạnh mẽ trong Java và OOP nói chung. Nó cho phép chúng ta viết code linh hoạt hơn, dễ mở rộng và dễ bảo trì hơn bằng cách cho phép các đối tượng khác nhau phản hồi theo cách riêng của chúng đối với cùng một thông điệp (lời gọi phương thức). Hiểu và vận dụng thành thạo cả đa hình lúc biên dịch (nạp chồng) và đặc biệt là đa hình lúc chạy (ghi đè) là chìa khóa để thiết kế và xây dựng các ứng dụng Java hiệu quả và chuyên nghiệp.
Thu, 17 Apr 2025
Thu, 17 Apr 2025
Sat, 22 Mar 2025
Để lại một bình luận