
Chào các "dev non" tương lai, hôm nay anh Creyt sẽ cùng các em "phá đảo" một trong những keyword mà nhiều bạn mới học Java cứ thấy nó là… muốn "tắt app" ngay lập tức: abstract. Nghe có vẻ "trừu tượng" thật, nhưng tin anh đi, sau buổi này các em sẽ thấy nó "dễ như ăn kẹo"!
1. abstract là gì mà Gen Z hay "ngáo ngơ"?
"Trừu tượng" trong lập trình, đặc biệt là trong Java OOP, không phải là cái gì đó khó hiểu hay mơ hồ đâu mấy đứa. Hãy hình dung thế này:
abstract class (Lớp trừu tượng): Nó giống như một bản thiết kế nhà nhưng chưa hoàn thiện. Bản thiết kế đó có thể có sẵn một số phòng ốc, cửa nẻo (các phương thức và thuộc tính bình thường), nhưng lại có những phần quan trọng chưa được vẽ xong (các phương thức trừu tượng). Vì nó chưa hoàn thiện, nên em không thể xây một căn nhà từ bản thiết kế này trực tiếp được. Em phải lấy bản thiết kế này, thêm thắt chi tiết vào những chỗ còn thiếu, rồi mới xây được nhà.
abstract method (Phương thức trừu tượng): Đây chính là những phần chưa được vẽ xong trong bản thiết kế đó. Nó chỉ là một cái tên phương thức, một chữ ký, không có phần "thân" (không có code bên trong). Nó như một lời hứa vậy: "Ê, đứa nào kế thừa tao, phải tự định nghĩa cái hành động này nha!".
Tóm lại: abstract sinh ra để:
- Định nghĩa một khuôn mẫu chung: Tạo ra một cấu trúc, một "hợp đồng" mà các lớp con phải tuân theo.
- Ép buộc triển khai: Bắt buộc các lớp con phải "hoàn thiện" những phần còn thiếu.
- Không cho phép tạo đối tượng trực tiếp: Vì bản thân lớp
abstractchưa hoàn chỉnh, nên không thể tạo ra "thực thể" từ nó.
2. Code Ví Dụ Minh Họa: "Thực chiến" ngay!
Giờ thì mình cùng "code dạo" một chút để hiểu rõ hơn nha. Anh Creyt sẽ lấy ví dụ về một hệ thống quản lý các loại phương tiện (xe cộ).
// Bước 1: Định nghĩa một abstract class 'Vehicle'
// Đây là bản thiết kế chung cho mọi loại xe, nhưng chưa hoàn chỉnh
abstract class Vehicle {
String brand;
int year;
// Constructor bình thường, abstract class vẫn có thể có constructor
public Vehicle(String brand, int year) {
this.brand = brand;
this.year = year;
System.out.println("Khởi tạo một Vehicle của hãng " + brand + " năm " + year);
}
// Một phương thức bình thường, có thân
public void displayInfo() {
System.out.println("Thương hiệu: " + brand + ", Năm sản xuất: " + year);
}
// Một phương thức trừu tượng: 'startEngine()'
// Mọi loại xe đều phải có cách khởi động, nhưng mỗi xe một kiểu
// Nên ở đây ta chỉ định nghĩa là 'phải có', chứ không nói 'làm thế nào'
public abstract void startEngine();
// Một phương thức trừu tượng khác: 'stopEngine()'
public abstract void stopEngine();
}
// Bước 2: Tạo các lớp con kế thừa 'Vehicle'
// Các lớp con này phải 'hoàn thiện' những phần còn thiếu của 'Vehicle'
class Car extends Vehicle {
public Car(String brand, int year) {
super(brand, year);
System.out.println("-> Là một chiếc Car.");
}
@Override
public void startEngine() {
System.out.println("Xe hơi " + brand + " khởi động bằng chìa khóa/nút bấm.");
}
@Override
public void stopEngine() {
System.out.println("Xe hơi " + brand + " tắt máy bằng cách xoay chìa/bấm nút.");
}
}
class Motorcycle extends Vehicle {
public Motorcycle(String brand, int year) {
super(brand, year);
System.out.println("-> Là một chiếc Motorcycle.");
}
@Override
public void startEngine() {
System.out.println("Xe máy " + brand + " khởi động bằng nút đề hoặc đạp.");
}
@Override
public void stopEngine() {
System.out.println("Xe máy " + brand + " tắt máy bằng cách gạt công tắc.");
}
}
// Bước 3: Thử nghiệm trong lớp Main
public class AbstractDemo {
public static void main(String[] args) {
// KHÔNG THỂ tạo đối tượng từ abstract class trực tiếp!
// Uncomment dòng dưới và xem lỗi:
// Vehicle genericVehicle = new Vehicle("Generic", 2020); // Lỗi compile time!
Car myCar = new Car("Toyota", 2022);
myCar.displayInfo();
myCar.startEngine();
myCar.stopEngine();
System.out.println("\n");
Motorcycle myBike = new Motorcycle("Honda", 2023);
myBike.displayInfo();
myBike.startEngine();
myBike.stopEngine();
System.out.println("\n");
// Polymorphism (tính đa hình) vẫn hoạt động ngon lành!
Vehicle[] vehicles = new Vehicle[2];
vehicles[0] = new Car("Ford", 2021);
vehicles[1] = new Motorcycle("Yamaha", 2024);
System.out.println("--- Các phương tiện trong danh sách ---");
for (Vehicle v : vehicles) {
v.displayInfo();
v.startEngine();
System.out.println("------------------");
}
}
}
Output khi chạy:
Khởi tạo một Vehicle của hãng Toyota năm 2022
-> Là một chiếc Car.
Thương hiệu: Toyota, Năm sản xuất: 2022
Xe hơi Toyota khởi động bằng chìa khóa/nút bấm.
Xe hơi Toyota tắt máy bằng cách xoay chìa/bấm nút.
Khởi tạo một Vehicle của hãng Honda năm 2023
-> Là một chiếc Motorcycle.
Thương hiệu: Honda, Năm sản xuất: 2023
Xe máy Honda khởi động bằng nút đề hoặc đạp.
Xe máy Honda tắt máy bằng cách gạt công tắc.
Khởi tạo một Vehicle của hãng Ford năm 2021
-> Là một chiếc Car.
Khởi tạo một Vehicle của hãng Yamaha năm 2024
-> Là một chiếc Motorcycle.
--- Các phương tiện trong danh sách ---
Thương hiệu: Ford, Năm sản xuất: 2021
Xe hơi Ford khởi động bằng chìa khóa/nút bấm.
------------------
Thương hiệu: Yamaha, Năm sản xuất: 2024
Xe máy Yamaha khởi động bằng nút đề hoặc đạp.
------------------
Thấy chưa? Lớp Vehicle là abstract nên không tạo đối tượng trực tiếp được, nhưng nó lại là một "khuôn mẫu" tuyệt vời để các lớp con như Car và Motorcycle kế thừa và "hoàn thiện" hành vi khởi động/tắt máy của riêng mình. Ngon lành cành đào!

