Thêm một bước nữa, nếu ta muốn có danh sách lưu được cả những đối tượng không phải động vật thì sao? Ta có thể tiếp tục thay đổi theo kiểu sửa kiểu mảng, kiểu đối số phương thức add() thành cái gì đó tổng quát hơn và trừu tượng hơn Animal? Nhưng ta không viết lớp cha cho Animal.

Thực ra Animal đã có lớp cha. Đối với Java, tất cả các lớp đều là lớp con của lớp Object. Object là tổ tiên của tất cả. Ngay từ đầu, ta đã viết các lớp con của Object mà không biết, ta viết lớp con của Object mà không cần phải khai báo quan hệ thừa kế đó bằng từ khóa extends.

Bất kì lớp nào không được khai báo tường minh là lớp con của một lớp khác thì đều được khai báo ẩn là lớp con của Object. Vậy nên, ta có Dog không phải là lớp con trực tiếp của Object, còn Animal là lớp con trực tiếp của Object, và tất cả Dog, Cat, Canine, Animal… đều nằm trong cây phả hệ có gốc là Object.

Với tất cả các lớp đều nằm trong cây thừa kế có Object tại gốc, cơ chế đa hình cho phép ta tạo các cấu trúc dữ liệu dành cho đối tượng thuộc tất cả các lớp. Chẳng hạn một mảng kiểu Object có thể lưu đối tượng thuộc đủ loại Animal, Cow, Dog, Cat, PhoneBook, String… Trong thư viện chuẩn của Java có lớp ArrayList được định nghĩa để quản lý các đối tượng thuộc kiểu Object. ArrayList có thể dùng để quản lý đối tượng thuộc tất cả các kiểu.

Lớp Object cho các lớp khác thừa kế những gì? Trong các phương thức được thừa kế của Object có bốn phương thức thông dụng:

  • boolean equals(Object o) kiểm tra xem hai đối tượng hiện hành có ‘bằng nhau’ hay không, xem thêm về ý nghĩa của khái niệm ‘bằng nhau’ này tại Chương 13.Cow cow1 = new Cow(); Cow cow2 = new Cow(); System.out.println(cow1.equals(cow2)); // false
  • Class getClass() trả về lớp mà đối tượng hiện hành đã được tạo từ đó,dog = new Dog(); System.out.println(dog.getClass()); // class Dog
  • int hashCode() trả về mã băm của đối tượng hiện hành, ta tạm thời xem mã này như là một định danh của đối tượng,cat = new Cat(); System.out.println(cat.hashCode()); // 901506536
  • String toString() trả về biểu diễn dạng String của đối tượng, ta thường cài đè phương thức này để trả về biểu diễn String theo ý muốn của ta thay vì trả về chuỗi kí tự được kết xuất một cách tổng quát như ví dụ bên dưới.cat = new Cat(); System.out.println(cat.toString()); // Cat@35bbe5e8

Đổi kiểu – khi đối tượng mất hành vi của mình

Rắc rối của việc dùng cơ chế đa hình coi mọi thứ như là một Object hay coi các đối tượng động vật như là một Animal là đôi khi các đối tượng có vẻ như đánh mất (tạm thời) các đặc trưng của mình. Dog có vẻ mất các đặc điểm của chó. Ta hãy xem chuyện gì xảy ra khi một phương thức trả về một tham chiếu tới một đối tượng Dog nhưng khai báo kiểu trả về là Animal.

Nhớ lại lớp AnimalList ta đã tạo để quản lý danh sách các con vật. Giả sử AnimalList đã có thêm phương thức get(int index) trả về tham chiếu tới đối tượng đứng tại vị trí index trong danh sách.

Ta thử nghiệm bằng chương trình DogTestDrive, trong đó một đối tượng Dog được tạo và đưa vào một danh sách AnimalList. Sau đó ta gọi phương thức get() của danh sách đó để lấy lại chính đối tượng vừa đưa vào.

