Java - ĐH Công Nghệ - 29

Đối với các lớp tự viết, ta có thể cần định nghĩa một phương thức equals() trong các lớp đó để có được hành vi đúng khi đối tượng thuộc các lớp đó được so sánh với nhau. Nếu equals không hoạt động đúng thì các phương thức của Collection như remove hay contains cũng không hoạt động như mong đợi.

Ta lấy một ví dụ. Hai quân bài được coi là giống nhau nếu giống nhau về giá trị (value: Át, 2, 3,.. J, Q, K) và cùng chất (suit: cơ, rô, pic, tép). Mã hóa Át, 2,..., J, Q, K thành các giá trị nguyên từ 1 đến 13, bốn chất cơ, rô, pic, tép thành các giá trị từ 0 đến 3.



Hình 13.7: Phương thức equals.


Ta có cài đặt đơn giản của lớp Card với phương thức equals như trong Hình

13.7. Do là phiên bản cài đè phương thức của Object nên kiểu tham số của equals phải giữ nguyên như bản cũ là Object.

Nếu ta sử dụng các cấu trúc tập hợp (kiểu Set), ta còn cần phải cài thêm một phương thức khác, đó là hashCode(), một trong các phương thức được thừa kế từ Object với hành vi mặc định của phiên bản thừa kế từ Object là cho mỗi đối tượng một giá trị băm khác nhau. Khi cần kiểm tra xem hai đối tượng có trùng nhau hay không, một cấu trúc HashSet sẽ gọi đến phương thức hashCode() của hai đối tượng để lấy giá trị băm của chúng. Nếu hai đối tượng có giá trị băm khác nhau, HashSet sẽ khẳng định chúng là hai đối tượng khác nhau. Còn nếu giá trị băm trùng nhau (dữ liệu khác nhau có thể có giá trị băm trùng nhau), HashSet sẽ dùng đến phương thức equals() để kiểm tra tiếp xem hai đối tượng có thực sự bằng nhau hay không.

Do đó, ta cần cài đè hashCode() để hai đối tượng bằng nhau sẽ cho giá trị băm trùng nhau, nhờ đó qua được bước kiểm tra đầu tiên.

Có thể bạn quan tâm!

Xem toàn bộ 251 trang tài liệu này.



Java - ĐH Công Nghệ - 29

Hình 13.8: Cài đè equals() và hashCode().


Ta lấy ví dụ với lớp Contact - địa chỉ liên lạc. Giả sử, ta quy ước hai Contact được cho là của một người nếu có trường name (tên) trùng nhau. Khi đó, có thể cài đè hai phương thức equals() và hashCode() như trong Hình 13.8, trong đó ta tận dụng các phiên bản sẵn có của equals() và hashCode() cho lớp String.


13.5.2. So sánh lớn hơn/nhỏ hơn

Tương tự với so sánh bằng là vấn đề so sánh lớn hơn, nhỏ hơn. Giả sử ta cần một cấu trúc contactList là danh sách các địa chỉ liên lạc – lớp Contact như đã cài ở mục trước, và đôi khi ta cần danh sách đó được sắp xếp theo tên. Có một số cách để làm việc này với các lớp có sẵn trong Collection framework. Ta có thể dùng phương thức Collections.sort() đối với danh bạ ở dạng một đối tượng List, hoặc dùng một cấu trúc tự động sắp xếp chẳng hạn như TreeSet để lưu danh bạ. Cả hai cách đều cần phải so sánh hai đối tượng Contact để biết đối tượng nào "lớn hơn" hay "nhỏ hơn".


Hình 13.9: Lỗi run-time khi sử dụng TreeSet cho Contact.


Tương tự như tình huống so sánh bằng, TreeSet, hay Collections không thể tự biết cách so sánh các đối tượng thuộc các lớp mà lập trình viên tự xây dựng. Chương trình như trong Hình 13.9 biên dịch không có lỗi do add() không yêu cầu tham số kiểu Comparable, nhưng khi chạy thì gặp lỗi run-time đối với lệnh đầu tiên gọi đến phương thức đó.

Tóm lại, các phần tử của cấu trúc danh bạ phải thuộc lớp đối tượng có cung cấp phương tiện so sánh.

Ta có thể chọn một trong hai cách sau để giải quyết vấn đề đó:

1. Các phần tử danh sách phải thuộc một lớp có cài interface Comparable. Ta sửa lớp Contact để bổ sung phần in đậm trong Hình 13.10, chương trình trong Hình 13.9, sau đó sẽ chạy không có lỗi.


Hình 13.10: Cài interface Comparable.


2. Sử dụng phương thức chồng có lấy tham số kiểu Comparator. Ta viết thêm lớp ContactCompare theo interface Comparator và dùng nó trong chương trình TestTreeSet như những dòng in đậm trong Hình 13.11. Theo đó, ContactCompare là một loại Comparator được thửa riêng dành cho việc so sánh các đối tượng Contact. Còn danh bạ là đối tượng TreeSet được tạo kèm với loại Comparator đặc biệt đó để

nó biết cách đối xử với các phần tử trong danh bạ (cContact là đối số khi gọi hàm khởi tạo TreeSet).


Hình 13.11: Sử dụng Comparator.


Cả hai cách trên đều áp dụng được cho phương thức sort() của Collection cũng như các tiện ích tổng quát tương tự trong thư viện Java.


