Nối tiếp nội dung phần 1, phần 2 của giáo trình môn học Lập trình hướng đối tượng cung cấp cho người học những nội dung chính sau: Vòng đời của đối tượng, thành viên lớp và thành viên thực thể, ngoại lệ, chuỗi hóa đối tượng và vào ra file, lập trình tổng quát và các lớp collection. Mời các bạn cùng tham khảo.
Chơng Vòng đời đối tợng Trong chng này, ta nói vịng đời đối tượng: đối tượng tạo nào, nằm đâu, làm để giữ vứt bỏ đối tượng cách có hiệu Cụ thể, chương trình bày khái niệm nhớ heap, nhớ stack, phạm vi, hàm khởi tạo, tham chiếu null 9.1 BỘ NHỚ STACK VÀ BỘ NHỚ HEAP Trước nói chuyện xảy ta tạo đối tượng, ta cần nói hai vùng nhớ stack heap lưu trữ đâu Đối với Java, heap stack hai vùng nhớ mà lập trình viên cần quan tâm Heap nơi đối tượng, stack chỗ phương thức biến địa phương Máy ảo Java toàn quyền quản lý hai vùng nhớ Lập trình viên khơng thể khơng cần can thiệp Đầu tiên, ta phân biệt rõ ràng biến thực thể biến địa phương, chúng sống đâu stack heap Nắm vững kiến thức này, ta dễ dàng hiểu rõ vấn đề phạm vi biến, việc tạo đối tượng, quản lý nhớ, luồng, xử lý ngoại lệ điều mà lập trình viên cần nắm (mà ta học dần chương chương sau) Biến thực thể khai báo bên lớp bên phương thức Chúng đại diện cho trường liệu đối tượng (mà ta điền liệu khác cho thực thể khác lớp đó) Các biến thực thể sống bên đối tượng chủ chúng Biến địa phương, có tham số, khai báo bên phương thức Chúng biến tạm thời, chúng sống bên khung nhớ phương thức tồn phương thức nằm nhớ stack, nghĩa phương thức chạy chưa chạy đến ngoặc kết thúc (}) Vậy biến địa phương đối tượng? Nhớ lại Java biến thuộc kiểu không thực tham chiếu tới đối tượng khơng phải đối tượng Do đó, biến địa phương nằm stack, cịn đối tượng mà chiếu tới nằm heap Bất kể tham chiếu khai báo đâu, 143 biến địa phương phương thức biến thực thể lớp, đối tượng mà chiếu tới nằm heap public void foo() { Cow c = new Cow(); } :Cow c Cow stack đối tượng Cow heap Vậy biến thực thể nằm đâu? Các biến thực thể kèm theo đối tượng, chúng sống bên vùng nhớ đối tượng chủ heap Mỗi ta gọi new Cow(), Java cấp phát nhớ cho đối tượng Cow heap, lượng nhớ cấp phát đủ chỗ để lưu giá trị tất biến thực thể đối tượng Nếu biến thực thể thuộc kiểu bản, vùng nhớ cấp phát cho có kích thước tùy theo kích thước kiểu liệu khai báo Ví dụ biến int cần 32 bit Còn biến thực thể đối tượng sao? Chẳng hạn, Car HAS-A Engine (ơ tơ có động cơ), nghĩa đối tượng Car có biến thực thể tham chiếu kiểu Engine Java cấp phát nhớ bên đối tượng Car đủ để lưu biến tham chiếu engine Còn thân biến chiếu tới đối tượng Engine nằm bên ngồi, khơng phải bên trong, đối tượng Car Hình 9.1: Đối tượng có biến thực thể kiểu tham chiếu Vậy đối tượng Engine cấp phát nhớ heap? Khi lệnh new Engine() cho chạy Chẳng hạn, ví dụ Hình 9.2, đối tượng Engine tạo để khởi tạo giá trị cho biến thực thể engine, lệnh khởi tạo nằm khai báo lớp Car 144 Hình 9.2: Biến thực thể khởi tạo khai báo Còn ví dụ Hình 9.3, khơng có đối tượng Engine tạo đối tượng Car cấp phát nhớ, engine không khởi tạo Ta cần đến lệnh riêng biệt sau để tạo đối tượng Engine gán trị cho engine, chẳng hạn c.engine = new Engine(); Hình 9.1 class Car { Engine engine; } khơng có đối tượng Engine tạo ra, biến engine chưa khởi tạo đối tượng thực :Car engine đối tượng Car Car c = new Car(); Hình 9.3: Biến thực thể khơng khởi tạo khai báo Bây ta đủ kiến thức tảng để bắt đầu sâu vào trình tạo đối tượng 9.2 KHỞI TẠO ĐỐI TƯỢNG Nhớ lại có ba bước muốn tạo đối tượng: khai báo biến tham chiếu, tạo đối tượng, chiếu tham chiếu tới đối tượng Ta hiểu rõ hai bước Mục trình bày kĩ phần cịn lại: tạo đối tượng Khi ta chạy lệnh new Cow(), máy ảo Java kích hoạt hàm đặc biệt gọi hàm khởi tạo (constructor) Nó khơng phải phương thức thơng thường, chạy ta khởi tạo đối tượng, cách để kích hoạt hàm khởi tạo cho đối tượng dùng từ khóa new kèm theo tên lớp để tạo đối tượng 145 (Thực cịn cách khác gọi trực tiếp từ bên hàm khởi tạo khác, ta nói cách sau) Trong ví dụ trước, ta chưa viết hàm khởi tạo, đâu máy ảo gọi ta tạo đối tượng mới? Ta viết hàm khởi tạo, ta viết nhiều hàm khởi tạo Nhưng ta khơng viết trình biên dịch viết cho ta hàm khởi tạo mặc định Hàm khởi tạo mặc định trình biên dịch dành cho lớp Cow có nội dung này: Hàm khởi tạo trông giống với phương thức, có đặc điểm là: khơng có kiểu trả (và khơng trả giá trị gì), có tên hàm trùng với tên lớp Hàm khởi tạo mà trình biên dịch tự tạo có nội dung rỗng, hàm khởi tạo ta tự viết có nội dung phần thân hàm Đặc điểm quan trọng hàm khởi tạo chạy trước ta làm việc khác đối tượng tạo, chiếu tham chiếu tới chẳng hạn Nghĩa là, ta có hội đưa đối tượng vào trạng thái sẵn sàng sử dụng trước bắt đầu sử dụng Nói cách khác, đối tượng có hội tự khởi tạo trước điều khiển tham chiếu Tại hàm khởi tạo Cow ví dụ Hình 9.4: Hàm khởi tạo khơng lấy đối số.Hình 9.4, ta khơng làm điều nghiêm trọng mà in thơng báo hình để thể chuỗi kiện xảy Hình 9.4: Hàm khởi tạo khơng lấy đối số Nhiều người dùng hàm khởi tạo để khởi tạo trạng thái đối tượng, nghĩa gán giá trị ban đầu cho biến thực thể đối tượng, chẳng hạn: public Cow() { 146 weight = 10.0; } Đó lựa chọn tốt người viết lớp Cow biết đối tượng Cow nên có cân nặng Nhưng lập trình viên khác – người viết đoạn mã dùng đến lớp Cow có thơng tin sao? Từ mục 5.4, ta biết giải pháp dùng phương thức truy nhập Cụ thể ta bổ sung phương thức setWeight() phép gán giá trị cho weight từ bên ngồi lớp Cow Nhưng điều có nghĩa người ta cần đến lệnh để hoàn thành việc khởi tạo đối tượng Cow: lệnh new Cow() để tạo đối tượng, lệnh gọi setWeight() để khởi tạo weight Và hai lệnh khoảng thời gian mà đối tượng Cow tạm thời có weight chưa khởi tạo9 Hình 9.5: Ví dụ biến thực thể chưa khởi tạo đối tượng Với cách làm vậy, ta phải tin tưởng người dùng lớp Cow khởi tạo weight hy vọng họ khơng làm kì cục trước khởi tạo weight Trông đợi vào việc người khác làm tương đương với việc hy vọng điều rủi ro không xảy Tốt ta nên tự đảm bảo cho tình không mong muốn không xảy Nếu đối tượng khơng nên sử dụng trước khởi tạo xong ta đừng cho động đến đối tượng trước ta hồn thành việc khởi tạo Các biến thực thể có sẵn giá trị mặc định, weight có sẵn giá trị 0.0, 147 Hình 9.6: Hàm khởi tạo có tham số Cách tốt để hoàn thành việc khởi tạo đối tượng trước có tham chiếu tới đối tượng đặt tất đoạn mã khởi tạo vào bên hàm khởi tạo Vấn đề lại viết hàm khởi tạo nhận đối số dùng đối số để truyền vào hàm khởi tạo thông số cần thiết cho việc khởi tạo đối tượng Kết sau lời gọi hàm khởi tạo kèm đối số, đối tượng khởi tạo xong sẵn sàng cho sử dụng Xem minh họa Hình 9.6 Tuy nhiên, khơng phải lúc người dùng Cow biết quan tâm đến trọng lượng cần khởi tạo cho đối tượng Cow Ta nên cho họ lựa chọn tạo Cow mà không cần rõ giá trị khởi tạo cho weight Cách giải bổ sung hàm khởi tạo không nhận đối số hàm tự gán cho weight giá trị mặc định Hình 9.7: Hai hàm khởi tạo chồng Nói cách khác ta có hàm khởi tạo chồng để phục vụ lựa chọn khác cho việc tạo đối tượng Và phương thức chồng khác, hàm khởi tạo chồng phải có danh sách tham số khác 148 Như với khai báo lớp Cow ví dụ Hình 9.7, ta viết hai hàm khởi tạo cho lớp Cow, người dùng có hai lựa chọn để tạo đối tượng Cow mới: Cow c1 = new Cow(12.1); Cow c1 = new Cow(); Quay lại vấn đề hàm khởi tạo không nhận đối số mà trình biên dịch cung cấp cho ta Khơng phải lúc ta có sẵn hàm khởi tạo Trình biên dịch cung cấp cho ta hàm khởi tạo mặc định ta không viết hàm khởi tạo cho lớp Khi ta viết dù hàm khởi tạo cho lớp đó, ta phải tự viết hàm khởi tạo khơng nhận đối số cần đến Những điểm quan trọng: • Biến thực thể sống bên đối tượng chủ • Các đối tượng sống vùng nhớ heap • Hàm khởi tạo đoạn mã chạy ta gọi new lớp đối tượng • Hàm khởi tạo mặc định hàm khởi tạo không lấy đối số • Nếu ta không viết hàm khởi tạo cho lớp trình biên dịch cung cấp hàm khởi tạo mặc định cho lớp Ngược lại, ta phải tự viết hàm khởi tạo mặc định • Nếu có thể, nên cung cấp hàm khởi tạo mặc định để tạo điều kiện thuận lợi cho lập trình viên sử dụng đối tượng Hàm khởi tạo mặc định khởi tạo giá trị mặc định cho biến thực thể • Ta có hàm khởi tạo khác cho lớp Đó hàm khởi tạo chồng • Các hàm khởi tạo chồng phải có danh sách đối số khác • Các biến thực thể ln có sẵn giá trị mặc định, kể ta không tự khởi tạo chúng Các giá trị mặc định 0/0.0/false cho kiểu null cho kiểu tham chiếu 9.3 HÀM KHỞI TẠO VÀ VẤN ĐỀ THỪA KẾ Nhớ lại Mục 8.6 ta nói cấu trúc bên lớp có chứa phần thừa kế từ lớp cha, lớp Cow bọc lõi phần Object mà thừa kế Nói cách khác, đối tượng lớp không chứa biến thực thể mà cịn chứa thứ hưởng từ lớp cha Mục nói việc khởi tạo phần thừa kế 149 9.3.1 Gọi hàm khởi tạo lớp cha Khi đối tượng tạo, cấp phát nhớ cho tất biến thực thể thứ thừa kế từ lớp cha, lớp ông, lớp cụ lớp Object đỉnh thừa kế Tất hàm khởi tạo trục thừa kế đối tượng phải thực thi ta tạo đối tượng Mỗi lớp tổ tiên lớp con, kể lớp trừu tượng, có hàm khởi tạo Tất hàm khởi tạo kích hoạt đối tượng lớp tạo Lấy ví dụ Hippo thừa kế Animal Một đối tượng Hippo chứa phần Animal, phần Animal lại chứa phần Object Nếu ta muốn tạo đối tượng Hippo, ta phải khởi tạo phần Animal đối tượng Hippo để sử dụng thừa kế từ Animal Tương tự, để tạo phần Animal đó, ta phải tạo phần Object chứa Khi hàm khởi tạo chạy, gọi hàm khởi tạo lớp cha Khi hàm khởi tạo lớp cha chạy, gọi hàm khởi tạo lớp ông, gặp hàm khởi tạo Object Quy trình gọi dây chuyền hàm khởi tạo (Constructor Chaining) 150 public class Animal { public Animal() { System.out.println("Making an Animal"); } } public class Hippo extends Animal { public Hippo() { System.out.println("Making a Hippo"); } } public class TestHippo { public static void main (String[] args) { System.out.println("Starting "); Hippo h = new Hippo(); % java TestHippo } Starting } Making an Animal Making a Hippo Hình 9.8: Dây chuyền hàm khởi tạo Ta minh họa dây chuyền hàm khởi tạo ví dụ Hình 9.8 Trong ví dụ đó, mã chương trình TestHippo gọi lệnh new Hippo() để tạo đối tượng Hippo mới, lệnh khởi động dây chuyền hàm khởi tạo Đầu tiên Hippo() kích hoạt, Hippo() gọi hàm khởi tạo lớp cha – Animal(), đến lượt nó, Animal gọi hàm khởi tạo lớp cha – Object() Sau Object() chạy xong, hoàn thành khởi tạo phần Object đối tượng Hippo, kết thúc trả quyền điều khiển cho nơi gọi – hàm khởi tạo Animal() Hàm khởi tạo Animal() khởi tạo xong phần Animal đối tượng Hippo kết thúc, trả quyền điều khiển cho nơi gọi – hàm khởi tạo Hippo() Hippo() thực cơng việc kết thúc Đối tượng Hippo khởi tạo xong Lưu ý hàm khởi tạo gọi hàm khởi tạo lớp cha trước thực lệnh thân hàm Nghĩa là, Hippo() gọi Animal() trước thực lệnh in hình Vậy nên kết chương trình TestHippo, ta thấy phần hiển thị Animal() in hình trước phần hiển thị Hippo() Ta nói hàm khởi tạo gọi hàm khởi tạo kia, Hình 9.8 hồn tồn khơng có lệnh gọi Animal() từ mã Hippo(), khơng có lệnh gọi Object() từ mã Animal() Một lần nữa, trình biên dịch làm cơng việc thay cho lập trình viên, tự động điền lệnh super() vào trước dòng thân hàm khởi tạo Việc xảy hàm khởi tạo mà lập trình viên khơng tự viết lời gọi đến hàm khởi tạo lớp cha Còn hàm khởi tạo mà lập trình viên tự gọi super, lời gọi phải lệnh thân hàm Tại lời gọi super() phải lệnh hàm khởi tạo? Đối tượng thuộc lớp phụ thuộc vào thừa kế từ lớp cha, 151 thừa kế nên khởi tạo trước Các phần thừa kế từ lớp cha phải xây dựng hoàn chỉnh trước xây dựng phần lớp Lưu ý cách để gọi hàm khởi tạo lớp cha từ hàm khởi tạo lớp lệnh super() khơng gọi đích danh tên hàm Animal() hay Object() Lệnh gọi hàm khởi tạo lớp cha mà trình biên dịch sử dụng super() khơng có đối số Nhưng ta tự gọi dùng super() với đối số để gọi hàm khởi tạo cụ thể hàm khởi tạo chồng lớp cha 9.3.2 Truyền đối số cho hàm khởi tạo lớp cha Ta hình dung tình sau: vật có tên, nên đối tượng Animal có biến thực thể name Lớp Animal có phương thức getName(), trả giá trị biến thực thể name Biến thực thể đánh dấu private, lớp Hippo thừa kế phương thức getName() Vấn đề Hippo có phương thức getName() qua thừa kế, lại khơng có biến thực thể name Hippo phải nhờ phần Animal giữ biến name trả giá trị name gọi getName() từ đối tượng Hippo Vậy đối tượng Hippo tạo, làm cách để gửi cho phần Animal giá trị cần khởi tạo cho name? Câu trả lời là: dùng giá trị làm đối số gọi hàm khởi tạo Animal Ta thấy thân hàm Hippo(String name) ví dụ Hình 9.9 khơng làm ngồi việc gọi phương thức khởi tạo lớp cha với danh sách tham số giống hệt Có thể có người đọc thắc mắc phải viết hàm khởi tạo lớp với nội dung Trong lớp thừa kế lớp cha lớp không cần cài lại sử dụng phiên thừa kế lớp cha với danh sách tham số giống hệt, việc viết phương thức cài đè lớp với nội dung gồm lời gọi tới phiên thừa kế lớp cha không cần thiết Thực ra, phương thức khởi tạo có danh sách tham số, phương thức Hippo(String name) Animal(String name) khác tên Hippo(String name) khơng cài đè Animal(String name) Tóm lại, lớp không thừa kế phương thức khởi tạo lớp cha 152 Hình 13.9: Lỗi run-time sử dụng TreeSet cho Contact Tương tự tình so sánh bằng, TreeSet, hay Collections tự biết cách so sánh đối tượng thuộc lớp mà lập trình viên tự xây dựng Chương trình Hình 13.9 biên dịch khơng có lỗi add() khơng u cầu tham số kiểu Comparable, chạy gặp lỗi run-time lệnh gọi đến phương thức Tóm lại, phần tử 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 chọn hai cách sau để giải vấn đề đó: Các phần tử danh sách phải thuộc lớp có cài interface Comparable Ta sửa lớp Contact để bổ sung phần in đậm Hình 13.10, chương trình Hình 13.9, sau chạy khơng có lỗi Hình 13.10: Cài interface Comparable 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 dùng chương trình TestTreeSet dịng in đậm Hình 13.11 Theo đó, ContactCompare loại Comparator riêng dành cho việc so sánh đối tượng Contact Còn danh bạ đối tượng TreeSet tạo kèm với loại Comparator đặc biệt để 227 biết cách đối xử với phần tử danh bạ (cContact đối số gọi hàm khởi tạo TreeSet) Hình 13.11: Sử dụng Comparator Cả hai cách áp dụng cho phương thức sort() Collection tiện ích tổng quát tương tự thư viện Java 13.6 KÍ TỰ ĐẠI DIỆN TRONG KHAI BÁO THAM SỐ KIỂU Quan hệ thừa kế hai lớp khơng có ảnh hưởng đến quan hệ cấu trúc tổng quát dùng cho hai lớp Chẳng hạn, Dog Cat lớp Animal, ta đưa đối tượng Dog Cat vào ArrayList, tính chất đa hình Dog, Cat, Animal hoạt động bình thường (xem ví dụ Hình 13.12) Tuy nhiên, ArrayList, ArrayList lại khơng có quan hệ với ArrayList Vậy cho nên, dùng ArrayList làm đối số cho phương thức yêu cầu đối số kiểu ArrayList, ví dụ Hình 13.13, trình biên dịch báo lỗi sai kiểu liệu 228 Hình 13.12: Đa hình bên cấu trúc tổng quát Hình 13.13: Khơng có đa hình cấu trúc tổng quát Tóm lại, ta khai báo phương thức lấy đối số kiểu ArrayList, lấy đối số kiểu ArrayList lấy kiểu ArrayList hay ArrayList Ta khơng hài lịng với với việc thỏa hiệp, nghĩa dùng ArrayList thay ArrayList cho danh sách chứa tồn Dog Vì trình biên dịch khơng kiểm tra kiểu liệu để ngăn chặn tình chẳng hạn danh sách chó nghiệp vụ lính cứu hỏa lại có mèo 229 Hình 13.14: Nguy cho mèo vào danh sách chó Vậy làm để làm cho phương thức nhận đối số thuộc kiểu ArrayList, ArrayList,…nghĩa ArrayList dành cho kiểu lớp Animal? Giải pháp sử dụng kí tự đại diện (wildcard) Ta sửa phương thức makeASymphony() sau, chương trình Hình 13.13 chạy chạy ? extends Animal có nghĩa kiểu thuộc loại Animal Nhớ từ khóa extends có nghĩa "là lớp của" "cài đặt", tùy vào việc theo sau từ khóa extends tên lớp hay tên interface Vậy nên muốn makeASymphony() lấy đối số ArrayList loại cài interface Pet, ta khai báo sau: Nhưng ArrayList khai báo, trình biên dịch khơng cho ta thêm vào danh sách mà tham số phương thức chiếu tới Ta gọi phương thức phần tử danh sách, ta thêm phần tử vào danh sách Do đó, ta n tâm chương trình chạy Ví dụ, makeASymphony() với nội dung không gặp lỗi biên dịch, takeAnimals() với nội dung Hình 13.14 khơng biên dịch 230 Hai cú pháp sau tương đương: public void foo( ArrayList