Tính Đa Hình (Polymorphism)
"Trong lập trình hướng đối tượng, tính đa hình (Polymorphism) là một nguyên lý quan trọng cho phép một đối tượng thể hiện các hành vi khác nhau tùy thuộc vào ngữ cảnh. Điều này giúp tăng tính linh hoạt và mở rộng khả năng của chương trình. Dưới đây là những điều cơ bản bạn cần biết về tính đa hình trong Java."
Ⅰ. Đa hình (Polymorphism) là gì?
Tính đa hình (polymorphism) là một trong bốn tính chất cơ bản của lập trình hướng đối tượng (OOP) trong Java, bên cạnh tính đóng gói (encapsulation), tính kế thừa (inheritance) và tính trừu tượng (abstraction). Tính đa hình cho phép một đối tượng có thể thực hiện các hành động (phương thức) theo nhiều cách khác nhau. Điều này có nghĩa là cùng một giao diện hoặc phương thức, nhưng các đối tượng khác nhau có thể triển khai và thực hiện hành động đó theo cách riêng của mình.
Từ nguyên:
↳ "Poly" trong tiếng Hy Lạp nghĩa là "nhiều".
↳ "Morphs" trong tiếng Hy Lạp nghĩa là "hình dạng".
↳ Vì vậy, đa hình nghĩa đen là "nhiều hình dạng".
Các thuật ngữ trong đa hình (Polymorphism)
↳ Nạp chồng phương thức (Method Overloading): Cùng một tên phương thức nhưng với các danh sách tham số khác nhau.
↳ Nạp chồng hàm tạo (Constructor Overloading): Cùng một tên hàm tạo nhưng với các danh sách tham số khác nhau.
↳ Ghi đè phương thức (Method Overriding): Lớp con định nghĩa lại một phương thức đã có trong lớp cha.
↳ Kế thừa: Cơ chế cho phép một lớp con kế thừa các thuộc tính và phương thức từ lớp cha.
↳ Giao diện (Interface): Một bản hợp đồng quy định các phương thức mà một lớp phải thực hiện.
↳ Lớp trừu tượng (Abstract class): Một lớp không thể tạo đối tượng trực tiếp, thường chứa các phương thức trừu tượng (chỉ khai báo, không có triển khai).
Các loại đa hình trong Java
1. Đa hình thời gian biên dịch (Compile-time polymorphism):
↳ Nạp chồng phương thức (Method Overloading)
↳ Nạp chồng hàm tạo (Constructor Overloading)
2. Đa hình thời gian chạy (Runtime polymorphism):
↳ Ghi đè phương thức (Method Overriding):

Ⅱ. Đa hình thời gian biên dịch (Compile-time polymorphism)
Đa hình thời gian biên dịch (Compile-time polymorphism) hay còn gọi là đa hình tĩnh (static polymorphism), là một khái niệm trong lập trình hướng đối tượng (OOP) mà trong đó việc xác định phương thức nào sẽ được gọi diễn ra ngay tại thời điểm biên dịch chương trình. Nói cách khác, trình biên dịch sẽ quyết định phương thức nào sẽ được thực thi dựa trên kiểu dữ liệu của các đối số truyền vào hàm.
Đa hình thời gian biên dịch chủ yếu được thực hiện thông qua nạp chồng phương thức (method overloading) và nạp chồng hàm tạo (constructor overloading). Dưới đây sẽ giải thích chi tiết hơn về hai khái niệm này:
1. Nạp chồng phương thức (Method Overloading)
Định nghĩa: Là khả năng định nghĩa nhiều phương thức có cùng tên trong một lớp, nhưng khác nhau về số lượng hoặc kiểu dữ liệu của các tham số. Giúp đơn giản hóa cú pháp gọi phương thức, dễ dàng truy cập và sử dụng.
Có 2 cách nạp chồng phương thức (Method Overloading) trong java:
↳ Thay đổi số lượng các tham số.
↳ Thay đổi kiểu dữ liệu của các tham số.