13.6. KÍ TỰ ĐẠI DIỆN TRONG KHAI BÁO THAM SỐ KIỂU


Quan hệ thừa kế giữa hai lớp không có ảnh hưởng gì đến quan hệ giữa các cấu trúc tổng quát dùng cho hai lớp đó. Chẳng hạn, Dog và Cat là các lớp con của Animal, ta có thể đưa các đối tượng Dog và Cat vào một ArrayList<Animal>, và tính chất đa hình giữa Dog, Cat, và Animal vẫn hoạt động như bình thường (xem ví dụ trong Hình 13.12). Tuy nhiên, ArrayList<Dog>, ArrayList<Cat> lại không có quan hệ gì với ArrayList<Animal>. Vậy cho nên, nếu dùng một ArrayList<Dog> làm đối số cho phương thức yêu cầu đối số kiểu ArrayList<Animal>, như ví dụ trong Hình 13.13, trình biên dịch sẽ báo lỗi sai kiểu dữ liệu.


Hình 13.12: Đa hình bên trong mỗi cấu trúc tổng quát.


Hình 13.13: Không có đa hình giữa các cấu trúc tổng quát.


Tóm lại, nếu ta khai báo một phương thức lấy đối số kiểu ArrayList<Animal>, nó sẽ chỉ có thể lấy đối số kiểu ArrayList<Animal> chứ không thể lấy kiểu ArrayList<Dog> hay ArrayList<Cat>.

Ta không hài lòng với lắm với việc thỏa hiệp, nghĩa là dùng ArrayList<Animal> thay vì ArrayList<Dog> cho danh sách chỉ được chứa toàn Dog. Vì nếu vậy trình biên dịch sẽ không kiểm tra kiểu dữ liệu để ngăn chặn những tình huống chẳng hạn như trong danh sách chó nghiệp vụ của lính cứu hỏa lại có một con mèo.


Hình 13.14: Nguy cơ cho mèo vào danh sách chó.


Vậy làm thế nào để làm cho một phương thức có thể nhận đối số thuộc kiểu ArrayList<Dog>, ArrayList<Cat>,nghĩa là ArrayList dành cho kiểu bất kì là lớp con của Animal? Giải pháp là sử dụng kí tự đại diện (wildcard).

Ta sửa phương thức makeASymphony() như sau, và chương trình trong Hình

13.13 sẽ chạy được và chạy đúng.


? extends Animal có nghĩa là kiểu gì đó thuộc loại Animal. Nhớ rằng từ khóa extends ở đây có nghĩa "là lớp con của" hoặc "cài đặt", tùy vào việc theo sau từ khóa extends là tên một lớp hay tên một interface. Vậy nên nếu muốn makeASymphony() lấy đối số là một ArrayList của loại nào cài interface Pet, ta khai báo nó như sau:


Nhưng ArrayList<? extends Animal> thì khác gì với ArrayList<Animal>? makeASymphony() thì an toàn vì nó không thêm/sửa danh sách mà tham số a chiếu tới. Nhưng liệu có tránh được chuyện cho mèo vào danh sách chó ở một phương thức khác hay không? Câu trả lời là Có.

Khi ta dùng kí tự đại diện <?> tại khai báo, trình biên dịch sẽ không cho ta thêm cái gì vào trong danh sách mà tham số của phương thức chiếu tới. Ta có thể gọi phương thức của các phần tử trong danh sách, nhưng ta không thể thêm phần tử mới vào danh sách. Do đó, ta có thể yên tâm khi chương trình chạy. Ví dụ, makeASymphony() với nội dung ở trên thì không gặp lỗi biên dịch, nhưng takeAnimals() với nội dung như trong Hình 13.14 sẽ không biên dịch được.


Hai cú pháp sau là tương đương:

public void foo( ArrayList<? extends Animal> a) public <T extends Animal> void foo( ArrayList<T> a)

Cách thứ hai, dùng "T", thường được sử dụng khi ta còn muốn T xuất hiện ở các vị trí khác. Ví dụ, cách viết sau quá dài:

public void bar( ArrayList<? extends Animal> a1, ArrayList<? extends Animal> a2)

thay vào đó, ta viết:

public <T extends Animal> void bar(ArrayList<T> a1 , ArrayList<T> a2)

Bài tập

1. Các phát biểu dưới đây đúng hay sai? nếu sai, hãy giải thích.

a) Một phương thức generic không thể trùng tên với một phương thức không generic.

b) Có thể chồng một phương thức generic bằng một phương thức generic khác trùng tên nhưng khác danh sách tham số

c) Một tham số kiểu có thể được khai báo đúng một lần tại phần tham số kiểu nhưng có thể xuất hiện nhiều lần tại danh sách tham số của phương thức generic

d) Các tham số kiểu của các phương thức generic khác nhau phải không được trùng nhau.

2. Trong các dòng khai báo sau đây, dòng nào có lỗi biên dịch?


3. Viết một phương thức generic sumArray với tham số là một mảng gồm các phần tử thuộc một kiểu tổng quát, phương thức này tính tổng các phần tử của mảng rồi trả về kết quả bằng lệnh return.

Viết một đoạn code ngắn minh họa cách sử dụng hàm sumArray

Ngày đăng: 24/12/2023