Giáo trình Lập trình hướng đối tượng với Java (Phần 2)

Hai nguyên lý thừa kế và đa hình của lập trình hướng đối tượng giúp ta có thể

xây dựng chương trình một cách nhanh chóng và hiệu quả hơn, thu được kết quả là

những mô-đun chương trình mà các lập trình viên khác dễ mở rộng hơn, có khả

năng đáp ứng tốt hơn đối với sự thay đổi liên tục của các yêu cầu của khách hàng.

7.1. QUAN HỆ THỪA KẾ

Nhớ lại ví dụ đầu tiên về lập trình hướng đối tượng tại Ch-¬ng 1. Trong đó, Dậu

xây dựng 4 lớp: Square (hình vuông), Circle (đường tròn), Triangle (hình tam giác),

và Amoeba (hình trùng biến hình). Cả bốn đều là các hình với hai phương thức

rotate() và playSound(). Do đó, anh ta dùng tư duy trừu tượng hóa để tách ra các

đặc điểm chung và đưa chúng vào một lớp mới có tên Shape (hình nói chung). Sau

đó, kết nối các lớp hình vẽ kia với lớp Shape bởi một quan hệ gọi là thừa kế.

Ta nói rằng "Square thừa kế từ Shape", "Circle thừa kế từ Shape", v.v. Ta tháo

gỡ rotate() và playSound ra khỏi 4 loại hình, và giờ thì chỉ còn phải quản lý một bản

đặt tại lớp Shape. Shape được gọi là lớp cha (superclass) hay lớp cơ sở (base class) của

bốn lớp kia. Còn bốn lớp đó là các lớp con (subclass) hay lớp dẫn xuất (derived class)

của lớp Shape. Các lớp con thừa kế các phương thức của lớp cha. Nói cách khác, nếu

lớp Shape có chức năng gì thì các lớp con của nó tự động có các chức năng đó.

Vậy thế nào là quan hệ thừa kế? Nếu ta cần xây dựng các lớp đại diện cho hai

loài mèo nhà và hổ, mèo nhà nên thừa kế từ hổ, hay hổ nên thừa kế từ mèo, hay cả

hai cùng thừa kế từ một lớp thứ ba?104

Khi ta dùng quan hệ thừa kế trong thiết kế, ta đặt các phần mã dùng chung tại

một lớp và coi đó là lớp cha – lớp dùng chung trừu tượng hơn, các lớp cụ thể hơn là

các lớp con. Các lớp con được thừa kế từ lớp cha đó. Quan hệ thừa kế có nghĩa rằng

lớp con được thừa hưởng các thành viên (member) của lớp cha. Thành viên của một

lớp là các biến thực thể và phương thức của lớp đó. Ví dụ, Shape trong ví dụ trên có

hai thành viên rotate() và playSound(), Cow trong Hình 5.6 có các thành viên name,

age, getName(), getAge(), setName(), setAge().

Ta còn nói rằng lớp con chuyên biệt hóa (specialize) lớp cha. Nghĩa của "chuyên

biệt hóa" ở đây gồm có hai phần: (1) lớp con là một loại con của lớp cha – thể hiện ở

chỗ lớp con tự động thừa hưởng các thành viên của lớp cha, (2) lớp con có những

đặc điểm của riêng nó - thể hiện ở chỗ lớp con có thể bổ sung các phương thức và

biến thực thể mới của riêng mình, và nó có thể cài đè (override) các phương thức thừa

kế từ lớp cha. Ví dụ, hình trùng biến hình (Amoeba) cũng là một hình (Shape), do đó

lớp con Amoeba có tất cả những gì mà Shape có. Ngoài ra, Amoeba có thêm những

đặc điểm riêng của thể loại hình trùng biến hình: các biến thực thể đại diện cho tâm

xoay để phục vụ cách xoay của riêng nó, và nó định nghĩa lại các phương thức rotate

để xoay theo cách riêng, định nghĩa lại playSound để chơi loại âm thanh riêng. Theo

thuật ngữ, và cũng là từ khóa, của Java, lớp con "nối dài" (extends) lớp cha.

Các biến thực thể không bị cài đè vì việc đó là không cần thiết. Biến thực thể

không quy định một hành vi đặc biệt nào và lớp con chỉ việc gán giá trị tùy chọn cho

biến được thừa kế.

pdf 139 trang yennguyen 2401
Bạn đang xem 20 trang mẫu của tài liệu "Giáo trình Lập trình hướng đối tượng với Java (Phần 2)", để tải tài liệu gốc về máy hãy click vào nút Download ở trên

Tóm tắt nội dung tài liệu: Giáo trình Lập trình hướng đối tượng với Java (Phần 2)

Giáo trình Lập trình hướng đối tượng với Java (Phần 2)
 103
