Không chỉ lớp, ta còn có thể khai báo các phương thức trừu tượng. Một lớp trừu tượng có nghĩa phải tạo lớp con cho nó; còn một phương thức trừu tượng có nghĩa rằng nó phải được cài đè.
Ta có thể quy định rằng một vài (hoặc tất cả) các hành vi của một lớp trừu tượng phải được cài đặt bởi một lớp con có tính đặc trưng hơn, nếu không các hành vi đó là vô nghĩa. Nói cách khác, ta không thể nghĩ ra một cài đặt tổng quát nào cho phương thức đó mà có thể hữu ích cho các lớp con. Một phương thức makeNoise()
tổng quát sẽ làm gì?
Cú pháp Java quy định rằng phương thức trừu tượng không có thân phương thức. Dòng khai báo phương thức kết thúc tại dấu chấm phảy và không có cặp ngoặc { }
.
public abstract void makeNoise();
Nếu ta khai báo một phương thức là abstract
, ta phải đánh dấu lớp đó cũng là abstract
. Ta không thể đặt một phương thức trừu tượng ở bên trong một lớp cụ thể. Tuy nhiên, ta có thể có phương thức không trừu tượng bên trong một lớp trừu tượng.
Các phương thức trừu tượng phải được cài đè tại một lớp con. Các phương thức trừu tượng không có nội dung, nó tồn tại chỉ để phục vụ cơ chế đa hình. Điều đó có nghĩa rằng lớp cụ thể đầu tiên nằm dưới nó trên cây phả hệ bắt buộc phải cài tất cả các phương thức trừu tượng; các lớp con trừu tượng có thể bỏ qua việc này.
Ví dụ, nếu cả Animal và Canine đều trừu tượng và cùng có các phương thức trừu tượng, lớp Canine không buộc phải cài các phương thức trừu tượng của Animal. Nhưng ngay khi ta đi xuống đến lớp con cụ thể đầu tiên, chẳng hạn Dog, lớp đó sẽ phải cài tất cả các phương thức trừu tượng thừa kế từ Animal và Canine.
Tuy nhiên, nhớ lại rằng một lớp trừu tượng có thể chứa cả các phương thức trừu tượng cũng như cụ thể, cho nên Canine chẳng hạn có thể cài một phương thức trừu tượng thừa kế từ Animal, dẫn tới Dog không phải làm việc này nữa. Còn nếu Canine không cài phương thức trừu tượng nào từ Animal, Dog sẽ phải cài tất cả các phương thức trừu tượng của Animal cũng nhưng những phương thức trừu tượng mà Canine bổ sung. Khi ta nói “cài đặt phương thức trừu tượng”, điều đó có nghĩa ta cài đè phương thức đó với một thân hàm để có một phiên bản cụ thể của phương thức đó (tất nhiên ở phiên bản mới không có từ khóa abstract trong khai báo).
Ví dụ về đa hình
Giả sử ta muốn viết một lớp danh sách để quản lí các đối tượng Dog mà không dùng đến các cấu trúc danh sách có sẵn trong thư viện Java. Bước đầu, ta chỉ cần một phương thức add() để đưa các đối tượng Dog vào danh sách. Ta dùng một mảng Dog đơn giản với kích thước 5 để lưu các đối tượng Dog được đưa vào danh sách. Khi trong danh sách đã đủ 5 đối tượng, ta vẫn có thể tiếp tục gọi phương thức add() nhưng nó sẽ không làm gì. Nếu chưa đủ 5, phương thức add() sẽ gắn đối tượng tiếp theo vào vị trí tiếp theo còn trống rồi tăng chỉ số của vị trí tiếp theo còn trống (nextIndex) thêm 1.
Nhưng nếu ta còn muốn quản lí cả mèo lẫn chó trong danh sách? Có một vài lựa chọn. Thứ nhất: viết thêm lớp MyCatList dành riêng cho các đối tượng Cat. Thứ hai: viết một lớp DogAndCatList chung, trong đó có hai mảng, một dành cho các đối tượng Dog, một dành cho các đối tượng Cat. Thứ ba: viết một lớp AnimalList trong đó có thể chấp nhận các đối tượng thuộc lớp con bất kì của Animal (phòng trường hợp đặc tả lại thay đổi để yêu cầu nhận thêm các loài vật khác). Lựa chọn thứ ba gọn gàng và có khả năng mở rộng cao nhất nên ta sẽ dùng cho phiên bản thứ hai. Ta sẽ sửa lớp MyDogList, tổng quát hóa nó để chấp nhận các lớp con bất kì của Animal thay vì chỉ Dog. Lô-gic chương trình vẫn giữ nguyên như cũ, chỉ có các thay đổi được đánh đậm trong đoạn mã dưới đây:
Kiểm thử bằng chương trình sau:
public class AnimalTestDrive { public static void main(String[] args) { AnimalList list = new AnimalList(); Dog dog = new Dog(); Cat cat = new Cat(); list.add(dog); list.add(cat); } }
Cho kết quả:
Animal added at 0 Animal added at 1
Ta lại lấy ví dụ Shape đã nói đến ở đầu chương. Lớp cha tổng quát Shape nên là lớp trừu tượng do ứng dụng không cần và không nên tạo đối tượng Shape. Ngoài ra, các phương thức draw và erase của lớp này cũng nên là phương thức trừu tượng do ta không thể nghĩ ra nội dung gì hữu ích cho chúng. Các lớp con cụ thể, Point, Circle, Rectangle, và các lớp mà sau này sẽ bổ sung vào thư viện khi cần, sẽ định nghĩa các phiên bản với nội dung riêng cụ thể phù hợp với chính mình. Chẳng hạn như ví dụ trong hình dưới đây.
Lớp Shape
:
abstract public class Shape { protected int x, y; protected Shape(int _x, int _y) { x = _x; y = _y; } abstract public void draw(); abstract public void erase(); public void moveTo(int _x, int _y) { erase(); x = _x; y = _y; draw(); } }
Lớp Circle
:
public class Circle extends Shape { private double radius; public Circle(int _x, int _y, double _r) { super(_x, _y); radius = _r; } public void draw() { System.out.println("Draw circle"); } public void erase() { System.out.println("Erase circle"); } }
Khác với draw và erase, moveTo lại là phương thức có thể định nghĩa ngay tại lớp Shape. Thuật toán ba bước cho moveTo là như nhau cho mọi hình: (1) xóa tại vị trí hiện hành, (2) sửa tọa độ hình, (3) vẽ tại vị trí mới, mặc dù xóa như thế nào và vẽ như thế nào là tùy theo từng loại hình cụ thể. Hiệu ứng đa hình cho phép moveTo dùng đến các phiên bản draw và erase khác nhau tùy theo nó được gọi cho đối tượng thuộc loại hình nào. Khi thư viện được bổ sung thêm các lớp đặc tả các loại hình khác, ta chỉ phải cài draw và erase cho loại hình đó mà không phải làm thêm gì cho các phương thức biến đổi hình có quy trình chung đã được định nghĩa sẵn tương tự như moveTo.
Ví dụ này cũng minh họa một mẫu thiết kế có tên Template Method (phương thức khuôn mẫu). Xem hình dưới đây. Ở đây, Shape là lớp trừu tượng (AbstractClass) định nghĩa một phương thức khuôn mẫu moveTo, và quy định hai thao tác cơ bản (PrimitiveOperation) là erase và draw mà phương thức khuôn mẫu dùng đến. Circle là lớp con cụ thể (ConcreteClass), nó cài đặt các thao tác cơ bản này. Đây là một trong những mẫu thiết kế thông dụng nhất.