Để ý rằng phương thức get() gọi từ list trả về một tham chiếu tới chính đối tượng Dog nói trên, nhưng dưới dạng một tham chiếu kiểu Animal. Việc này hoàn toàn hợp lệ. Nhưng trình biên dịch không biết rằng thứ được trả về từ đó thực chất đang chiếu tới một đối tượng Dog, cho nên nó không cho phép ta gán giá trị trả về đó cho một tham chiếu kiểu Dog.

Nếu ta gán giá trị đó cho một tham số kiểu Animal, chẳng hạn, Animal a = list.get(0), thì trình biên dịch sẽ không phàn nàn gì. Tuy nhiên, khi đó ta sẽ chỉ có thể gọi các phương thức mà Dog thừa kế từ Animal, chẳng hạn roam(), chứ không thể gọi phương thức mà chỉ Dog mới có, như chaseCats() chẳng hạn.

Ngay cả khi ta biết chắc chắn đối tượng có hành vi chaseCats (nó thực sự là một đối tượng Dog!), trình biên dịch chỉ nhìn thấy nó như là một thứ kiểu Animal, mà Animal thì không có chaseCats().

Vấn đề ở đây giống như ta đã nói đến ở bài trước. Để xác định xem ta có thể gọi một phương thức nào đó hay không, trình biên dịch dựa trên kiểu tham chiếu chứ không dựa trên kiểu đối tượng thực tế.

Vậy cơ chế thừa kế có bản chất như thế nào?

Mỗi đối tượng chứa tất cả những gì nó thừa kế từ tất cả các lớp cha, ông, tổ tiên của nó, trong đó có cả lớp Object. Vậy nên nó có thể được coi là một thực thể của mỗi lớp cha ông đó. Lấy ví dụ lớp Cow đơn giản. Một đối t ượng Cow có thể được đối xử không chỉ như một đối tượng Cow, nó còn có thể được xem như một Object. Khi ta gọi new Cow(), ta được một đối tượng tại heap – một đối tượng Cow – nhưng đối tượng đó có một cái lõi là phần Object (chữ cái O viết hoa) của nó. Một tham chiếu kiểu Cow tới đối tượng này có thể ‘nhìn thấy’ toàn bộ đối tượng Cow, do đó có thể truy nhập toàn bộ các phương thức của Cow, bao gồm cả các phương thức được thừa kế. Trong khi đó, một tham chiếu kiểu Object chiếu tới cùng một đối tượng chỉ có thể ‘nhìn thấy’ phần Object của đối tượng đó, do đó chỉ có thể truy cập phần đó.

Như vậy ta đã giải thích được tại sao khi dùng một tham chiếu kiểu lớp cha cho đối tượng thuộc lớp con thì lớp con có vẻ như mất bản sắc riêng.

Nhưng ta vẫn chưa giải quyết xong vấn đề của chương trình DogTestDrive. Đối tượng mà ta lấy ra từ danh sách list thực sự là Dog, vậy làm cách nào để gọi được phương thức của Dog? Ta phải dùng một tham chiếu được khai báo kiểu Dog. Sao chép tham chiếu kiểu Animal mà ta đang có và ép sang kiểu Dog để ghi vào một tham chiếu kiểu Dog. Sau đó, ta có thể dùng tham chiếu Dog để gọi phương thức của Dog như bình thường.

public class DogTestDrive {
    public static void main(String[] args) {
        AnimalList animalList = new AnimalList();
        Dog dog = new Dog();
        animalList.add(dog);

        Animal animal = animalList.get(0);
        Dog dog2 = (Dog) animal; // <--- ép kiểu từ Object trở về kiểu Dog
        dog2.roam();
        dog2.chaseCat();
    }
}

Nếu hành động ép kiểu của ta là sai, nghĩa là đối tượng đang quan tâm thực ra không phải kiểu Dog, thì khi chạy, chương trình của ta sẽ bị ngắt giữa chừng do lỗi run-time ClassCastException. Do đó, trong những trường hợp mà ta không chắc chắn về kiểu của đối tượng, ta có thể dùng toán tử instanceof để kiểm tra.

if (animal instanceof Dog) {
    Dog dog2 = (Dog) animal; // <--- ép kiểu từ Object trở về kiểu Dog
}

Bài viết liên quan

Leave a Reply

Your email address will not be published.