Ví dụ: Example.java
class TinhTong {
// Phương thức tính tổng với hai số nguyên
public int add(int a, int b) {
return a + b;
}
// Nạp chồng phương thức tính tổng với ba số nguyên
public int add(int a, int b, int c) {
return a + b + c;
}
// Nạp chồng phương thức tính tổng với hai số double
public double add(double a, double b) {
return a + b;
}
}
public class Example {
public static void main(String[] args) {
TinhTong tong = new TinhTong();
// Gọi các phương thức add với các số khác nhau
System.out.println("Tổng hai số Nguyên: "+tong.add(1, 2)); // Gọi add(int, int)
System.out.println("Tổng ba số Nguyên: "+tong.add(1, 2, 3)); // Gọi add(int, int, int)
System.out.println("Tổng hai số Thực: "+tong.add(1.5, 2.5)); // Gọi add(double, double)
}
}
Kết quả của chương trình là:
Tổng ba số Nguyên: 6
Tổng hai số Thực: 4.0
Trong ví dụ này, phương thức add trong lớp TinhTong được nạp chồng ba lần với các tham số khác nhau: hai số nguyên, ba số nguyên và hai số double.
Cơ chế hoạt động: Khi gọi phương thức add, trình biên dịch sẽ dựa vào kiểu dữ liệu của các đối số truyền vào để chọn phương thức phù hợp.
Các câu hỏi về nạp chồng phương thức trong java
Câu hỏi 1: Tại sao không thể nạp chồng phương thức chỉ bằng cách thay đổi kiểu trả về?
↳ Trả lời: Trong Java, không thể nạp chồng phương thức chỉ bằng cách thay đổi kiểu trả về vì nếu chỉ khác nhau bởi kiểu trả về, hai phương thức có thể có cùng tên và cùng các tham số. Điều này dẫn đến sự mơ hồ khi gọi phương thức, vì không rõ được phương thức nào sẽ được sử dụng.
Ví dụ: Example.java
class TinhTong {
// Phương thức tính tổng với hai số nguyên
public int add(int a, int b) {
return a + b;
}
// Nạp chồng phương thức tính tổng với hai số double
public double add(int a, int b) {
return a + b;
}
}
public class Example {
public static void main(String[] args) {
TinhTong tong = new TinhTong();
// Gọi các phương thức add với các số khác nhau
System.out.println("Tổng hai số Nguyên: "+tong.add(1, 2)); // Gọi add(int, int)
System.out.println("Tổng hai số Thực: "+tong.add(1.5, 2.5)); // Gọi add(double, double)
}
}
Kết quả của chương trình là:
Trong ví dụ này, hai phương thức add cố gắng có cùng tên và cùng số lượng tham số, chỉ khác nhau bởi kiểu trả về (int và double). Điều này không được phép trong Java vì trình biên dịch không thể xác định được phương thức nào sẽ được gọi khi gọi add(2, 3), do cả hai phương thức đều có thể phù hợp với việc truyền vào hai số nguyên.
Câu hỏi 2: Có thể nạp chồng phương thức main() không?
↳ Trả lời: Có, bạn có thể nạp chồng (overload) phương thức main() có cùng tên trong một lớp, nhưng chỉ có một phương thức main() có chữ ký chuẩn để JVM (Java Virtual Machine) có thể chạy khi bạn thực thi chương trình Java.
Thông thường, chúng ta sử dụng một phương thức main() với chữ ký như sau:
Cú Pháp
public static void main(String[] args) {
// Code ở đây
}
Ví dụ: OverloadingMain.java
public class OverloadingMain {
public static void main(String[] args) {
System.out.println("Phương thức main chính");
}
public static void main(int[] args) {
System.out.println("Phương thức main với mảng số nguyên");
}
public static void main(String arg) {
System.out.println("Phương thức main với một chuỗi");
}
}
Kết quả của chương trình là:
Lưu ý:
↳ Phương thức main() chính: Mặc dù có thể nạp chồng main(), nhưng luôn phải có một phương thức main() với chữ ký chuẩn (public static void main(String[] args)) để JVM có thể bắt đầu thực thi chương trình.
↳ JVM chỉ gọi một phương thức main(): Khi bạn chạy chương trình, JVM chỉ gọi một phương thức main() duy nhất, đó là phương thức main() có chữ ký chuẩn.
Phân tích về nạp chồng phương thức và sự thay đổi kiểu dữ liệu
Chúng ta cần xem xét cách trình biên dịch Java quyết định phương thức nào sẽ được gọi khi có nhiều phương thức cùng tên (nạp chồng):
↳ Tìm kiếm phương thức phù hợp: Trình biên dịch sẽ tìm kiếm một phương thức có cùng tên và số lượng tham số với cuộc gọi phương thức.
↳ So sánh kiểu dữ liệu của tham số: Nếu có nhiều phương thức thỏa mãn điều kiện trên, trình biên dịch sẽ so sánh kiểu dữ liệu của các tham số truyền vào với kiểu dữ liệu của các tham số trong định nghĩa phương thức.
↳ Ép kiểu ngầm định: Nếu có sự khác biệt về kiểu dữ liệu, trình biên dịch sẽ cố gắng thực hiện ép kiểu ngầm định (implicit casting) để các đối số truyền vào phù hợp với kiểu dữ liệu của các tham số trong định nghĩa phương thức.
↳ Ưu tiên phương thức không cần ép kiểu: Nếu có nhiều phương thức đều có thể được gọi sau khi ép kiểu, trình biên dịch sẽ ưu tiên chọn phương thức không cần ép kiểu.
Cơ chế tự động ép kiểu trong nạp chồng phương thức
Khi bạn gọi một phương thức, trình biên dịch sẽ tìm kiếm phương thức phù hợp nhất dựa trên các đối số truyền vào. Nếu không tìm thấy một phương thức khớp chính xác, trình biên dịch sẽ cố gắng ép kiểu các đối số để tìm một phương thức phù hợp.
↳ Quy tắc ép kiểu: Java có một hệ thống quy tắc rõ ràng về việc ép kiểu các kiểu dữ liệu nguyên thủy (primitive data types). Các kiểu dữ liệu có phạm vi giá trị nhỏ hơn có thể được tự động ép lên các kiểu dữ liệu có phạm vi giá trị lớn hơn. Ví dụ: một byte có thể được ép lên int, long, float hoặc double.

