Giáo trình Lập trình hướng đối tượng (Phần 2) - Đại học Công nghệ thuộc Đại học Quốc gia Hà Nội
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 (Phần 2) - Đại học Công nghệ thuộc Đại học Quốc gia Hà Nội", để tải tài liệu gốc về máy bạn click vào nút DOWNLOAD ở trên
Tài liệu đính kèm:
- giao_trinh_lap_trinh_huong_doi_tuong_phan_2_dai_hoc_cong_ngh.pdf
Nội dung text: Giáo trình Lập trình hướng đối tượng (Phần 2) - Đại học Công nghệ thuộc Đại học Quốc gia Hà Nội
- Ch−¬ng 9. Vßng ®êi cña mét ®èi t−îng Trong chương này, ta nói về vòng đời của đối tượng: đối tượng được tạo ra như thế nào, nó nằm ở đâu, làm thế nào để giữ hoặc vứt bỏ đối tượng một cách có hiệu quả. Cụ thể, chương này trình bày về các khái niệm bộ nhớ heap, bộ 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 khi nói về chuyện gì xảy ra khi ta tạo một đối tượng, ta cần nói về hai vùng bộ nhớ stack và heap và cái gì được lưu trữ ở đâu. Đối với Java, heap và stack là hai vùng bộ nhớ mà lập trình viên cần quan tâm. Heap là nơi ở của các đối tượng, còn stack là chỗ của các phương thức và biến địa phương. Máy ảo Java toàn quyền quản lý hai vùng bộ nhớ này. Lập trình viên không thể và không cần can thiệp. Đầu tiên, ta hãy phân biệt rõ ràng biến thực thể và biến địa phương, chúng là cái gì và sống ở đâu trong stack và heap. Nắm vững kiến thức này, ta sẽ dễ dàng hiểu rõ những vấn đề như phạm vi của biến, việc tạo đối tượng, quản lý bộ nhớ, luồng, xử lý ngoại lệ những điều căn bản mà một lập trình viên cần nắm được (mà ta sẽ học dần trong chương này và những chương sau). Biến thực thể được khai báo bên trong một lớp chứ không phải bên trong một phương thức . Chúng đại diện cho các trường dữ liệu của mỗi đối tượng (mà ta có thể điền các dữ liệu khác nhau cho các thực thể khác nhau của lớp đó). Các biến thực thể sống bên trong đối tượng chủ của chúng. Biến địa phương, trong đó có các tham số, được khai báo bên trong một phương thức . Chúng là các biến tạm thời, chúng sống bên trong khung bộ nhớ của phương thức và chỉ tồn tại khi phương thức còn nằm trong bộ nhớ stack, nghĩa là khi phương thức đang chạy và chưa chạy đến ngoặc kết thúc (}). Vậy còn các biến địa phương là các đối tượng? Nhớ lại rằng trong Java một biến thuộc kiểu không cơ bản thực ra là một tham chiếu tới một đối tượng chứ không phải chính đối tượng đó. Do đó, biến địa phương đó vẫn nằm trong stack, còn đối tượng mà nó chiếu tới vẫn nằm trong heap. Bất kể tham chiếu được khai báo ở đâu, 143
- là biến địa phương của một phương thức hay là biến thực thể của một lớp, đối tượng mà nó chiếu tới bao giờ cũng nằm trong heap. () { } Vậy biến thực thể nằm ở đâu? Các biến thực thể đi kèm theo từng đối tượng, chúng sống bên trong vùng bộ nhớ của đối tượng chủ tại heap. Mỗi khi ta gọi new Cow(), Java cấp phát bộ nhớ cho đối tượng Cow đó tại heap, lượng bộ nhớ được cấp phát đủ chỗ để lưu giá trị của tất cả các biến thực thể của đối tượng đó. Nếu biến thực thể thuộc kiểu cơ bản, vùng bộ nhớ được cấp phát cho nó có kích thước tùy theo kích thước của kiểu dữ liệu nó được khai báo. Ví dụ một biến int cần 32 bit. Còn nếu biến thực thể là đối tượng thì sao? Chẳng hạn, Car HAS-A Engine (ô tô có một động cơ), nghĩa là mỗi đối tượng Car có một biến thực thể là tham chiếu kiểu Engine. Java cấp phát bộ nhớ bên trong đối tượng Car đủ để lưu biến tham chiếu engine. Còn bản thân biến này sẽ chiếu tới một đối tượng Engine nằm bên ngoài, chứ 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 khi nào đối tượng Engine được cấp phát bộ nhớ trong heap? Khi nào lệnh new Engine() cho nó được chạy. Chẳng hạn, trong ví dụ Hình 9.2, đối tượng Engine được tạo mới để khởi tạo giá trị cho biến thực thể engine, lệnh khởi tạo nằm ngay trong khai báo lớp Car. 144
- Hình 9.2: Bi ến th ực th ể c kh ởi t ạo khi khai báo. Còn trong ví dụ Hình 9.3, không có đối tượng Engine nào được tạo khi đối tượng Car được cấp phát bộ nhớ, engine không được khởi tạo. Ta sẽ cần đến các lệnh riêng biệt ở sau đó để tạo đối tượng Engine và gán trị cho engine, chẳng hạn như c.engine = new Engine(); trong Hình 9.1. không có đối tượng Engine nào được tạo ra, biến engine chưa được khởi tạo bởi một đối tượng thực Hình 9.3: Bi ến th ực th ể không được kh ởi t ạo khi khai báo. Bây giờ ta đã đủ kiến thức nền tảng để bắt đầu đi sâu vào quá trình tạo đối tượng. 9.2. KHỞI TẠO ĐỐI TƯỢNG Nhớ lại rằng có ba bước khi muốn tạo mới một đối tượng: khai báo một biến tham chiếu, tạo một đối tượng, chiếu tham chiếu tới đối tượng đó. Ta đã hiểu rõ về hai bước 1 và 3. Mục này sẽ trình bày kĩ về phần còn lại: tạo một đối tượng. Khi ta chạy lệnh new Cow(), máy ảo Java sẽ kích hoạt một hàm đặc biệt được gọi là hàm khởi tạo (constructor ). Nó không phải một phương thức thông thường, nó chỉ chạy khi ta khởi tạo một đối tượng, và cách duy nhất để kích hoạt một hàm khởi tạo cho một đối tượng là dùng từ khóa new kèm theo tên lớp để tạo chính đối tượng 145
- đó. (Thực ra còn một cách khác là gọi trực tiếp từ bên trong một hàm khởi tạo khác, nhưng ta sẽ nói về cách này sau). Trong các ví dụ trước, ta chưa hề viết hàm khởi tạo, vậy nó ở đâu ra để cho máy ảo gọi mỗi khi ta tạo đối tượng mới? Ta có thể viết hàm khởi tạo, và ta sẽ viết nhiều hàm khởi tạo. Nhưng nếu ta không viết thì trình biên dịch sẽ viết cho ta một hàm khởi tạo mặc định. Hàm khởi tạo mặc định của trình biên dịch dành cho lớp Cow có nội dung như thế này: Hàm khởi tạo trông giống với một phương thức, nhưng có các đặc điểm là: không có kiểu trả về (và sẽ không trả về giá trị gì), và 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 sẽ có nội dung ở trong phần thân hàm. Đặc điểm quan trọng của một hàm khởi tạo là nó chạy trước khi ta làm được bất cứ việc gì khác đối với đối tượng được tạo, chiếu một tham chiếu tới nó chẳng hạn. Nghĩa là, ta có cơ hội đưa đối tượng vào trạng thái sẵn sàng sử dụng trước khi nó bắt đầu được sử dụng. Nói cách khác, đối tượng có cơ hội tự khởi tạo trước khi bất cứ ai có thể điều khiển nó bằng một cái tham chiếu nào đó. Tại hàm khởi tạo của Cow trong 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 gì nghiêm trọng mà chỉ in thông báo ra màn hình để thể hiện chuỗi sự kiện đã xảy ra. 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 của đối tượng, nghĩa là gán các giá trị ban đầu cho các biến thực thể của đối tượng, chẳng hạn: public Cow() { 146
- weight = 10.0; } Đó là lựa chọn tốt nếu như người viết lớp Cow biết được đối tượng Cow nên có cân nặng bao nhiêu. Nhưng nếu những lập trình viên khác – người viết những đoạn mã dùng đến lớp Cow mới có thông tin này thì sao? Từ mục 5.4, ta đã biết về giải pháp dùng các phương thức truy nhập. Cụ thể ở đây ta có thể bổ sung phương thức setWeight() để cho phép gán giá trị cho weight từ bên ngoài lớp Cow. Nhưng điều đó có nghĩa người ta sẽ cần đến 2 lệnh để hoàn thành việc khởi tạo một đối tượng Cow: một lệnh new Cow() để tạo đối tượng, một lệnh gọi setWeight() để khởi tạo weight. Và ở giữa hai lệnh đó là khoảng thời gian mà đối tượng Cow tạm thời có weight chưa được khởi tạo 9. Hình 9.5: Ví d ụ v ề bi ến th ực th ể ch ưa được kh ởi t ạo cùng đối t ượng. Với cách làm như vậy, ta phải tin tưởng là người dùng lớp Cow sẽ khởi tạo weight và hy vọng họ sẽ không làm gì kì cục trước khi khởi tạo weight. Trông đợi vào việc người khác sẽ làm đúng cũng tương đương với việc hy vọng điều rủi ro sẽ không xảy ra. Tốt hơn cả là ta nên tự đảm bảo sao cho những tình huống không mong muốn sẽ không xảy ra. Nếu một đối tượng không nên được sử dụng trước khi nó được khởi tạo xong thì ta đừng cho ai động đến đối tượng đó trước khi ta hoàn thành việc khởi tạo. 9 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 nhất để hoàn thành việc khởi tạo đối tượng trước khi ai đó có được một tham chiếu tới đối tượng là đặt tất cả những đoạn mã khởi tạo vào bên trong hàm khởi tạo. Vấn đề còn lại chỉ là viết một hàm khởi tạo nhận đối số rồi dùng đối số để truyền vào hàm khởi tạo các thông số cần thiết cho việc khởi tạo đối tượng. Kết quả là sau đúng một lời gọi hàm khởi tạo kèm đối số, đối tượng được khởi tạo xong và sẵn sàng cho sử dụng. Xem minh họa tại Hình 9.6. Tuy nhiên, không phải lúc nào người dùng Cow cũng biết hoặc quan tâm đến trọng lượng cần khởi tạo cho đối tượng Cow mới. Ta nên cho họ lựa chọn tạo mới Cow mà không cần chỉ rõ giá trị khởi tạo cho weight. Cách giải quyết là bổ sung một hàm khởi tạo không nhận đối số và hàm này sẽ tự gán cho weight một giá trị mặc định nào đó. Hình 9.7: Hai hàm kh ởi t ạo ch ồng. Nói cách khác là ta có các hàm khởi tạo chồng nhau để phục vụ các lựa chọn khác nhau cho việc tạo mới đối tượng. Và cũng như các phương thức chồng khác, các hàm khởi tạo chồng nhau phải có danh sách tham số khác nhau. 148
- Như với khai báo lớp Cow trong ví dụ Hình 9.7, ta viết hai hàm khởi tạo cho lớp Cow, và người dùng sẽ có hai lựa chọn để tạo một đối tượng Cow mới: Cow c1 = new Cow(12.1); hoặc Cow c1 = new Cow(); Quay lại vấn đề về 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 nào ta cũng có sẵn một hàm khởi tạo như vậy. Trình biên dịch chỉ cung cấp cho ta một hàm khởi tạo mặc định nếu ta không viết bất cứ một hàm khởi tạo nào cho lớp đó . Khi ta đã viết dù chỉ một hàm khởi tạo cho lớp đó, thì ta phải tự viết cả hàm khởi tạo không nhận đối số nếu cần đến nó. Những điểm quan trọng: • Biến thực thể sống ở bên trong đối tượng chủ của nó. • Các đối tượng sống trong vùng bộ nhớ heap. • Hàm khởi tạo là đoạn mã sẽ chạy khi ta gọi new đối với một lớp đối tượng • Hàm khởi tạo mặc định là hàm khởi tạo không lấy đối số. • Nếu ta không viết một hàm khởi tạo nào cho một lớp thì trình biên dịch sẽ cung cấp một hàm khởi tạo mặc định cho lớp đó. Ngược lại, ta sẽ 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 các 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 các giá trị mặc định cho các biến thực thể. • Ta có thể có các hàm khởi tạo khác nhau cho một lớp. Đó là các hàm khởi tạo chồng. • Các hàm khởi tạo chồng nhau phải có danh sách đối số khác nhau. • Các biến thực thể luôn có sẵn giá trị mặc định, kể cả khi ta không tự khởi tạo chúng. Các giá trị mặc định là 0/0.0/false cho các kiểu cơ bản và 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 khi ta nói về cấu trúc bên trong của lớp con có chứa phần được thừa kế từ lớp cha, lớp Cow bọc ra ngoài cái lõi là phần Object mà nó được thừa kế. Nói cách khác, mỗi đối tượng lớp con không chỉ chứa các biến thực thể của chính nó mà còn chứa mọi thứ được hưởng từ lớp cha của nó . Mục này nói về việc khởi tạo phần được thừa kế đó 149
- 9.3.1. Gọi hàm khởi tạo của lớp cha Khi một đối tượng được tạo, nó được cấp phát bộ nhớ cho tất cả các biến thực thể của chính nó cũng như những thứ nó được thừa kế từ lớp cha, lớp ông, lớp cụ cho đến lớp Object trên đỉnh cây thừa kế. Tất cả các hàm khởi tạo trên trục thừa kế của một đối tượng đều phải được thực thi khi ta tạo mới đối tượng đó. Mỗi lớp tổ tiên của một lớp con, kể cả các lớp trừu tượng, đều có hàm khởi tạo. Tất cả các hàm khởi tạo đó được kích hoạt lần lượt mỗi khi một đối tượng của lớp con được tạo. Lấy ví dụ Hippo trong cây thừa kế Animal. Một đối tượng Hippo mới chứa trong nó phần Animal, phần Animal đó lại chứa trong nó phần Object. Nếu ta muốn tạo một đối tượng Hippo, ta cũng phải khởi tạo phần Animal của đối tượng Hippo đó để nó có thể sử dụng được những gì được thừa kế từ Animal. Tương tự, để tạo phần Animal đó, ta cũng phải tạo phần Object chứa trong đó. Khi một hàm khởi tạo chạy, nó lập tức gọi hàm khởi tạo của lớp cha. Khi hàm khởi tạo của lớp cha chạy, nó lập tức gọi hàm khởi tạo của lớp ông, cứ như thế cho đến khi gặp hàm khởi tạo của Object. Quy trình đó được gọi là dây chuyền hàm khởi tạo (Constructor Chaining ). 150