Ch−¬ng 7. Thõa kÕ vµ ®a h×nh 
Hai nguyên lý thừa kế và đa hình của lập trình hướng đối tượng giúp ta có thể 
xây dựng chương trình một cách nhanh chóng và hiệu quả hơn, thu được kết quả là 
những mô-đun chương trình mà các lập trình viên khác dễ mở rộng hơn, có khả 
năng đáp ứng tốt hơn đối với sự thay đổi liên tục của các yêu cầu của khách hàng. 
7.1. QUAN HỆ THỪA KẾ 
Nhớ lại ví dụ đầu tiên về lập trình hướng đối tượng tại Ch-¬ng 1. Trong đó, Dậu 
xây dựng 4 lớp: Square (hình vuông), Circle (đường tròn), Triangle (hình tam giác), 
và Amoeba (hình trùng biến hình). Cả bốn đều là các hình với hai phương thức 
rotate() và playSound(). Do đó, anh ta dùng tư duy trừu tượng hóa để tách ra các 
đặc điểm chung và đưa chúng vào một lớp mới có tên Shape (hình nói chung). Sau 
đó, kết nối các lớp hình vẽ kia với lớp Shape bởi một quan hệ gọi là thừa kế. 
Ta nói rằng "Square thừa kế từ Shape", "Circle thừa kế từ Shape", v.v.. Ta tháo 
gỡ rotate() và playSound ra khỏi 4 loại hình, và giờ thì chỉ còn phải quản lý một bản 
đặt tại lớp Shape. Shape được gọi là lớp cha (superclass) hay lớp cơ sở (base class) của 
bốn lớp kia. Còn bốn lớp đó là các lớp con (subclass) hay lớp dẫn xuất (derived class) 
của lớp Shape. Các lớp con thừa kế các phương thức của lớp cha. Nói cách khác, nếu 
lớp Shape có chức năng gì thì các lớp con của nó tự động có các chức năng đó. 
Shape
rotate()
playSound() 
Square Circle Triangle
lớp cha
các lớp con
quan hệ thừa kế
những gì cỏ ở 
cả bốn lớp
Amoeba
rotate() {
 // mã xoay hình
 // riêng cho amoeba
}
playSound() {
 // mã chơi nhạc
 // riêng cho amoeba}
overriding
Vậy thế nào là quan hệ thừa kế? Nếu ta cần xây dựng các lớp đại diện cho hai 
loài mèo nhà và hổ, mèo nhà nên thừa kế từ hổ, hay hổ nên thừa kế từ mèo, hay cả 
hai cùng thừa kế từ một lớp thứ ba? 
 104
Khi ta dùng quan hệ thừa kế trong thiết kế, ta đặt các phần mã dùng chung tại 
một lớp và coi đó là lớp cha – lớp dùng chung trừu tượng hơn, các lớp cụ thể hơn là 
các lớp con. Các lớp con được thừa kế từ lớp cha đó. Quan hệ thừa kế có nghĩa rằng 
lớp con được thừa hưởng các thành viên (member) của lớp cha. Thành viên của một 
lớp là các biến thực thể và phương thức của lớp đó. Ví dụ, Shape trong ví dụ trên có 
hai thành viên rotate() và playSound(), Cow trong Hình 5.6 có các thành viên name, 
age, getName(), getAge(), setName(), setAge(). 
Ta còn nói rằng lớp con chuyên biệt hóa (specialize) lớp cha. Nghĩa của "chuyên 
biệt hóa" ở đây gồm có hai phần: (1) lớp con là một loại con của lớp cha – thể hiện ở 
chỗ lớp con tự động thừa hưởng các thành viên của lớp cha, (2) lớp con có những 
đặc điểm của riêng nó - thể hiện ở chỗ lớp con có thể bổ sung các phương thức và 
biến thực thể mới của riêng mình, và nó có thể cài đè (override) các phương thức thừa 
kế từ lớp cha. Ví dụ, hình trùng biến hình (Amoeba) cũng là một hình (Shape), do đó 
lớp con Amoeba có tất cả những gì mà Shape có. Ngoài ra, Amoeba có thêm những 
đặc điểm riêng của thể loại hình trùng biến hình: các biến thực thể đại diện cho tâm 
xoay để phục vụ cách xoay của riêng nó, và nó định nghĩa lại các phương thức rotate 
để xoay theo cách riêng, định nghĩa lại playSound để chơi loại âm thanh riêng. Theo 
thuật ngữ, và cũng là từ khóa, của Java, lớp con "nối dài" (extends) lớp cha. 
Các biến thực thể không bị cài đè vì việc đó là không cần thiết. Biến thực thể 
không quy định một hành vi đặc biệt nào và lớp con chỉ việc gán giá trị tùy chọn cho 
biến được thừa kế. 
7.2. THIẾT KẾ CÂY THỪA KẾ 
Giả sử ta cần thiết kế một chương trình giả lập cho phép người dùng thả một 
đám các con động vật thuộc các loài khác nhau vào một môi trường để xem chuyện 
gì xảy ra. Ta hiện chưa phải viết mã mà mới chỉ ở giai đoạn thiết kế. 
Ta biết rằng mỗi con vật sẽ được đại diện bởi một đối tượng, và các đối tượng sẽ 
di chuyển loanh quanh trong môi trường, thực hiện các hành vi được lập trình cho 
loài vật đó. Ta được giao một danh sách các loài vật sẽ được đưa vào chương trình: 
sư tử, hà mã, hổ, chó, mèo, sói. 
Và ta muốn rằng, khi cần, các lập trình viên khác cũng có thể bổ sung các loài vật mới 
vào chương trình. 
Bước 1, ta xác định các đặc điểm chung và trừu tượng mà tất cả các loài động 
vật đều có. 
Các đặc điểm chung đó bao gồm: 
năm biến thực thể: 
picture – tên file ảnh đại diện cho con vật này 
 105
food – loại thức ăn mà con vật thích. Hiện giờ, biến này chỉ có hai giá trị: cỏ 
(grass) hoặc thịt (meat). 
hunger – một biến int biểu diễn mức độ đói của con vật. Biến này thay đổi tùy 
theo khi nào con vật ăn và nó ăn bao nhiêu. 
boundaries – các giá trị biểu diễn chiều dọc và chiều ngang (ví dụ 640 x 480) của 
khu vực mà các con vật sẽ đi lại hoạt động trong đó. 
location – các tọa độ X và Y của con vật trong khu vực của nó. 
và bốn phương thức: 
makeNoise() – hành vi khi con vật phát ra tiếng kêu 
eat() – hành vi khi con vật gặp nguồn thức ăn ưa thích, thịt hoặc cỏ. 
sleep() – hành vi khi con vật được coi là đang ngủ. 
roam() – hành vi khi con vật không phải đang ăn hay đang ngủ, có thể chỉ đi 
lang thang đợi gặp món gì ăn được hoặc gặp biên giới lãnh địa. 
Bước 2, thiết kế một lớp với tất cả các thuộc tính và hành vi chung kể trên. Đây 
sẽ là lớp mà tất cả các lớp động vật đều có thể chuyên biệt hóa. Các đối tượng trong 
ứng dụng đều là các con vật (animal), do đó, ta sẽ gọi tên lớp cha chung của chúng 
là Animal. Ta đưa vào đó các phương thức và biến thực thể mà tất cả các con vật đều 
có thể cần. Kết quả là ta được lớp cha là lớp tổng quát hơn, hay nói cách khác là trừu 
tượng hơn, còn các lớp con mang tính đặc thù hơn, chuyên biệt hơn lớp cha. 
Các con vật hoạt động có giống nhau không? 
Ta đã biết rằng mỗi loại Animal đều có tất cả các biến thực thể đã khai báo cho 
Animal. Một con sư tử sẽ có các giá trị riêng cho picture, food, hunger, boundaries, 
và location. Một con hà mã sẽ có những giá trị khác cho bộ biến thực thể tương tự. 
Cũng như vậy đối với chó, hổ... Thế còn các hành vi của chúng thì sao? 
 106
Bước 3: Xác định xem các lớp con có cần các hành vi (cài đặt của các phương 
thức) đặc thù của thể loại con cụ thể đó hay không? 
Để ý lớp Animal. Chắc chắn sư tử không ăn giống hà mã. Còn về tiếng kêu, ta có 
thể viết duy nhất một phương thức makeNoise tại Animal trong đó chơi một file âm 
thanh có tên là giá trị của một biến thực thể mà có giá trị khác nhau tùy loài, để con 
vật này kêu khác con vật khác. Nhưng làm vậy có vẻ chưa đủ vì tùy từng tình huống 
mà các loài khác nhau phát ra các tiếng kêu khác nhau, chẳng hạn tiếng kêu khi 
đang ăn và tiếng kêu khi gặp kẻ thù, v.v.. 
Do đó, ta quyết định rằng eat() và makeNoise() nên được cài đè tại từng lớp con. 
Tạm coi các con vật sleep và roam như nhau và không cần cài đè hai phương thức 
này. Ngoài ra, một số loài có những hành vi riêng đặc trưng của loài đó, chẳng hạn 
chó có thêm hành vi đuổi mèo (chaseCats()) bên cạnh các hành vi mà các loài động 
vật khác cũng có. 
Bước 4: Tiếp tục dùng trừu tượng hóa tìm các lớp con có thể còn có hành vi 
giống nhau, với mục đích phân nhóm mịn hơn nếu cần. 
Ví dụ, sói và chó có họ hàng gần, cùng thuộc họ Chó (canine) trong phân loại 
động vật học, chúng cùng có xu hướng di chuyển theo bầy đàn nên có thể dùng 
chung một phương thức roam(). Mèo, hổ và sư tử cùng thuộc họ Mèo (feline). Ba loài 
này có thể chung phương thức roam() vì khi di chuyển chúng cùng có xu hướng 
tránh đồng loại. Ta sẽ để cho hà mã tiếp tục dùng phương thức roam() tổng quát 
được thừa kế từ Animal. 
Ta tạm hoàn thành thiết kế như trong Hình 7.1 và sẽ quay lại bài toán này trong 
chương sau. 
 107
Hình 7.1: Cây thừa kế của các loài động vật. 
7.3. CÀI ĐÈ – PHƯƠNG THỨC NÀO ĐƯỢC GỌI? 
Lớp Wolf có bốn phương thức: sleep() được thừa kế từ Animal, roam() được 
thừa kế từ Canine (thực ra là phiên bản đè bản của Animal), và hai phương thức mà 
Wolf cài đè bản của Animal - makeNoise() và eat(). Khi ta tạo một đối tượng Wolf và 
gán một biến tham chiếu tới nó, ta có thể dùng biến đó để gọi cả bốn phương thức 
trên. Nhưng phiên bản nào của chúng đó sẽ được gọi? 
 108
Khi gọi phương thức từ một tham chiếu đối tượng, ta đang gọi phiên bản đặc 
thù nhất của phương thức đó đối với lớp của đối tượng cụ thể đó. Nếu hình dung 
cây thừa kế theo kiểu các lớp cha ở phía trên còn các lớp con ở phía dưới, thì quy tắc 
ở đây là: phiên bản thấp nhất sẽ được gọi. Trong ví dụ dùng biến w để gọi phương 
thức cho một đối tượng Wolf ở trên, thứ tự từ thấp lên cao lần lượt là Wolf, Canine, 
Animal. Khi gọi một phương thức cho một đối tượng Wolf, máy ảo Java bắt đầu tìm 
từ lớp Wolf lên, nếu nó không tìm được một phiên bản của phương thức đó tại Wolf 
thì nó chuyển lên tìm tại lớp tiếp theo bên trên Wolf ở cây thừa kế, cứ như vậy cho 
đến khi tìm thấy một phiên bản khớp với lời gọi phương thức. Với ví dụ đang xét, 
như được minh họa trong hình vẽ, w.makeNoise() sẽ dẫn đến việc kích hoạt phiên 
bản của Wolf, w.roam() gọi phiên bản của Canine, v.v.. 
7.4. CÁC QUAN HỆ IS-A VÀ HAS-A 
Như đã trình bày trong các chương trước, khi một lớp kế thừa từ một lớp khác, 
ta nói rằng lớp con chuyên biệt hóa lớp cha. Nhưng liệu khi nào thì nên chuyên biệt 
hóa một lớp khác? 
Nhớ lại rằng lớp cha là loại tổng quát, còn lớp con là loại cụ thể và chuyên biệt, 
là loại con của lớp cha. Nhìn từ khía cạnh khác, tập hợp các đối tượng mà lớp con 
đại diện là một tập con của các đối tượng mà lớp cha đại diện. Do đó, để đưa ra lựa 
chọn đúng đắn cho vấn đề nên hay không nên để lớp X là lớp chuyên biệt hóa lớp Y, 
ta có một phương pháp hiệu quả: kiểm tra quan hệ IS-A, nghĩa là xem thứ này có là 
thứ kia hay không. 
Để xem X có nên là lớp con của Y hay không, ta đặt câu hỏi theo dạng "Nếu phát 
biểu một cách tổng quát rằng loại X là một dạng/thứ/kiểu của loại Y thì có lý hay 
không?". Nếu câu trả lời là "Có", thì X có thể là lớp con của Y. 
Ví dụ: Tam giác là một hình (Triangle IS-A Shape)? Đúng. Mèo là một động vật 
họ Mèo (Cat IS-A Feline)? Đúng. Xe tải là một phương tiện giao thông (Truck IS-A 
Vehicle)? Đúng. Nghĩa là, Triangle có thể là lớp con của Shape, Cat có thể là lớp con 
của Feline, Truck có thể là lớp con của Vehicle. 
Ta xét tiếp: Phòng bếp là một cái nhà (Kitchen IS-A House)? Chắc chắn sai. 
Ngược lại thì sao? Nhà là một phòng bếp (House IS-A Kitchen)? Đúng là có một số 
người vì phong tục hay điều kiện sống mà ngôi nhà của họ chỉ có một phòng duy 
nhất nên đó vừa là nơi nấu bếp vừa là phòng cho nhiều chức năng khác. Tuy nhiên, 
các trường hợp đó chỉ là "một số", nên câu trả lời tổng quát vẫn là "Sai". Cho nên, 
Kitchen không thể là lớp con của House hay ngược lại. 
Phòng bếp và nhà rõ ràng có liên quan đến nhau, nhưng không phải qua quan 
hệ thừa kế mà là một quan hệ chứa – HAS-A. Câu hỏi ở đây là: Nhà có chứa một 
phòng bếp hay không (House HAS-A Kitchen)? Nếu câu trả lời là "Có", điều đó có 
nghĩa House có một biến thực thể kiểu Kitchen. Nói cách khác, House có một tham 
 109
chiếu tới một đối tượng Kitchen, chứ House không chuyên biệt hóa Kitchen hay ngược 
lại. 
Quan hệ HAS-A trong Java được cài đặt bằng tham chiếu đặt tại đối tượng 
chứa chiếu tới đối tượng thành phần. Quan hệ HAS-A giữa hai lớp thể hiện một 
trong ba quan hệ: kết hợp (association), tụ hợp (aggregation) và hợp thành 
(composition) mà các tài liệu về thiết kế hướng đối tượng thường nói đến. Giữa hai 
lớp có quan hệ kết hợp nếu như các đối tượng thuộc lớp này cần biết đến đối tượng 
thuộc lớp kia để có thể thực hiện được công việc của mình. Chẳng hạn, một người 
nhân viên chịu sự quản lý của một người quản lý, ta có quan hệ kết hợp nối từ 
Employee tới Manager, thể hiện ở việc mỗi đối tượng Employee có một tham chiếu 
boss kiểu Manager. Hợp thành và tụ hợp là các quan hệ giữa một đối tượng và 
thành phần của nó (cũng là đối tượng). Khác nhau ở chỗ, với quan hệ hợp thành, đối 
tượng thành phần là phần không thể thiếu được của đối tượng chứa nó, còn với 
quan hệ tụ hợp thì ngược lại. Ví dụ, một cuốn sách bao gồm nhiều trang sách và một 
cuốn sách không thể tồn tại nếu không có trang nào. Do đó giữa Book (sách) và Page 
(trang) có quan hệ hợp thành. Thư viện có nhiều sách, nhưng thư viện không có 
cuốn sách nào vẫn là một thư viện, nên quan hệ giữa Library (thư viện) và Book là 
quan hệ tụ hợp. Java không có cấu trúc nào dành riêng để cài đặt các quan hệ tụ hợp 
hay hợp thành. Ta chỉ cài đặt đơn giản bằng cách đặt vào đối tượng chủ các tham 
chiếu tới đối tượng thành phần, hay nói cách khác là phân rã thành các quan hệ 
HAS-A, chẳng hạn quan hệ hợp thành giữa Book và Page có thể được phân rã thành 
'Book HAS-A ArrayList' và nhiều quan hệ 'ArrayList HAS-A Page'. 
Các ràng buộc khác được đảm bảo bởi các phương thức có nhiệm vụ khởi tạo hay 
sửa các tham chiếu đó. 
Quay lại quan hệ IS-A, có một điểm cần lưu ý: quan hệ thừa kế IS-A chỉ có một 
chiều. Ví dụ: "Tam giác là một hình" là phát biểu có lý, nhưng khẳng định theo chiều 
ngược lại, "Hình là một tam giác", thì không đúng. Có nhiều hình là hình tam giác, 
nhưng cũng có vô số hình không phải hình tam giác. 
Thực ra, lưu ý trên là hiển nhiên, nếu ta nhớ đến mô tả về lớp con tại mục trước: 
Lớp con chuyên biệt hóa lớp cha. 
Đến đây, chúng ta chưa kết thúc câu chuyện về quan hệ thừa kế. Chương sau sẽ 
tiếp tục trình bày về các vấn đề hướng đối tượng. Một số giải pháp thiết kế trong 
chương này sẽ được xem lại và cải tiến. 
 110
7.5. KHI NÀO NÊN DÙNG QUAN HỆ THỪA KẾ? 
Mục này liệt kê một số quy tắc hướng dẫn việc sử dụng quan hệ thừa kế trong 
thiết kế. Tại thời điểm này, ta tạm bằng lòng với việc biết quy tắc. Việc hiểu quy tắc 
nếu chưa trọn vẹn thì sẽ được bồi đắp dần trong những phần sau của cuốn sách. 
NÊN dùng quan hệ thừa kế khi một lớp là một loại cụ thể hơn của một lớp cha. 
Ví dụ, tài khoản tiết kiệm (saving account) là một loại tài khoản ngân hàng (bank 
account), nên SavingAccount là lớp con của BankAccount là hợp lí. 
NÊN cân nhắc việc thừa kế khi ta có một hành vi (mã đã được viết) nên được 
dùng chung giữa nhiều lớp thuộc cùng một kiểu tổng quát nào đó. Ví dụ, Square, 
Circle và Triangle trong bài toán của Dậu và Tuất cùng cần xoay và chơi nhạc, nên 
việc đặt các chức năng đó tại một lớp cha Shape là hợp lí. Tuy vậy, cần lưu ý rằng 
mặc dù thừa kế là một trong những đặc điểm quan trọng của lập trình hướng đối 
tượng nhưng nó không nhất thiết là cách tốt nhất cho việc tái sử dụng hành vi. Quan 
hệ thừa kế giúp ta khởi động việc tái sử dụng, và nó thường là lựa chọn đúng khi 
thiết kế, nhưng các mẫu thiết kế sẽ giúp ta nhận ra những lựa chọn khác tinh tế và 
linh hoạt hơn. 
KHÔNG NÊN dùng thừa kế chỉ nhằm mục đích tái sử dụng mã của một lớp 
khác, trong khi quan hệ giữa lớp cha và lớp con vi phạm một trong hai quy tắc ở 
trên. Ví dụ, giả sử ta đã viết cho lớp DoorBell (chuông cửa) một đoạn mã dành riêng 
cho việc in, và giờ ta cần viết mã cho chức năng in của lớp Piano. Không nên vì nhu 
cầu đó mà cho Piano làm lớp con của DoorBell. Đàn piano không phải là một loại 
chuông gọi cửa. (Giải pháp nên chọn cho tình huống này là: phần mã cho chức năng 
in nên được đặt trong một lớp Printer, và các lớp cần có chức năng ... 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 đó để 
 228
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, 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, ArrayList lại không có quan hệ 
gì với ArrayList. Vậy cho nên, nếu dùng một ArrayList làm đối số 
cho phương thức yêu cầu đối số kiểu ArrayList, 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. 
 229
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, 
nó sẽ chỉ có thể lấy đối số kiểu ArrayList chứ không thể lấy kiểu 
ArrayList hay ArrayList. 
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 
thay vì ArrayList 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. 
 230
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, ArrayList,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 thì khác gì với ArrayList? 
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. 
 231
Hai cú pháp sau là tương đương: 
public void foo( ArrayList a) 
public void foo( ArrayList 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 a1, ArrayList<? extends 
Animal> a2) 
thay vào đó, ta viết: 
public void bar(ArrayList a1 , ArrayList a2) 
 232
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 
 233
Phụ lục A. DÞch ch−¬ng tr×nh b»ng JDK 
Phụ lục này hướng dẫn những bước cơ bản nhất trong việc biên dịch và chạy 
một chương trình Java đơn giản bằng công cụ JDK tại môi trường Windows. 
A.1. Soạn thảo mã nguồn chương trình 
Có thể chọn một chương trình soạn thảo văn bản đơn giản, chẳng hạn như 
Notepad. Hoặc để thuận tiện, ta có thể chọn một chương trình có tính năng tự động 
hiển thị màu theo cú pháp nhưng vẫn đơn giản, chẳng hạn như Notepad++. 
Mã nguồn chương trình cần được lưu vào file có tên trùng tên lớp (chính xác cả 
chữ hoa và chữ thường) và phần mở rộng .java. Chẳng hạn lớp HelloWorld được 
lưu trong file có tên HelloWorld.java. 
A.2. Biên dịch mã nguồn thành file .class 
Mở một cửa sổ lệnh (console) bằng cách lần lượt chọn Start menu, Run..., rồi gõ 
lệnh cmd. Cửa sổ hiện ra sẽ có dạng như trong Hình 13.15. 
Hình 13.15: Cửa sổ lệnh 
Tại cửa sổ lệnh, dấu nhắc cho biết thư mục hiện tại. Để dịch file mã nguồn, ta 
cần thay đổi thư mục hiện tại về thư mục nơi ta đã lưu file đó. Ví dụ, nếu thư mục 
mã nguồn của ta là C:\java, ta gõ lệnh sau tại dấu nhắc và nhấn Enter 
cd C:\java 
Kết quả là dấu nhắc sẽ chuyển thành C:\java>. 
Khi chạy lệnh dir tại dấu nhắc, ta sẽ thấy danh sách các file mã nguồn đặt tại thư 
mục hiện tại như trong Hình 13.16. 
 234
Hình 13.16: Danh sách các file mã nguồn. 
Để dịch chương trình HelloWorld, ta gõ lệnh sau tại dấu nhắc: 
javac HelloWorld.java 
Nếu thành công, trình biên dịch sẽ sinh ra một file bytecode có tên 
HelloWorld.class. Khi dùng lệnh dir lần nữa, ta sẽ thấy file đó được liệt kê trên 
màn hình như hình dưới đây. Chương trình đã được dịch xong và sẵn sàng chạy. 
Hình 13.17: File . class kết quả của biên dịch. 
Nếu không thành công, ta có thể đã gặp một trong những tình huống sau đây: 
1. Lỗi cú pháp: dựa theo thông báo lỗi được trình biên dịch hiển thị ra màn 
hình, ta cần quay lại trình soạn thảo để sửa lỗi trước khi chạy lệnh javac 
lần nữa để dịch lại. 
2. Thông báo lỗi 'javac' is not recognized as an internal or external 
command, operable program or batch file. Nguyên nhân là Windows 
không tìm thấy chương trình javac. 
Cách giải quyết thứ nhất cho tình huống thứ hai là: khi gọi javac ta cần gõ đầy 
đủ đường dẫn tới chương trình này, chẳng hạn: 
"C:\Program Files\Java\jdk1.6.0_26\bin\javac" HelloWorld.java 
 235
Chú ý rằng đường dẫn trên có chứa dấu trắng (Program Files) nên ta cần có cặp 
nháy kép bọc đầu cuối. 
Cách giải quyết thứ hai là sửa biến môi trường của hệ điều hành để đặt đường 
dẫn tới javac. Hướng dẫn cài đặt JDK cho mỗi hệ điều hành đều có hướng dẫn chi 
tiết cách làm. 
A.3. Chạy chương trình 
Ngay tại thư mục chứa mã nguồn, ta gõ lệnh sau tại dấu nhắc (chú ý không kèm 
đuôi .class): 
java HelloWorld 
Kết quả là chương trình chạy như trong hình dưới đây: 
Hình 13.18: Kết quả chạy chương trình. 
 236
Phụ lục B. Package – tæ chøc gãi cña java 
Mỗi lớp trong thư viện Java API thuộc về một gói (package) trong đó chứa một 
nhóm các lớp có liên quan với nhau. Khi các ứng dụng trở nên ngày càng phức tạp, 
việc tổ chức chương trình thành các gói giúp lập trình viên quản lí được các thành 
phần của ứng dụng. Các gói còn hỗ trợ việc tái sử dụng phần mềm bằng cách cho 
phép chương trình import lớp từ các gói khác (như ta vẫn làm ở hầu hết các chương 
trình ví dụ). Một lợi ích khác của tổ chức gói là cơ chế đặt tên lớp không trùng nhau. 
Điều này giúp tránh xung đột tên lớp. Phụ lục này giới thiệu cách tạo gói của chính 
mình. 
Các bước khai báo một lớp tái sử dụng được: 
1. Khai báo public cho lớp đó. Nếu không, nó sẽ chỉ được sử dụng bởi các lớp trong 
cùng một gói. 
2. Chọn một tên gói và đặt khai báo gói vào đầu file mã nguồn của lớp. Trong mỗi 
file mã nguồn chỉ có tối đa một khai báo gói và nó phải được đặt trước tất cả các 
lệnh khác. 
3. Dịch lớp đó sao cho nó được đặt vào đúng chỗ trong cấu trúc thư mục của gói 
Sau ba bước trên, lớp đó đã sẵn sàng cho việc import và sử dụng trong một 
chương trình. 
Sau đây là chi tiết về cách biên dịch các lớp trong một gói. 
Ngữ cảnh: 
Hướng dẫn này viết cho môi trường Windows và dùng một trình biên dịch 
tương đương với javac, có thể dễ dàng chuyển đổi sang nội dung tương đương cho 
môi trường Unix/Linux. 
Giả sử ta có hai gói, com.mycompanypackage chứa các lớp CompanyApp và 
BusinessLogic; và org.mypersonalpackages.util chứa các lớp Semaphore và 
HandyBits. BusinessLogic cần truy nhập tới HandyBits 
Viết mã và biên dịch 
Việc đầu tiên: tổ chức mã nguồn. Ta cần chọn một thư mục "gốc" cho cây thư 
mục chứa mã nguồn của mình. (Từ đây ta sẽ gọi nó đơn giản là gốc.) Ta sẽ dùng 
c:\java cho các ví dụ ở đây. 
Ta cần có 4 file mã nguồn sau: 
c:\java\com\mycompanypackage\CompanyApp.java 
c:\java\com\mycompanypackage\BusinessLogic.java 
c:\java\org\mypersonalpacakges\util\Semaphore.java 
 237
c:\java\org\mypersonalpacakges\util\HandyUtil.java 
Lưu ý rằng các file mã nguồn được tổ chức giống như cấu trúc gói. Điều này rất 
quan trọng, nó giúp trình biên dịch tìm thấy các file nguồn - nó cũng giúp ta trong 
hoàn cảnh y hệt. 
Tại đầu mỗi file nguồn (trước tất cả các lệnh import hay bất cứ gì không phải 
chú thích), ta cần có một dòng khai báo gói. Ví dụ, CompanyApp.java sẽ bắt đầu 
bằng: 
package com.mycompanypackage; 
Nếu lớp của ta cần import gì đó từ các gói khác, các dòng import có thể đặt sau 
đó. Ví dụ, BusinessLogic.java có thể bắt đầu bằng: 
package com.mycompanypackage; 
import org.mypersonalpackages.util.*; 
hoặc 
package com.mycompanypackage; 
import org.mypersonalpackages.util.HandyUtil; 
Một số người thích dùng import-on-demand (cách đầu), người khác thì không. 
Thật ra đây chủ yếu chỉ là vấn lười biếng. Ta hiểu rằng cách này có thể gây ra các sự 
bất tương thích nếu sau này các class bị trùng tên, nhưng bên trong các gói chuẩn 
của Java mà ta sự dụng, chuyện đó hiếm khi xảy ra. (Một phần là vì ta không dùng 
GUI mấy. Nếu dùng các gói java.awt và java.util trong cùng một class, ta sẽ phải 
thận trọng hơn.) 
Đến lúc biên dịch các class. Ta thường biên dịch tất cả các file, để chắc chắn là 
mình luôn dùng phiên bản mới nhất của tất cả các class. Trong Java có một số sự 
phụ thuộc không dễ thấy, chẳng hạn như các hằng đối tượng thuộc một class được 
nhúng trong một class khác (chẳng hạn nếu HandyUtil tham chiếu tới 
Semaphore.SOME_CONSTANT - một hằng String loại static final, giá trị của nó sẽ 
được nhúng vào trong HandyUtil.class.) Có hai cách để biên dịch tất cả. Hoặc là 
dùng lệnh một cách tường minh: 
c:\java> javac -d . com\mycompanypackage\*.java 
org\mypersonalpackage\util\*.java 
hoặc tạo một danh sách các file và chuyển nó cho javac: 
c:\java> dir /s /b *.java > srcfiles.txt 
c:\java> javac -d . @srcfiles.txt 
Lưu ý rằng ta biên dịch nó từ thư mục gốc, và ta dùng tùy chọn -d . để bảo trình 
biên dịch xếp các file .class vào một cấu trúc gói xuất phát từ gốc (dấu chấm theo sau 
có nghĩa rằng thư mục gốc là thư mục hiện tại). Một số người không thích để các file 
.class và các file nguồn cùng một chỗ - trong trường hợp đó, ta có thể dùng tùy chọn 
-d classes, nhưng ta phải tạo thư mục classes từ trước. (Ta cũng sẽ cần hoặc là lần 
nào cũng dịch tất cả hoặc đặt classes vào phần classpath cho trình biên dịch bằng tùy 
chọn -classpath.) Nếu chưa thực sự thành thạo, ta nên làm theo cách đầu và kiểm tra 
 238
chắc chắn là ta không đặt classpath . Nếu vì lý do nào đó mà ta nhất định phải dùng 
một classpath, hãy đảm bảo là . (thư mục hiện hành) nằm trong classpath. 
Chạy ứng dụng 
Nhiều người "tình cờ" đặt được các file .class của mình vào đúng chỗ, do may 
mắn chẳng hạn, nhưng rồi lại gặp phải những lỗi như: 
java.lang.NoClassDefFoundError: MyCompanyApp (wrong name: 
com/mycompanypackage/MyCompanyApp. Tình huống đó xảy ra nếu ta cố chạy 
chương trình bằng một lệnh kiểu như: 
c:\java\com\mycompanypackage> java MyCompanyApp 
Đây là cách để tránh: 
Hãy đứng yên ở thư mục "gốc" của mình, ví dụ c:\java 
Luôn luôn dùng tên đầy đủ của class. Ví dụ: 
c:\java> java com.mycompanypackage.MyCompanyApp 
Máy ảo Java biết cách tìm file .class trong thư mục com\mycompanypackage 
(lưu ý, đây là một quy ước của máy ảo, hầu hết các máy ảo dùng cách này - không có 
chỗ nào trong đặc tả ngôn ngữ nói rằng gói phải được lưu trữ theo kiểu đó; máy ảo 
Java đơn giản là phải biết cách tìm và nạp một class), nhưng trong file .class có ghi 
tên đầy đủ của nó - và máy ảo dùng thông tin đó để kiểm tra xem cái class mà nó 
được yêu cầu nạp có phải cái mà nó tìm thấy hay không. 
 239
Phụ lục C. B¶ng thuËt ng÷ anh viÖt 
Tiếng Anh Tiếng Việt Các cách dịch khác 
abstract class lớp trừu tượng 
abstract method phương thức trừu tượng 
abstraction trừu tượng hóa 
aggregation quan hệ tụ hợp quan hệ kết tập 
argument đối số tham số thực sự 
association quan hệ kết hợp 
attribute thuộc tính 
behavior hành vi 
chain stream dòng nối tiếp 
class lớp, lớp đối tượng 
class variable 
/ class attribute 
biến lớp, biến của lớp, 
thuộc tính của lớp 
biến static 
class method phương thức của lớp phương thức static 
composition quan hệ hợp thành 
concrete class lớp cụ thể 
connection stream dòng kết nối 
constructor hàm khởi tạo hàm tạo, cấu tử 
copy constructor hàm khởi tạo sao chép hàm tạo sao chép, 
cấu tử sao chép 
encapsulation đóng gói 
exception ngoại lệ 
information hiding che giấu thông tin 
inheritance thừa kế 
instance thực thể thể hiện 
instance variable biến thực thể, biến của 
thực thể 
trường, thành viên 
dữ liệu 
message thông điệp 
 240
method / 
member function 
phương thức, hàm hàm thành viên 
object đối tượng 
object serialization chuỗi hóa đối tượng 
overload cài chồng hàm trùng tên 
override cài đè ghi đè, định nghĩa lại 
package gói 
parameter tham số tham số hình thức 
pass-by-value truyền bằng giá trị 
polymorphism đa hình 
reference tham chiếu 
state trạng thái 
stream dòng 
subclass / 
derived class 
lớp con, lớp dẫn xuất 
superclass / 
base class 
lớp cha, lớp cơ sở 
top-down 
programming 
lập trình từ trên xuống 
variable biến 
virtual machine máy ảo 
 241
Tµi liÖu tham kh¶o 
[1]. Deitel & Deitel, Java How to Program, 9th edition, Prentice Hall, 2012. 
[2]. Kathy Sierra, Bert Bates, Head First Java, 2nd edition, O'Reilly, 2008. 
[3]. Oracle, JavaTM Platform Standard Ed.6, URL: 
[4]. Oracle, JavaTM Platform Standard Ed.7, URL: 
[5]. Oracle, The JavaTM Tutorials, URL:  
[6]. Ralph Morelli, Ralph Walde, Java, Java, Java – Object-Oriented Problem Solving, 
3th edition, Prentice Hall, 2005. 
[7]. Joshua Bloch, Effective Java, 2nd edition, Addison-Wesley, 2008. 

File đính kèm:

  • pdfgiao_trinh_lap_trinh_huong_doi_tuong_voi_java_phan_2.pdf