3. Mẹo (Best Practices) để "Nhớ dai" và "Dùng đúng"
- "Ông bố chưa sẵn sàng có con": Hãy nhớ, lớp
abstractlà một "ông bố" chưa hoàn chỉnh, chưa "sẵn sàng" để tự mình "sinh ra" một đứa con (object). Nó cần các "bà mẹ" (lớp con concrete) để "hoàn thiện" và sinh ra những "đứa con" thực sự. - "Hợp đồng bắt buộc": Khi em khai báo một phương thức là
abstract, nó giống như một điều khoản bắt buộc trong hợp đồng. Đứa nào ký (kế thừa) hợp đồng này, phải thực hiện điều khoản đó. Nếu không, compiler sẽ "đấm" cho một trận. abstract classvs.interface: Đừng nhầm lẫn!abstract class: Có thể có cả phương thứcabstractvàconcrete(có thân). Có thể có thuộc tính, constructor. Kế thừa mộtabstract classthì dùngextends.interface: Từ Java 8 trở về trước, chỉ có phương thứcabstract(ngầm định). Từ Java 8 trở đi có thể códefaultvàstaticmethods. Không có constructor. Triển khaiinterfacethì dùngimplements.- Mẹo nhớ:
abstract classlà một cái gì đó nhưng chưa hoàn thiện (is-a relationship, nhưng còn thiếu).interfacecó thể làm cái gì đó (can-do relationship).
- Chỉ
abstractkhi thực sự cần: Đừng lạm dụng. Nếu một lớp đã đủ chi tiết để tạo đối tượng, đừng biến nó thànhabstractchỉ vì "nghe có vẻ pro". Mục đích chính là để tạo ra một cấu trúc chung và buộc các lớp con phải tuân thủ.
4. Ứng dụng thực tế: "Abstract" ở đâu trong cuộc sống số?
"Abstract" không chỉ nằm trong sách vở đâu các em, nó len lỏi khắp nơi trong các ứng dụng và framework mà chúng ta dùng hàng ngày:
- Java Collections Framework: Các lớp như
java.util.AbstractList,AbstractSet,AbstractMaplà những ví dụ điển hình. Chúng cung cấp các triển khai cơ bản cho các phương thức chung của List, Set, Map, để các lớp con cụ thể (nhưArrayList,HashSet) chỉ cần tập trung vào những logic riêng biệt của mình mà không cần viết lại từ đầu. - Frameworks lớn (Spring, Hibernate): Rất nhiều framework sử dụng
abstract classđể tạo ra các điểm mở rộng (extension points). Ví dụ, bạn muốn tạo một bộ lọc (filter) tùy chỉnh trong Spring Security, bạn có thể kế thừa từ mộtabstract classnào đó, và framework sẽ yêu cầu bạn triển khai một số phương thức cụ thể để nó biết cách xử lý bộ lọc của bạn. - Hệ thống xử lý tài liệu: Tưởng tượng bạn có một lớp
AbstractDocumentvới phương thứcabstract render(). Các lớp con nhưPdfDocument,WordDocument,HtmlDocumentsẽ triển khai phương thứcrender()theo cách riêng của từng định dạng.
5. Thử nghiệm đã từng và hướng dẫn nên dùng cho case nào
Anh Creyt đã từng "nghiên cứu" và áp dụng abstract trong rất nhiều dự án, và đây là kinh nghiệm xương máu:
-
Nên dùng khi:
- Bạn muốn định nghĩa một "kiểu" đối tượng chung nhưng không muốn tạo đối tượng trực tiếp từ nó. Ví dụ, "Động vật" là một khái niệm chung, nhưng bạn không thể tạo ra một "con động vật" chung chung được, bạn phải tạo ra "con chó", "con mèo". Lúc này,
Animalnên làabstract class. - Bạn muốn các lớp con phải có một hành vi cụ thể nào đó, nhưng cách thực hiện hành vi đó lại khác nhau. Như ví dụ
startEngine()ở trên, mọi xe đều khởi động, nhưng cách khởi động khác nhau. - Bạn muốn cung cấp một số triển khai mặc định (concrete methods) cùng với các phương thức trừu tượng. Điều này giúp tái sử dụng code tốt hơn so với
interface(trước Java 8).
- Bạn muốn định nghĩa một "kiểu" đối tượng chung nhưng không muốn tạo đối tượng trực tiếp từ nó. Ví dụ, "Động vật" là một khái niệm chung, nhưng bạn không thể tạo ra một "con động vật" chung chung được, bạn phải tạo ra "con chó", "con mèo". Lúc này,
-
Không nên dùng khi:
- Bạn chỉ muốn định nghĩa một "hợp đồng" thuần túy, không có bất kỳ triển khai nào. Lúc này,
interfacelà lựa chọn tốt hơn, vì nó tập trung hoàn toàn vào việc định nghĩa hành vi mà không dính dáng gì đến trạng thái (thuộc tính) hay triển khai mặc định. - Lớp của bạn đã đủ "hoàn chỉnh" và có thể tạo đối tượng trực tiếp. Đừng biến nó thành
abstractnếu không có lý do chính đáng.
- Bạn chỉ muốn định nghĩa một "hợp đồng" thuần túy, không có bất kỳ triển khai nào. Lúc này,
Vậy đó, abstract không hề "trừu tượng" chút nào nếu chúng ta hiểu đúng bản chất của nó. Nó là một công cụ mạnh mẽ trong OOP giúp chúng ta xây dựng các hệ thống linh hoạt, dễ mở rộng và có cấu trúc rõ ràng. Hãy thực hành nhiều vào, rồi các em sẽ thấy nó "ngấm" lúc nào không hay!
Chúc các em "code trâu" và "debug ít"! Hẹn gặp lại trong bài học tiếp theo của anh Creyt!
Thuộc Series: Java – OOP
Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!