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ế.
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)
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:
- giao_trinh_lap_trinh_huong_doi_tuong_voi_java_phan_2.pdf