1. Đa hình: nó là gì và dùng để làm gì
Nếu bạn nghĩ đa hình là thứ gì đó từ thế giới dị nhân Marvel, xin lỗi phải làm bạn thất vọng: trong lập trình mọi thứ bình lặng hơn nhiều, nhưng không kém phần “ma thuật”. Đa hình là khả năng các đối tượng với hiện thực khác nhau phản hồi khác nhau với cùng một lời gọi phương thức.
Ví dụ đời thường:
Bạn có lớp Book và lớp Magazine, cả hai đều kế thừa từ lớp trừu tượng LibraryItem. Bạn muốn có thể gọi phương thức printInfo() cho bất kỳ phần tử thư viện nào và nó sẽ in ra thông tin cần thiết — với sách là tác giả và tiêu đề, với tạp chí là số phát hành và ngày.
Ví dụ mã:
abstract class LibraryItem {
String title;
LibraryItem(String title) {
this.title = title;
}
abstract void printInfo();
}
class Book extends LibraryItem {
String author;
Book(String title, String author) {
super(title);
this.author = author;
}
@Override
void printInfo() {
System.out.println("Sách: " + title + ", tác giả: " + author);
}
}
class Magazine extends LibraryItem {
int issueNumber;
Magazine(String title, int issueNumber) {
super(title);
this.issueNumber = issueNumber;
}
@Override
void printInfo() {
System.out.println("Tạp chí: " + title + ", số phát hành: " + issueNumber);
}
}
Giờ bạn có thể tạo một mảng gồm các phần tử khác nhau và gọi printInfo() cho từng phần tử:
LibraryItem[] items = {
new Book("Chúa ruồi", "William Golding"),
new Magazine("Khoa học và Đời sống", 5)
};
for (LibraryItem item : items) {
item.printInfo();
}
// Sẽ in:
// Sách: Chúa ruồi, tác giả: William Golding
// Tạp chí: Khoa học và Đời sống, số phát hành: 5
Đa hình hoạt động như vậy đấy!
2. Những lỗi thường gặp với đa hình
Cố gọi các phương thức không tồn tại trong kiểu cơ sở
Một trong những lỗi phổ biến nhất là cố gắng gọi một phương thức chỉ được khai báo ở lớp con thông qua tham chiếu kiểu cơ sở.
LibraryItem item = new Book("Harry Potter", "J. K. Rowling");
// item.getAuthor(); // Lỗi biên dịch! Trong LibraryItem không có phương thức getAuthor()
Java biên dịch mã dựa trên những gì nó thấy ở kiểu của biến (LibraryItem), chứ không phải ở đối tượng thực tế (Book). Vì vậy, nếu bạn cần gọi một phương thức đặc thù của sách, bạn phải ép kiểu:
if (item instanceof Book) {
Book book = (Book) item;
// Giờ có thể gọi book.getAuthor()
}
Ép kiểu mà không kiểm tra
Nếu bạn chắc rằng đối tượng là Book nhưng thực tế không phải, bạn sẽ nhận ClassCastException lúc chạy. Ví dụ:
LibraryItem item = new Magazine("Forbes", 12);
Book book = (Book) item; // BÙM! ClassCastException
Cách đúng — luôn kiểm tra kiểu trước:
if (item instanceof Book) {
Book book = (Book) item;
// OK
} else {
System.out.println("Đây không phải là sách!");
}
Không tận dụng lợi thế của đa hình
Đôi khi lập trình viên viết mã gắn chặt vào các kiểu cụ thể, trong khi có thể dùng trừu tượng hóa. Ví dụ, nếu bạn viết:
Book[] books = ...;
for (Book book : books) {
book.printInfo();
}
Điều này chỉ hoạt động cho sách. Còn nếu ngày mai có tạp chí, báo, truyện tranh thì sao? Tốt hơn hãy dùng mảng LibraryItem[] và làm việc với các phương thức của lớp cơ sở hoặc interface.
3. Trừu tượng hóa: tại sao cần và làm sao không làm hỏng
Lớp trừu tượng và interface
Trừu tượng hóa là nghệ thuật nhấn mạnh cái chính và ẩn đi chi tiết. Trong Java có lớp trừu tượng và interface để làm việc này.
- Lớp trừu tượng — là lớp không thể được tạo trực tiếp, chỉ có thể được kế thừa.
- Interface — là bản hợp đồng: lớp phải làm gì, chứ không phải làm như thế nào.
Lỗi 1: Tạo lớp trừu tượng mà không có phương thức trừu tượng
Nếu lớp trừu tượng của bạn không có phương thức trừu tượng nào, hãy cân nhắc — liệu nó có thực sự cần là trừu tượng không? Có lẽ đơn giản là làm lớp thường?
abstract class UselessAbstract {
void sayHello() {
System.out.println("Hello!");
}
}
// Nên làm lớp thường nếu không có phương thức trừu tượng
Lỗi 2: Thiếu triển khai các phương thức bắt buộc ở lớp con
Nếu một lớp kế thừa lớp trừu tượng hoặc triển khai interface, nó bắt buộc phải hiện thực tất cả các phương thức trừu tượng. Nếu quên — trình biên dịch sẽ nhắc, nhưng đôi khi phương thức được hiện thực “cho có” và không làm gì cả. Điều này khiến việc bảo trì khó khăn.
class Magazine extends LibraryItem {
Magazine(String title, int issueNumber) {
super(title);
// ...
}
@Override
void printInfo() {
// Trống! Tệ!
}
}
Lỗi 3: Hệ phân cấp trừu tượng quá sâu hoặc rối rắm
Khi các lớp kế thừa nhau thành 5–10 cấp, việc hiểu chúng trở nên rất khó. Tốt hơn là làm hệ phân cấp “phẳng”, dễ hiểu.
Ví dụ tệ:
LibraryItem
|
BookItem
|
PrintedBook
|
IllustratedBook
|
ChildrenIllustratedBook
Khó hiểu, đúng không? Tốt hơn nên giới hạn ở 2–3 cấp.
4. Thực hành: Áp dụng đa hình và trừu tượng hóa trong ứng dụng học tập
Hãy hoàn thiện ứng dụng thư viện học tập của bạn. Trước đây bạn chỉ có sách. Giờ thêm tạp chí và hiện thực giao diện chung cho ấn phẩm in.
Khai báo lớp trừu tượng:
abstract class LibraryItem {
protected String title;
public LibraryItem(String title) {
this.title = title;
}
public abstract void printInfo();
}
Thêm các lớp con:
class Book extends LibraryItem {
private String author;
public Book(String title, String author) {
super(title);
this.author = author;
}
@Override
public void printInfo() {
System.out.println("Sách: " + title + ", tác giả: " + author);
}
}
class Magazine extends LibraryItem {
private int issueNumber;
public Magazine(String title, int issueNumber) {
super(title);
this.issueNumber = issueNumber;
}
@Override
public void printInfo() {
System.out.println("Tạp chí: " + title + ", số phát hành: " + issueNumber);
}
}
Sử dụng đa hình:
LibraryItem[] items = {
new Book("Mã sạch", "Robert C. Martin"),
new Magazine("Java World", 3)
};
for (LibraryItem item : items) {
item.printInfo();
}
Thêm interface cho ấn phẩm điện tử
Giả sử một số ấn phẩm có thể đọc online. Hãy tạo interface:
interface ReadableOnline {
void openOnline();
}
class EBook extends Book implements ReadableOnline {
private String url;
public EBook(String title, String author, String url) {
super(title, author);
this.url = url;
}
@Override
public void openOnline() {
System.out.println("Mở sách điện tử tại địa chỉ: " + url);
}
}
Giờ có thể làm việc với sách điện tử thông qua interface:
ReadableOnline ebook = new EBook("Java cho người mới bắt đầu", "Barry Burd", "https://example.com/java");
ebook.openOnline();
5. Cách tránh các vấn đề với đa hình và trừu tượng hóa: thực hành tốt nhất
- Dùng interface và lớp trừu tượng để mô tả hành vi, không phải trạng thái.
Ví dụ, interface Printable mô tả tốt khả năng “in”, nhưng việc giữ trường String title trong interface — là ý tưởng tệ. - Kiểm tra kiểu của đối tượng bằng instanceof trước khi ép kiểu.
Đặc biệt nếu đối tượng có thể thuộc nhiều kiểu khác nhau. Điều này giúp tránh ClassCastException. - Hướng tới hệ phân cấp “phẳng” và dễ hiểu.
Cây thừa kế càng đơn giản — càng dễ bảo trì và mở rộng. - Tránh tạo “trừu tượng vô nghĩa”.
Nếu lớp không chứa phương thức trừu tượng và không định dùng để kế thừa — đừng làm nó thành trừu tượng. - Luôn dùng annotation @Override khi ghi đè phương thức.
Giúp trình biên dịch bắt lỗi chữ ký phương thức.
6. Những lỗi thường gặp khi làm việc với đa hình và trừu tượng hóa
Lỗi số 1: Ép kiểu không kiểm tra
Đôi khi muốn “đi đường tắt” và ép kiểu không kiểm tra. Có thể sẽ chạy, nhưng cũng có thể làm chương trình sập bất ngờ. Luôn dùng instanceof:
if (item instanceof Book) {
Book book = (Book) item;
// ...
}
Lỗi số 2: Cố gọi phương thức của lớp con qua tham chiếu kiểu cơ sở
LibraryItem item = new Book("Java", "Tác giả");
item.getAuthor(); // Lỗi biên dịch: trong LibraryItem không có phương thức như vậy!
Giải pháp — hoặc ép kiểu, hoặc thêm phương thức cần thiết vào lớp cơ sở (nếu hợp lý).
Lỗi số 3: Hiện thực không đầy đủ interface hoặc lớp trừu tượng
Nếu quên hiện thực tất cả phương thức của interface — trình biên dịch sẽ không cho build dự án. Nhưng nếu hiện thực “stub” không làm gì, điều này có thể dẫn đến hành vi bất ngờ.
Lỗi số 4: Hệ thừa kế quá sâu
Nếu bạn có hơn ba cấp thừa kế — hãy cân nhắc xem có thể đơn giản hóa kiến trúc không.
Lỗi số 5: Vi phạm nguyên tắc trách nhiệm đơn
Nếu một trừu tượng mô tả quá nhiều trách nhiệm, nó trở nên khó bảo trì. Tốt hơn là tách thành nhiều interface hoặc lớp.
GO TO FULL VERSION