Ví dụ 1: Java sẽ tự động ép kiểu trước khi thực hiện phép cộng.
Ví dụ: Example.java
public class Example {
// Phương thức nạp chồng với một số thực và một số nguyên
public void sum(double a,long b) {
System.out.println("Tổng một số thực và một số nguyên: " + (a + b));
}
// Phương thức nạp chồng với một số nguyên và một số thực
public void sum(double a, double b,double c) {
System.out.println("Tổng ba số thực: " + (a + b + c));
}
public static void main(String[] args) {
Example example = new Example();
//Java sẽ tự động nâng cấp biến b lên kiểu double trước khi thực hiện phép cộng.
example.sum(10,10);
example.sum(10, 10, 10);
}
}
Kết quả của chương trình là:
Tổng ba số thực: 30.0
Trong trường hợp này:
↳ Biến b kiểu long có phạm vi hẹp hơn so với biến a kiểu double.
↳ Khi thực hiện phép cộng a + b, Java sẽ tự động nâng cấp biến b lên kiểu double trước khi thực hiện phép cộng.
↳ Kết quả của phép cộng sẽ là một số có kiểu double.
Ví dụ 2: Nạp chồng hai phương thức những có kiểu đối số khác nhau
Ví dụ: Example.java
public class Example {
// Phương thức nạp chồng với hai int
public void sum(int a, int b) {
System.out.println("Tổng của 2 số int : " + (a + b));
}
// Phương thức nạp chồng với hai long
public void sum(long a, long b) {
System.out.println("Tổng của 2 số long: " + (a + b));
}
public static void main(String[] args) {
Example example = new Example();
// phương thứ sum() có đối số int sẽ được gọi
example.sum(10, 10);
}
}
Kết quả của chương trình là:
Trong trường hợp này, phương thức sum(int a, int b) sẽ được gọi vì các đối số 10 và 10 đều là int và khớp chính xác với phương thức sum đầu tiên.
↳ Lưu ý: Nếu bạn gọi example.sum(10, 20L), với 20L là một số long, thì phương thức sum(long a, long b) sẽ được gọi vì đối số thứ hai là kiểu long.
Ví dụ: Example.java
public class Example {
// Phương thức nạp chồng với hai int
public void sum(int a, int b) {
System.out.println("Tổng của 2 số int: " + (a + b));
}
// Phương thức nạp chồng với hai long
public void sum(long a, long b) {
System.out.println("Tổng của 2 số long: " + (a + b));
}
public static void main(String[] args) {
Example example = new Example();
// phương thứ sum() có đối số long sẽ được gọi
example.sum(10, 10L);
}
}
Kết quả của chương trình là:
Ví dụ 3: Nạp chồng hai phương thức tổng một số nguyên một số thực và ngược lại
Ví dụ: Example.java
public class Example {
// Phương thức nạp chồng với một số thực và một số nguyên
public void sum(double a, int b) {
System.out.println("Tổng một số thực và một số nguyên: " + (a + b));
}
// Phương thức nạp chồng với một số nguyên và một số thực
public void sum(int a, double b) {
System.out.println("Tổng một số nguyên và một số thực: " + (a + b));
}
public static void main(String[] args) {
Example example = new Example();
example.sum(10,10);
}
}
Kết quả của chương trình là:
Trong trường hợp này, vì cả hai phương thức đều yêu cầu một ép kiểu tương tự (ép int thành double), không có sự ưu tiên rõ ràng dựa trên độ chính xác của kiểu. Trong các trường hợp mâu thuẫn như vậy, trình biên dịch sẽ báo lỗi vì không thể quyết định rõ ràng phương thức nào cần gọi.
↳ Lưu ý: Nếu bạn gọi example.sum(10d, 20), với 10d là một số thực, thì phương thức sum(double a, int b) sẽ được gọi vì đối số thứ nhất là kiểu double.
↳ Lưu ý: Trong Java, một kiểu dữ liệu không thể tự động ép kiểu sang một kiểu dữ liệu nhỏ hơn. Cụ thể, kiểu double không thể tự động ép kiểu sang bất kỳ kiểu nào khác như float, long, int, short, hay byte. Điều này là do việc ép kiểu từ một kiểu lớn hơn sang kiểu nhỏ hơn có thể dẫn đến mất mát dữ liệu và cần phải được thực hiện rõ ràng bằng cách sử dụng ép kiểu tường minh (explicit casting).
2. Nạp chồng Constructor (Constructor Overloading)
↳ Định nghĩa: Là khả năng định nghĩa nhiều hàm tạo có cùng tên trong một lớp, nhưng khác nhau về số lượng hoặc kiểu dữ liệu của các tham số. Nạp chồng constructor đã được chúng tôi trình bày chi tiết trong phần về Constructor.
Tại sao cần nạp chồng hàm tạo?
↳ Khởi tạo đối tượng linh hoạt: Cho phép tạo đối tượng với các giá trị ban đầu khác nhau, tùy thuộc vào yêu cầu của từng trường hợp sử dụng.
↳ Tăng tính đọc hiểu: Làm cho code dễ đọc và dễ hiểu hơn, đặc biệt khi có nhiều cách khác nhau để khởi tạo một đối tượng.
↳ Hỗ trợ các kiểu thiết kế khác nhau: Ví dụ, pattern Builder thường sử dụng nạp chồng hàm tạo để xây dựng các đối tượng phức tạp một cách tuần tự.
Dưới đây là ví dụ về nạp chồng hàm tạo với một lớp Book:
Ví dụ: Book.java
public class Book {
private String title;
private String author;
private double price;
// Hàm tạo không tham số (default constructor)
public Book() {
this.title = "Unknown";
this.author = "Unknown";
this.price = 0.0;
}
// Hàm tạo với một tham số
public Book(String title) {
this.title = title;
this.author = "Unknown";
this.price = 0.0;
}
// Hàm tạo với hai tham số
public Book(String title, String author) {
this.title = title;
this.author = author;
this.price = 0.0;
}
// Hàm tạo với ba tham số
public Book(String title, String author, double price) {
this.title = title;
this.author = author;
this.price = price;
}
// Phương thức hiển thị thông tin sách
public void display() {
System.out.println("Tên sách: " + title);
System.out.println("Tác giả: " + author);
System.out.println("Giá: $" + price);
}
public static void main(String[] args) {
// Sử dụng các hàm tạo khác nhau để khởi tạo các đối tượng Book
Book book1 = new Book();
Book book2 = new Book("Java Programming");
Book book3 = new Book("Java Programming", "Java");
Book book4 = new Book("Java Programming", "Java", 39.99);
// Hiển thị thông tin các sách
book1.display();
book2.display();
book3.display();
book4.display();
}
}
Kết quả của chương trình là:
Tác giả: Unknown
Giá: $0.0
Tên sách: Java Programming
Tác giả: Unknown
Giá: $0.0
Tên sách: Java Programming
Tác giả: Java
Giá: $0.0
Tên sách: Java Programming
Tác giả: Java
Giá: $39.99
Như vậy, nạp chồng hàm tạo cho phép tạo các đối tượng với nhiều cách khởi tạo khác nhau tùy thuộc vào tham số được cung cấp.
↳ Lưu ý: Nạp chồng constructor đã được chúng tôi trình bày chi tiết trong phần về Constructor.
Ⅲ. Đa hình thời gian chạy (Runtime polymorphism)
Đa hình thời gian chạy (Runtime polymorphism), hay còn gọi là gọi phương thức động (Dynamic Method Dispatch), là một tính năng quan trọng trong lập trình hướng đối tượng. Nó cho phép các đối tượng của các lớp con khác nhau có thể thực hiện các hành động (phương thức) khác nhau như thể chúng là các đối tượng của lớp cha. Điều này chủ yếu được đạt được thông qua ghi đè phương thức (method overriding).
Nói cách khác, cùng một đoạn mã có thể hoạt động trên các đối tượng khác nhau nhưng lại có hành vi khác nhau tùy thuộc vào kiểu thực tế của đối tượng đó tại thời điểm chạy. Đây là một khía cạnh quan trọng của tính đa hình, giúp các chương trình trở nên linh hoạt và dễ bảo trì hơn.
Trước khi đi sâu vào đa hình thời gian chạy, chúng ta cần hiểu về "Upcasting".
1. Upcasting là gì?
Upcasting là việc gán một đối tượng của lớp con vào một biến tham chiếu có kiểu dữ liệu của lớp cha. Nói một cách đơn giản, upcasting là việc bạn gán một đối tượng của một lớp con (subclass) vào một biến tham chiếu có kiểu dữ liệu của lớp cha (superclass).
Tại sao lại gọi là upcasting?
↳ Up: Nghĩa là lên trên.
↳ Casting: Nghĩa là ép kiểu.
Vì vậy, upcasting nghĩa là "ép kiểu lên trên" trong hệ thống phân cấp lớp. Chúng ta đang "ép" một đối tượng của lớp con lên thành một đối tượng của lớp cha.
Tại sao upcasting lại hữu ích?
↳ Đa hình: Upcasting là cơ sở cho đa hình thời gian chạy. Nó cho phép chúng ta viết các đoạn code chung để xử lý các đối tượng thuộc các lớp khác nhau.
↳ Tính trừu tượng: Upcasting giúp chúng ta tập trung vào các đặc tính chung của các đối tượng, chứ không cần quan tâm đến các chi tiết cụ thể của từng lớp con.
↳ Dễ dàng quản lý: Upcasting giúp chúng ta quản lý các đối tượng một cách dễ dàng hơn, đặc biệt khi chúng ta có một hệ thống phân cấp lớp phức tạp.
Ví dụ đầu tiên về upcasting
Ví dụ
interface I {} // Giao diện I
class A {} // Lớp cha A
class B extends A implements I {} // Lớp con B kế thừa từ A và implement giao diện I
Các mối quan hệ lớp:
↳ B IS-A A: Lớp B kế thừa từ lớp A, do đó B "là một loại" của A.
↳ B IS-A I: Lớp B implements interface I, do đó B "là một loại" thỏa mãn các yêu cầu của I.
↳ B IS-A Object: Tất cả các lớp trong Java đều kế thừa trực tiếp hoặc gián tiếp từ lớp Object, vì vậy B cũng "là một loại" của Object.
Hãy xem xét một ví dụ cụ thể để giúp bạn hiểu rõ hơn về upcasting và mối quan hệ với đa hình thời gian chạy trong Java:
Giả sử chúng ta có các lớp sau:
Ví dụ
class Animal {
void sound() {
System.out.println("Con vật phát ra âm thanh");
}
}
class Dog extends Animal {
void sound() {
System.out.println("Chú Chó sủa gâu...gâu...");
}
void play() {
System.out.println("Chú chó chơi trò nhặt đồ");
}
}
Bây giờ, chúng ta sẽ sử dụng upcasting để tạo một đối tượng của lớp Dog và gán cho một biến tham chiếu của lớp Animal:
Ví dụ: Example.java
public class Example {
public static void main(String[] args) {
Animal animal = new Dog(); // Upcasting
animal.sound(); // Phương thức sound() của lớp Dog được gọi
}
}
Kết quả của chương trình là:
Tuy nhiên, không thể gọi animal.play(); vì biến animal chỉ được coi là Animal, và không biết đối tượng thực tế là Dog.
Ví dụ: Example.java
public class Example {
public static void main(String[] args) {
Animal animal = new Dog(); // Upcasting
animal.play(); // Lỗi biên dịch, vì phương thức play() không tồn tại trong Animal
// Vì biến tham chiếu là Animal, nên không thể gọi các phương thức hay thuộc tính chỉ có trong Dog
// Tuy nhiên, nếu animal thực sự là đối tượng của Dog thì các phương thức của Dog sẽ được thực thi khi chạy
}
}
Kết quả của chương trình là:
Tuy nhiên, để truy cập các phương thức và thuộc tính đặc biệt của lớp con, chúng ta cần sử dụng downcasting.
Ví dụ: Example.java
public class Example {
public static void main(String[] args) {
Animal animal = new Dog(); // Upcasting
animal.sound(); // Phương thức sound() của lớp Dog được gọi
// Nếu cần truy cập các phương thức đặc biệt của Dog hoặc Cat, ta cần downcasting
Dog dog = (Dog) animal; // Downcasting an toàn
dog.play(); // Đây là cách sử dụng phương thức run() của lớp Dog sau khi downcasting
}
}
Kết quả của chương trình là:
Chú chó chơi trò nhặt đồ
↳ Lưu ý: Trong downcasting (Dog dog = (Dog) animal;), biến animal được coi là Dog tạm thời để có thể gọi phương thức play() của lớp Dog.
2. Ghi đè phương thức (Method Overriding)
Ghi đè phương thức (Method Overriding) là quá trình một lớp con cung cấp triển khai (implementation) lại một phương thức đã được định nghĩa trong lớp cha của nó. Khi lớp con ghi đè một phương thức của lớp cha, nó cung cấp một triển khai mới cho phương thức đó, thay thế triển khai mặc định của lớp cha.
Trong Java, bạn có thể ghi đè (override) các phương thức của lớp cha trong lớp con nhưng không thể ghi đè các biến trường (fields), constructors và static methods. Dưới đây là các điểm quan trọng cần nhớ:
1. Ghi đè phương thức (Method overriding):
↳ Cho phép lớp con cung cấp một triển khai mới cho một phương thức đã được định nghĩa trong lớp cha.
↳ Các phương thức ghi đè phải có cùng tên, cùng số lượng đối số và cùng kiểu trả về như phương thức mà nó ghi đè.
↳ Cung cấp tính đa hình và cho phép lập trình linh hoạt hơn.
2. Không thể ghi đè các biến trường (fields):
↳ Các biến trường (instance variables) trong lớp cha không thể được ghi đè trong lớp con.
↳ Lớp con có thể sử dụng các biến trường của lớp cha nếu chúng được khai báo là protected hoặc public, nhưng không thể thay đổi giá trị của chúng.
3. Không thể ghi đè constructors:
↳ Constructors (hàm khởi tạo) không được phép ghi đè trong Java.
↳ Lớp con có thể gọi hàm khởi tạo của lớp cha bằng cách sử dụng super() nhưng không thể thay đổi hoặc ghi đè chúng.
4. Không thể ghi đè static methods:
↳ Static methods (phương thức tĩnh) không thể được ghi đè trong Java.
↳ Chúng không liên quan đến đối tượng cụ thể nào mà liên quan đến lớp chứa chúng, do đó không thể thay đổi hoặc ghi đè chúng từ các lớp con.
Ví dụ về đơn kế thừa trong Java khi sử dụng ghi đè phương thức (method overriding):
Ví dụ: Example.java
// Lớp cha Person
class Person {
String name;
int age;
// Constructor của lớp Person
Person(String name, int age) {
this.name = name;
this.age = age;
}
// Phương thức in thông tin
void displayInfo() {
System.out.println("Tên: " + name);
System.out.println("Tuổi: " + age);
}
}
// Lớp con Student kế thừa từ lớp Person
class Student extends Person {
String major;
// Constructor của lớp Student
Student(String name, int age, String major) {
super(name, age); // Gọi constructor của lớp cha
this.major = major;
}
// Ghi đè phương thức displayInfo từ lớp cha
@Override
void displayInfo() {
super.displayInfo(); // Gọi phương thức của lớp cha
System.out.println("Chuyên Ngành: " + major);
}
}
// Class Example sử dụng lớp Student
public class Example {
public static void main(String[] args) {
Student student = new Student("Vương", 20, "Lập trình");
student.displayInfo(); // Gọi phương thức displayInfo của Student
}
}
Kết quả của chương trình là:
Tuổi: 20
Chuyên Ngành: Lập trình
Đoạn code, trên ví dụ cơ bản về đơn kế thừa và ghi đè phương thức trong Java, cho thấy cách mà lớp con có thể mở rộng và thay đổi hành vi của lớp cha theo nhu cầu của nó.
Ví dụ về kế thừa nhiều cấp trong Java khi sử dụng ghi đè phương thức (method overriding):
Ví dụ: Example.java
// Lớp cơ bản Vehicle
class Vehicle {
void display() {
System.out.println("Đây là một phương tiện.");
}
}
// Lớp Car kế thừa từ Vehicle
class Car extends Vehicle {
@Override
void display() {
System.out.println("Đây là một chiếc ô tô.");
}
}
// Lớp SportsCar kế thừa từ Car
class SportsCar extends Car {
@Override
void display() {
System.out.println("Đây là một chiếc xe thể thao.");
}
}
// Lớp Example sử dụng các lớp trên
public class Example {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle();
vehicle.display(); // In ra: Đây là một phương tiện.
Vehicle car = new Car();
car.display(); // In ra: Đây là một chiếc ô tô.
Vehicle sportsCar = new SportsCar();
sportsCar.display(); // In ra: Đây là một chiếc xe thể thao.
}
}
Kết quả của chương trình là:
Đây là một chiếc ô tô.
Đây là một chiếc xe thể thao.
Khi chạy ví dụ trên, các phương thức display được gọi tương ứng với đối tượng thực tế mà biến tham chiếu đang trỏ đến, cho phép ta thấy được tính đa hình trong hành vi của các đối tượng kế thừa nhiều cấp trong Java.
Ví dụ về kế thừa thứ bậc trong Java khi sử dụng ghi đè phương thức (method overriding):
Ví dụ: Example.java
// Lớp cơ bản Bank
class Bank {
protected double SoDu; // Số dư trong tài khoản
public Bank(double SoDu) {
this.SoDu = SoDu;
}
// Phương thức tính lãi suất
public double TinhLaiXuat() {
return 0; // Mặc định không có lãi suất
}
public void display() {
System.out.println("Số dư trong tài khoản là: " + SoDu);
}
}
// Lớp con ABCBank kế thừa từ Bank
class ABCBank extends Bank {
public ABCBank(double SoDu) {
super(SoDu);
}
// Ghi đè phương thức TinhLaiXuat để tính lãi suất cho tài khoản tiết kiệm
@Override
public double TinhLaiXuat() {
return SoDu * 0.05;
}
// Ghi đè phương thức display để hiển thị lãi xuất hằng năm
@Override
public void display() {
System.out.println("Lãi xuất hàng nằm của ABCBank: " + TinhLaiXuat());
}
}
// Lớp con DEFBank kế thừa từ Bank
class DEFBank extends Bank {
public DEFBank(double SoDu) {
super(SoDu);
}
// Ghi đè phương thức TinhLaiXuat để tính lãi suất cho tài khoản tiết kiệm
@Override
public double TinhLaiXuat() {
return SoDu * 0.06;
}
// Ghi đè phương thức display để hiển thị lãi xuất hằng năm
@Override
public void display() {
System.out.println("Lãi xuất hàng nằm của DEFBank: " + TinhLaiXuat());
}
}
// Lớp con IGHBank kế thừa từ Bank
class IGHBank extends Bank {
public IGHBank(double SoDu) {
super(SoDu);
}
// Ghi đè phương thức TinhLaiXuat để tính lãi suất cho tài khoản tiết kiệm
@Override
public double TinhLaiXuat() {
return SoDu * 0.07;
}
// Ghi đè phương thức display để hiển thị lãi xuất hằng năm
@Override
public void display() {
System.out.println("Lãi xuất hàng nằm của IGHBank: " + TinhLaiXuat());
}
}
// Lớp Example sử dụng các lớp trên
public class Example {
public static void main(String[] args) {
ABCBank abcbank = new ABCBank(2000000);
abcbank.display();
DEFBank defbank = new DEFBank(3000000);
defbank.display();
IGHBank ighbank = new IGHBank(1000000);
ighbank.display();
}
}
Kết quả của chương trình là:
Lãi xuất hàng nằm của DEFBank: 180000.0
Lãi xuất hàng nằm của IGHBank: 70000.0
Khi chạy ví dụ trên, các phương thức display được gọi tương ứng với đối tượng thực tế mà biến tham chiếu đang trỏ đến, cho phép ta thấy được tính đa hình trong hành vi của các đối tượng kế thừa thứ bậc trong Java.
Lưu ý khi ghi đè phương thức trong kế thừa
Khi ghi đè phương thức trong kế thừa, việc tuân thủ các quy tắc và nguyên tắc nhất định sẽ giúp code của bạn rõ ràng, dễ bảo trì và tránh các lỗi không mong muốn. Dưới đây là một số lưu ý quan trọng:
Quy tắc cơ bản:
↳ Tên phương thức: Phải hoàn toàn giống với phương thức trong lớp cha.
↳ Tham số: Số lượng và kiểu dữ liệu của các tham số phải giống hệt.
↳ Kiểu trả về: Kiểu trả về của phương thức con phải giống hoặc là kiểu con của kiểu trả về trong lớp cha.
↳ Quyền truy cập: Quyền truy cập của phương thức con không thể hẹp hơn quyền truy cập của phương thức trong lớp cha (ví dụ: nếu phương thức trong lớp cha là public thì phương thức trong lớp con cũng phải là public hoặc protected).
Hãy cùng so sánh chi tiết giữa nạp chồng phương thức (Overloading) và ghi đè phương thức (Overriding) nhé:
Tính năng | Nạp Chồng Phương Thức (Overloading) | Ghi Đè Phương Thức (Overriding) |
---|---|---|
Mục đích | Cung cấp nhiều phiên bản của cùng một phương thức trong cùng một lớp, với các danh sách tham số khác nhau. | Cho phép lớp con định nghĩa lại một phương thức đã có sẵn trong lớp cha. |
Tên phương thức | Cùng tên. | Cùng tên. |
Tham số | Số lượng hoặc kiểu dữ liệu của tham số khác nhau. | Số lượng và kiểu dữ liệu của tham số giống nhau. |
Kiểu trả về | Có thể khác nhau. | Có thể giống hoặc là kiểu con của kiểu trả về trong lớp cha. |
Quyền truy cập | Có thể khác nhau. | Không thể hẹp hơn quyền truy cập của phương thức trong lớp cha. |
Thời điểm quyết định | Tại thời điểm biên dịch. | Tại thời điểm chạy. |
Mục đích | Tăng tính linh hoạt trong việc sử dụng phương thức, cho phép xử lý các dữ liệu khác nhau với cùng một tên phương thức. | Thực hiện đa hình, cho phép các đối tượng của các lớp con khác nhau thực hiện cùng một hành động theo những cách khác nhau. |
Khi nào nên sử dụng:
↳ Nạp chồng phương thức: Khi bạn muốn thực hiện cùng một hành động trên các kiểu dữ liệu khác nhau.
↳ Ghi đè phương thức: Khi bạn muốn tùy chỉnh hành vi của một phương thức trong lớp con để phù hợp với nhu cầu cụ thể.