Class và Object
Xem thửTình huống mở đầu
Quán Cà Phê Gió thêm chương trình khuyến mãi: khách được tặng thêm 20 điểm ngay trên tấm thẻ đang cầm. Bạn viết Card b = a; để "sao" thẻ của An cho nhân viên thử trước, rồi cộng thêm điểm vào bản sao. Thử xong, bạn in tấm thẻ gốc để kiểm tra, và thấy thẻ gốc của An đã bị cộng 20 điểm thật. Cả quán có 150 thẻ, bạn chỉ định thử trên một thẻ.
Máy không hề báo lỗi. Bản "sao" kia nằm ở đâu, và vì sao thẻ gốc lại đổi theo? Hãy giữ câu hỏi này, cuối bài bạn sẽ tự trả lời và tự sửa.
Dự án chạy xuyên chương: Hệ thống thẻ thành viên của quán Cà Phê Gió (bước 2 trên 8). Ở bước 1, bạn đã thấy Card là cái khuôn và mỗi thẻ có điểm riêng. Bài này thêm một thứ còn thiếu: thẻ nằm ở đâu và biến chỉ tới nó bằng cách nào, để bạn không còn nhầm "sao thẻ" với "thêm một cái tên cho thẻ".
Ôn nhanh bài trước: new Card("An") và new Card("Binh") chạy trong cùng một chương trình thì hai thẻ có dùng chung số điểm không?
Đáp án: không. Mỗi lệnh new tạo một object mới với bộ field riêng, nên đổi điểm của thẻ này không làm đổi thẻ kia.
Mục tiêu bài học
Sau bài này, bạn có thể:
- Viết được class
Cardcó field và method, tạo object bằngnew, rồi đọc và ghi field bằng dấu chấm. - Giải thích được rằng biến kiểu
Cardgiữ một tham chiếu tới object, vàCard b = a;chỉ thêm một cái tên cho cùng object chứ không tạo object mới. - Dự đoán được giá trị mặc định của field và kết quả của phép
==giữa hai biến tham chiếu. - Sửa được lỗi
NullPointerExceptiondo biến hoặc ô mảng chưa trỏ tới object nào.
Bài này đo được bằng năm câu trong phần Tự kiểm tra, mỗi câu ghi rõ nó kiểm tra mục tiêu nào.
Hãy xem video dưới đây để có cái nhìn đầu tiên, sau đó mình đi chậm hơn từng bước ở các phần sau.
Bức tranh lớn (Big Picture)
Trước khi vào chi tiết, hãy nhìn ba thứ dễ bị lẫn với nhau: class, object và biến.
Cách đọc hình: ô vàng ở trên là class Card, chỉ mô tả. Hai mũi tên new Card() đi xuống tạo ra hai object, hai ô xanh lục ở giữa. Hàng dưới là ba biến: a và b cùng trỏ vào object một, còn c trỏ vào object hai. Mỗi ô đều có chữ, màu chỉ giúp bạn tìm nhanh.
Điều cần thấy: hai object ở giữa có giá trị bên trong giống nhau, nhưng là hai object khác nhau. Còn a và b là hai cái tên của cùng một object.
Ẩn dụ xuyên chương: xưởng dập thẻ của quán. Ở bài 21, class là cái khuôn dập thẻ và object là từng tấm thẻ dập ra. Bài này thêm ý thứ ba: thẻ dập xong được cất vào tủ, còn biến là mảnh giấy ghi số ngăn tủ nơi cất tấm thẻ, không phải chính tấm thẻ. Card b = a; là chép mảnh giấy, không dập thêm thẻ nào. Dự án vẫn là hệ thống thẻ thành viên của quán Cà Phê Gió. Bài 21 cho bạn thấy Card có constructor và private, bài này tạm gỡ chúng ra để thấy phần lõi, bài 23 và 24 sẽ lắp lại.
Chỗ ẩn dụ không còn đúng: mảnh giấy ghi số ngăn tủ có thể bị xoá trắng, và khi đó biến không chỉ vào ngăn nào cả. Java gọi trạng thái đó là null, và nó là nguồn của lỗi phổ biến nhất ở bài này.
Khái niệm cốt lõi
Bài này có ba khái niệm. Mỗi khái niệm đi kèm một hình.
Khái niệm 1: class gồm field và method
Class là bản mô tả một loại object. Field là dữ liệu mà mỗi object của class đều có, như owner và points. Method là việc mà mỗi object của class đều làm được, như describe(). Khai báo class chưa tạo ra tấm thẻ nào cả.
Cách đọc hình: ô vàng là class Card với ghi chú "a description, not a card yet". Ba ô bên dưới là các thành phần của nó: hai field (owner và points, ô xanh lục nhạt) và một method (describe(), ô xanh lục đậm). Mũi tên đứt sang phải chỉ rằng muốn có thẻ thật thì phải dùng new Card().
Một cách nhớ: field là danh từ (thẻ có gì), method là động từ (thẻ làm được gì). Để dùng một field hay method của một object, bạn viết tên biến, dấu chấm, rồi tên thành phần: a.points, a.describe().
Khái niệm 2: new tạo object với giá trị mặc định
new xin Java một vùng nhớ cho object mới, đặt các field vào đó rồi trả về một tham chiếu (reference) tới vùng nhớ ấy. Field chưa được gán sẽ nhận giá trị mặc định: số là 0, boolean là false, còn mọi kiểu tham chiếu như String là null, nghĩa là "chưa trỏ tới object nào".
Cách đọc hình: ô trắng bên trái là new Card(). Hai mũi tên đi tới hai field của object mới: owner có giá trị null vì String là kiểu tham chiếu, points có giá trị 0 vì là số. Ô xám bên phải ghi quy tắc chung.
Quy tắc mặc định này chỉ áp dụng cho field. Biến cục bộ trong method thì khác: Java bắt bạn gán trước khi đọc, và bạn sẽ thấy lỗi tương ứng ở phần Thử phá nó đi.
Khái niệm 3: biến giữ tham chiếu, không giữ object
Khi bạn viết Card a = new Card();, biến a không chứa tấm thẻ. Nó chứa một tham chiếu: một mũi tên tới tấm thẻ nằm ở nơi khác. Hệ quả là Card b = a; chép mũi tên chứ không chép tấm thẻ. Sau dòng đó a và b là hai cái tên của cùng một object, nên đổi qua b thì a cũng thấy. Và phép == giữa hai biến tham chiếu hỏi "hai mũi tên có chỉ vào cùng một object không", không hỏi "giá trị bên trong có bằng nhau không".
Cách đọc hình: ba ô trắng bên trái là ba biến. Hai mũi tên từ a và b cùng tới ô xanh lục (object một), mũi tên từ c tới ô vàng (object hai). Hai object có chữ giống hệt nhau, nhưng chúng là hai ô tách biệt, và đó là lý do a == c cho false.
Đây là khái niệm quan trọng nhất của bài, và là câu trả lời cho tình huống mở đầu: bản "sao" b thật ra chính là thẻ gốc.
Concept Check 1. Đoạn sau in ra gì? Hãy chọn trước khi đọc tiếp.
Card a = new Card();
a.points = 7;
Card b = a;
b.points = b.points + 1;
System.out.println(a.points);
- A. 7, vì b là một bản sao riêng nên a không bị ảnh hưởng.
- B. 8, vì a và b là hai tên của cùng một object nên đổi qua b thì a thấy.
- C. 0, vì dòng gán b = a làm các field của a quay về giá trị mặc định.
- D. Lỗi biên dịch, vì Java không cho gán một biến Card cho biến Card khác.
Đáp án: B. Dòng Card b = a; chép tham chiếu, và cả hai biến cùng trỏ vào một object. A là mô hình sai mà hình "mô hình sai và mô hình đúng" ở phần sau sẽ vẽ ra. C bịa ra một tác dụng mà phép gán không có: gán không đụng tới nội dung của object. D sai vì gán giữa hai biến cùng kiểu là hợp lệ, và chính vì nó hợp lệ nên lỗi này không bị trình biên dịch chặn.
Ví dụ đầu tiên (Worked Example) và "Xem từng bước"
Chương trình dưới đây đi qua đủ ba khái niệm: tạo thẻ, đọc giá trị mặc định, thêm một tên cho thẻ cũ, rồi so sánh.
CardRefs.java
public class CardRefs {
public static void main(String[] args) {
// Field chưa gán nhận giá trị mặc định: null và 0
Card a = new Card();
System.out.println(a.owner + " " + a.points);
a.owner = "An";
a.points = 30;
// Không có new: chỉ chép mũi tên, a và b cùng một thẻ
Card b = a;
b.points = 50;
System.out.println(a.describe());
// Thẻ thứ hai, giá trị giống nhưng là object khác
Card c = new Card();
c.owner = "An";
c.points = 50;
// == so hai mũi tên, không so giá trị bên trong
System.out.println(a == b);
System.out.println(a == c);
}
}
class Card {
String owner;
int points;
String describe() {
return owner + " has " + points;
}
}
Predict: hãy dừng lại, lấy giấy nháp và đoán: chương trình in ra bốn dòng nào? Đặc biệt, a.points bằng bao nhiêu sau dòng b.points = 50;, và hai phép == cho gì? Đoán xong rồi hãy đọc tiếp.
Kết quả mong đợi [Đã chạy, JDK 17.0.15]:
null 0
An has 50
true
false
Đọc code từng phần:
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
Card a = new Card(); | Tạo object mới với field mặc định, biến a trỏ vào nó | Vế trái khai báo biến kiểu Card, vế phải mới là thứ tạo ra object |
a.owner + " " + a.points | Đọc hai field của object mà a trỏ tới | In ra null 0 vì chưa ai gán giá trị |
a.owner = "An"; và a.points = 30; | Ghi giá trị vào hai field của object | Dấu chấm theo mũi tên của a để tới đúng object |
Card b = a; | Chép tham chiếu từ a sang b, không tạo object | Không có new, nên không có object mới nào |
b.points = 50; | Ghi 50 vào field của object mà b trỏ tới | b trỏ cùng chỗ với a, nên a.describe() cũng thấy 50 |
Card c = new Card(); rồi gán hai field | Tạo object thứ hai có giá trị giống object thứ nhất | Giá trị giống nhau không làm chúng thành một object |
a == b và a == c | So hai mũi tên | true vì cùng object, false vì khác object |
String describe() trong class Card | Ghép owner và points thành một chuỗi | Method dùng thẳng tên field vì nó chạy trên object đang gọi |
Bây giờ xem từng bước. Mỗi bước nêu dòng đang chạy, mũi tên của các biến và nội dung object:
Đang tải Animation…
Bảng trace (bản chữ tương đương của animation, [Suy luận] bằng cách đi từng dòng và khớp với kết quả chạy thật):
| Dòng chạy | Biến trỏ vào | Object 1 | Object 2 | Ghi chú |
|---|---|---|---|---|
Card a = new Card(); | a vào object 1 | null, 0 | chưa có | In null 0 |
| gán owner và points cho a | a vào object 1 | An, 30 | chưa có | Ghi qua mũi tên của a |
Card b = a; | a và b vào object 1 | An, 30 | chưa có | Không có object mới |
b.points = 50; | a và b vào object 1 | An, 50 | chưa có | In An has 50 |
Card c = new Card(); rồi gán | c vào object 2 | An, 50 | An, 50 | Hai object giống nhau về giá trị |
a == b và a == c | không đổi | An, 50 | An, 50 | In true rồi false |
Bạn vừa làm được: đọc một chương trình có hai object và ba biến, rồi nói đúng biến nào trỏ vào object nào sau từng dòng.
Concept Check xen kẽ
Chọn đáp án của bạn trước, rồi đọc phần đáp án ngay bên dưới mỗi câu.
Concept Check 2. Class Cup có ba field: boolean full;, String label;, double ml;. Chương trình chạy Cup cup = new Cup(); rồi in cup.full + " " + cup.label + " " + cup.ml. Kết quả là gì?
- A.
false "" 0, vì Java tự điền chuỗi rỗng và số nguyên cho field chưa gán. - B. Lỗi biên dịch, vì field chưa gán thì không được đọc, giống biến cục bộ trong method.
- C.
false null 0.0, vì mỗi kiểu có giá trị mặc định riêng và chuỗi mặc định là null. - D.
null null null, vì mọi field chưa gán đều mang giá trị null như nhau.
Đáp án: C. boolean mặc định false, String là kiểu tham chiếu nên mặc định null, double mặc định 0.0. A sai vì Java không tự tạo chuỗi rỗng cho String. B nhầm field với biến cục bộ: chỉ biến cục bộ mới bị bắt gán trước khi đọc. D sai vì null chỉ dành cho kiểu tham chiếu, còn số và boolean có giá trị mặc định riêng. [Đã chạy, JDK 17.0.15]
Concept Check 3. Đoạn Card[] cards = new Card[3]; cards[0].points = 5; chạy sẽ ra sao?
- A. Ghi 5 vào thẻ ở ô 0, vì new Card[3] đã tạo sẵn ba thẻ có points bằng 0.
- B. Ném ArrayIndexOutOfBoundsException, vì ô 0 chưa từng được dùng nên chưa hợp lệ.
- C. Lỗi biên dịch, vì mảng chứa object không cho truy cập field qua chỉ số.
- D. Ném NullPointerException, vì new Card[3] chỉ tạo ba ô trống, chưa có thẻ nào.
Đáp án: D. new Card[3] chỉ tạo mảng ba ô, mỗi ô là một biến tham chiếu mang giá trị null. A là hiểu lầm hay gặp nhất: tạo mảng object chưa tạo object nào, bạn phải viết new Card() cho từng ô. B sai vì chỉ số 0 hợp lệ với mảng ba ô. C sai vì cú pháp này hợp lệ, lỗi chỉ lộ lúc chạy. [Đã chạy, JDK 17.0.15]
Làm cùng mình (Guided Practice)
Bạn sẽ dựng dần chương trình CardShop qua năm bước. Mỗi bước thêm vào bước trước, và hỗ trợ giảm dần: bước 1 làm sẵn gần hết, bước 5 chỉ còn đề. Gõ code vào file CardShop.java rồi chạy bằng java CardShop.java.
Bước 1: xem giá trị mặc định.
public class CardShop {
public static void main(String[] args) {
// Field chưa gán nhận giá trị mặc định
Card a = new Card();
System.out.println("step 1: " + a.owner + " " + a.points);
}
}
class Card {
String owner;
int points;
String describe() {
return owner + " has " + points;
}
}
Chạy xong bạn sẽ thấy: step 1: null 0.
Bước 2: gán giá trị cho thẻ. Ngay dưới dòng in của bước 1, thêm:
// TODO 2: gán owner là "An" và points là 30 cho thẻ a
System.out.println("step 2: " + a.describe());
Chạy xong bạn sẽ thấy: step 2: An has 30.
Bước 3: thêm một tên cho thẻ.
Card b = a;
// TODO 3: đặt b.points là 50
System.out.println("step 3: " + a.points + " " + (a == b));
Chạy xong bạn sẽ thấy: step 3: 50 true.
Vẽ ra giấy: vẽ hai ô biến a và b, một ô object có owner và points. Vẽ trạng thái sau bước 2, rồi sau bước 3. Đối chiếu: cả hai biến đều có mũi tên tới cùng một ô object, và ô object ghi 50. Hình Card b = a ở khái niệm 3 là cùng ý.
Bước 4: tạo một thẻ giống hệt nhưng là thẻ khác. Bạn tự quyết cách viết.
// TODO 4: tạo thẻ c bằng new, owner "An", points 50
System.out.println("step 4: " + (a == c));
Chạy xong bạn sẽ thấy: step 4: false. Gợi ý: c cần dòng new Card() riêng, giống như bước đầu tiên.
Vẽ ra giấy: thêm một object thứ hai và một biến c. Đối chiếu: hai object có chữ giống nhau nhưng là hai ô tách biệt.
Bước 5: mảng thẻ. Phần này chỉ còn đề.
Card[] cards = new Card[2];
System.out.println("step 5: " + cards[0]);
// TODO 5: đưa a vào ô 0 và c vào ô 1 của mảng cards
System.out.println("step 5: " + cards[1].points);
Chạy xong bạn sẽ thấy hai dòng: step 5: null rồi step 5: 50. Nếu chưa điền, bạn sẽ gặp NullPointerException, và đó là chủ đích để bạn thấy lỗi thật. Gợi ý: mỗi ô của mảng là một biến, nên gán cards[0] = a; chỉ chép một mũi tên. Lời giải đầy đủ nằm ở cuối bài.
Bạn vừa làm được: tự dựng một chương trình với object, tham chiếu và mảng, và tự thấy == cho kết quả khác nhau cho cùng một giá trị. Toàn bộ mã nguồn của bài cũng nằm trong thư mục 06-oop/02-class-object của kho GitHub, nhưng bạn không cần mở nó để học.
Thử phá nó đi (Lỗi thường gặp)
Bốn lỗi dưới đây là những lỗi người mới gặp nhiều nhất với object. Mỗi dòng đã được chạy hoặc biên dịch thật [Đã chạy, JDK 17.0.15]. Khi chạy bằng java Ten.java, tên biến trong thông báo NullPointerException hiện dạng <local1>; biên dịch bằng javac -g thì thông báo hiện tên thật, và chữ trong thông báo có thể khác ở phiên bản Java khác.
| Code lỗi | Thông báo lỗi | Nguyên nhân bằng lời thường | Cách sửa |
|---|---|---|---|
Card d = null; rồi d.points | NullPointerException: Cannot read field "points" because "<local1>" is null | d không trỏ vào object nào, nên đi theo mũi tên là đi vào khoảng trống | Gán d = new Card(); trước khi dùng |
Card[] cards = new Card[3]; rồi cards[0].points = 5; | NullPointerException: Cannot assign field "points" because "<local2>[0]" is null | Mảng object mới tạo chỉ có các ô trống | cards[0] = new Card(); cho từng ô trước khi dùng |
Card x; rồi x.points = 5; | variable x might not have been initialized | Biến cục bộ phải được gán trước khi dùng, Java không tự điền | Viết Card x = new Card(); |
a == c khi a và c là hai thẻ cùng giá trị | Không báo lỗi, kết quả là false | == so mũi tên, không so nội dung | Chỉ dùng == để hỏi "cùng một object không" |
Lỗi thứ tư là lỗi im lặng, và nó cùng gốc với tình huống mở đầu: gán và so sánh biến tham chiếu chỉ làm việc với mũi tên. Hình dưới gom mô hình sai và đúng về dòng Card b = a;.
Cách đọc hình: hàng trên màu đỏ nhạt là mô hình sai: nhiều người tưởng Card b = a; chép cả tấm thẻ sang một thẻ mới, nên đoán a.points vẫn là 30. Hàng dưới màu xanh lục là mô hình đúng: phép gán chỉ chép mũi tên, nên a.points thành 50. Chữ trong ô đủ để hiểu, màu chỉ giúp bạn tìm nhanh.
Mô hình đúng này giải thích luôn tình huống mở đầu: muốn có bản sao thật, bạn phải tự new một thẻ mới rồi chép từng field sang, như bước 4 ở trên.
Bạn vừa làm được: đọc bốn lỗi phổ biến của object và nói được nguyên nhân bằng lời thường.
Tự tay làm (Bài tập)
Làm lần lượt từng mức. Mỗi bài có đề, dữ liệu vào, kết quả mong đợi, tiêu chí nghiệm thu và gợi ý. Lời giải nằm riêng ở cuối bài, hãy thử trước rồi mới so.
Mức Cơ bản
Bài 1. Vẽ trạng thái. Dữ liệu vào là đoạn code sau, với class Card chỉ có field int points:
Card p = new Card();
p.points = 10;
Card q = p;
q.points = q.points + 5;
Card r = new Card();
r.points = p.points;
r.points = r.points + 1;
q = r;
q.points = 0;
System.out.println(p.points + " " + q.points + " " + r.points);
Điền bảng bốn cột: dòng vừa chạy, biến trỏ vào object nào, điểm của object 1, điểm của object 2. Kết quả mong đợi: một bảng chín dòng và dòng in cuối cùng. Tiêu chí nghiệm thu: dòng in trùng với kết quả chạy thật, và bạn chỉ ra được dòng nào làm q đổi sang trỏ vào object khác. Gợi ý: dòng q = r; chép mũi tên, không chép giá trị.
Bài 2. Sửa NullPointerException. Dữ liệu vào là đoạn code sau, chạy ra NullPointerException:
Card[] cards = new Card[3];
cards[1].points = 40;
System.out.println(cards[1].points);
Kết quả mong đợi: sau khi sửa, in ra 40. Tiêu chí nghiệm thu: bạn sửa bằng một dòng new Card() đặt đúng chỗ, và nói được vì sao hai ô còn lại vẫn là null. Gợi ý: mỗi ô mảng cần được gán một object trước khi dùng.
Mức Vận dụng
Bài 3. Tìm thẻ nhiều điểm nhất. Dữ liệu vào là ba thẻ có owner C0, C1, C2, trong đó thẻ C1 có 40 điểm và hai thẻ còn lại có 0 điểm. Viết chương trình tạo ba thẻ bằng vòng for, tìm thẻ nhiều điểm nhất, cộng 10 điểm vào đúng thẻ đó, rồi in cả ba thẻ bằng describe(). Kết quả mong đợi:
C0 has 0
C1 has 50
C2 has 0
Tiêu chí nghiệm thu: ba dòng khớp từng ký tự, không có NullPointerException, và việc cộng 10 điểm chỉ dùng biến đang giữ thẻ tốt nhất, không tìm lại bằng chỉ số. Gợi ý: một biến Card best chỉ cần giữ mũi tên tới thẻ đang dẫn đầu.
Mức Thử thách (tuỳ chọn)
Bài 4. Truyền thẻ vào method. Chép static như một khuôn mẫu, bài 28 sẽ giải thích. Dữ liệu vào là hai method sau và hàm main:
static void bonus(Card card) {
// Đi theo mũi tên đã chép nên đổi được thẻ thật
card.points = card.points + 10;
}
static void replace(Card card) {
// Chỉ gán lại tham số, biến an bên ngoài không đổi
card = new Card();
card.points = 999;
}
Trong main, tạo Card an với 5 điểm, gọi bonus(an) rồi in an.points, sau đó gọi replace(an) rồi in an.points. Hãy dự đoán hai số in ra trước khi chạy. Tiêu chí nghiệm thu: bạn dự đoán đúng cả hai số và giải thích bằng hình mũi tên vì sao replace không đổi được an. Gợi ý: tham số card là một mũi tên chép riêng từ an.
Tự kiểm tra (Retrieval Practice)
Trả lời bằng trí nhớ, không xem lại bài. Mỗi câu ghi rõ nó kiểm tra mục tiêu nào trong bốn mục tiêu ở đầu bài.
Câu 1 (nhớ lại, mục tiêu 1). Class Card gồm những loại thành phần nào? Mỗi loại cho một ví dụ, và nói cách đọc hoặc gọi thành phần đó từ biến a.
Câu 2 (dự đoán kết quả code, mục tiêu 2 và 3). Class Card chỉ có field int points. Đoạn sau in ra gì ở hai dòng?
Card x = new Card();
Card y = x;
Card z = new Card();
x.points = 4;
y.points = y.points * 2;
z.points = x.points;
z.points = z.points + 1;
y = z;
y.points = y.points + 10;
System.out.println(x.points + " " + y.points + " " + z.points);
System.out.println((x == y) + " " + (y == z));
Câu 3 (dự đoán trạng thái sau nhiều bước, mục tiêu 2). Class Card chỉ có field int points. Chạy lần lượt: Card u = new Card(); Card v = new Card(); u.points = 3; v = u; v.points = 9; Card w = v; w.points = w.points + 1;. Sau cùng có mấy object đang được trỏ tới, và u.points, v.points, w.points là bao nhiêu?
Câu 4 (tìm lỗi, mục tiêu 4). Đoạn sau gặp NullPointerException ở dòng cuối. Chỉ ra lỗi và sửa.
Card[] cards = new Card[2];
cards[0] = new Card();
cards[0].points = 5;
System.out.println(cards[1].points);
Câu 5 (giải thích cho bạn chưa học, mục tiêu 2). Bạn của bạn hỏi: "Mình viết Card b = a; để tạo bản sao thẻ, sao đổi b mà a cũng đổi?" Hãy giải thích bằng hình ảnh mảnh giấy ghi số ngăn tủ cất thẻ, rồi nói cách tạo một bản sao thật.
Điều cần nhớ
- Class gồm field (dữ liệu) và method (việc cần làm); object được tạo bằng
new, và bạn đọc ghi bằng dấu chấm (mục tiêu 1). - Biến kiểu
Cardgiữ một mũi tên tới object, nênCard b = a;chỉ thêm một cái tên cho cùng object (mục tiêu 2). - Field chưa gán có giá trị mặc định (số 0,
booleanfalse, tham chiếu null), và==giữa hai tham chiếu so mũi tên, không so giá trị (mục tiêu 3). - Biến hoặc ô mảng mang giá trị
nullmà bạn đi theo bằng dấu chấm sẽ gâyNullPointerException, và cách sửa là gán object trước (mục tiêu 4).
Cheat sheet một trang.
| Việc cần làm | Cú pháp | Kết quả | Ghi chú |
|---|---|---|---|
| Tạo object | Card a = new Card(); | Một object mới, a trỏ vào | Field nhận giá trị mặc định |
| Ghi field | a.points = 30; | Đổi object mà a trỏ tới | Mọi biến cùng trỏ đều thấy |
| Đọc field | a.points | Giá trị hiện tại | Lỗi NullPointerException nếu a đang là null |
| Gọi method | a.describe() | Chuỗi mô tả | Chạy trên đúng object của a |
| Thêm một tên | Card b = a; | Không có object mới | Chép mũi tên |
| So cùng object | a == b | true hoặc false | Không so giá trị bên trong |
| Mảng object | new Card[3] | Ba ô null | Cần new Card() cho từng ô |
Cách đọc hình: mỗi hàng là một dòng code bên trái và điều nó thật sự làm bên phải. Đọc theo hàng ngang: new Card() tạo object mới, Card b = a; chỉ thêm một tên, a == c so mũi tên, new Card[3] chỉ tạo ba ô trống.
Câu hỏi để dành cho ngày mai: mỗi lần new Card() bạn phải gán owner và points bằng hai dòng riêng. Nếu quên dòng gán owner thì thẻ sẽ có owner là null. Có cách nào bắt buộc mọi thẻ phải có chủ ngay lúc được tạo không? Hãy đoán trước khi sang bài sau.
Đào sâu
Phần này dành cho bạn tò mò. Bỏ qua cũng không thiếu gì ở trên.
Bên trong bộ nhớ. Mỗi biến cục bộ như a nằm trong vùng nhớ của lần gọi method và chỉ chứa một tham chiếu, còn các object nằm ở một vùng riêng gọi là heap. Mô hình "mũi tên" trong bài đủ dùng để dự đoán đúng mọi kết quả ở trên [Suy luận từ mô hình, địa chỉ thật và cách sắp xếp vùng nhớ phụ thuộc JVM]. Khi không còn mũi tên nào trỏ tới một object, nó không còn dùng được nữa và Java sẽ tự dọn khi cần.
So với lựa chọn khác. Ngôn ngữ như C cho bạn giữ cả cấu trúc lẫn con trỏ riêng rẽ, còn Java luôn đi qua tham chiếu cho object. Kiểu nguyên thuỷ như int thì khác: biến chứa chính giá trị, nên int m = n; chép giá trị thật và đổi m không đổi n.
Hiểu lầm nâng cao. Nhiều người nghĩ Java "truyền object bằng tham chiếu". Chính xác hơn, Java luôn chép giá trị của biến vào tham số, và với object thì giá trị đó là một mũi tên. Vì vậy bonus(an) đổi được thẻ của an (đi theo mũi tên đã chép), còn replace(an) không đổi được biến an vì chỉ gán lại mũi tên trong tham số. Chạy thử hai method ở Bài 4 cho 15 rồi lại 15 [Đã chạy, JDK 17.0.15].
Giới hạn của bài. Bài này chưa nói về constructor để buộc thẻ có chủ ngay khi tạo (bài 23), về private để khoá field (bài 24), về equals để so sánh giá trị của hai object, và về cách Java dọn các object không còn mũi tên nào trỏ tới.
Đọc thêm (tài liệu chính thống, Java SE 17):
- The Java Tutorials: Classes
- The Java Tutorials: Creating Objects
- The Java Tutorials: Passing Information to a Method or a Constructor
Bài tiếp theo
Bài sau là Constructor: bạn sẽ học cách bắt buộc mọi thẻ có chủ ngay lúc được tạo, và một lỗi viết sai tên tham số khiến thẻ có owner là null mà không báo gì.
Mini-dự án nối tiếp: giữ lại file CardShop.java bạn vừa viết. Ở bài sau bạn sẽ thay hai dòng gán owner và points bằng một dòng new Card("An", 30).
Dự án chạy xuyên chương: đến cuối bài này Card có hai field owner và points đọc ghi tự do, một method describe(), và bạn biết biến chỉ là mảnh giấy chỉ tới thẻ. Bài 23 thêm khâu kiểm tra tại xưởng, tức constructor, để thẻ nào ra khỏi new cũng đã có chủ.
Lời giải và đáp án
Hãy thử xong rồi mới đọc phần này.
Lời giải Làm cùng mình. Thay các dòng // TODO bằng những dòng dưới đây, ghép lại chạy ra đúng các dòng step ở trên [Đã chạy, JDK 17.0.15]:
a.owner = "An";
a.points = 30;
b.points = 50;
Card c = new Card();
c.owner = "An";
c.points = 50;
cards[0] = a;
cards[1] = c;
| Dòng code | Máy làm gì | Lý do chọn cách này |
|---|---|---|
a.owner = "An"; và a.points = 30; | Đi theo mũi tên của a rồi ghi hai field | Phải gán sau new, vì lúc mới tạo các field còn mặc định |
b.points = 50; | Ghi 50 vào object mà b trỏ tới, chính là object của a | Đây là chỗ bạn thấy a.points đổi theo mà không đụng tới a |
Card c = new Card(); | Tạo object thứ hai | Muốn có thẻ riêng thì bắt buộc phải có new |
c.owner = "An"; và c.points = 50; | Cho object thứ hai cùng giá trị với object thứ nhất | Để thấy giá trị bằng nhau vẫn cho a == c là false |
cards[0] = a; và cards[1] = c; | Chép mũi tên vào hai ô của mảng | Ô mảng là biến tham chiếu, gán xong mới đi theo được mũi tên |
Lời giải Bài 1. [Đã chạy, in ra 15 0 0]
| Dòng vừa chạy | Biến trỏ vào | Điểm object 1 | Điểm object 2 |
|---|---|---|---|
Card p = new Card(); | p vào 1 | 0 | chưa có |
p.points = 10; | p vào 1 | 10 | chưa có |
Card q = p; | p và q vào 1 | 10 | chưa có |
q.points = q.points + 5; | p và q vào 1 | 15 | chưa có |
Card r = new Card(); | r vào 2 | 15 | 0 |
r.points = p.points; | r vào 2 | 15 | 15 |
r.points = r.points + 1; | r vào 2 | 15 | 16 |
q = r; | p vào 1, q và r vào 2 | 15 | 16 |
q.points = 0; | p vào 1, q và r vào 2 | 15 | 0 |
Dòng q = r; làm q bỏ object 1 và trỏ sang object 2. Từ đó q.points = 0 ghi vào object 2, nên p.points vẫn là 15 còn q và r cùng là 0.
Lời giải Bài 2. Thêm một dòng trước khi dùng ô 1 [Đã chạy, in ra 40]:
cards[1] = new Card();
Mảng mới tạo chỉ có các ô null. Sau dòng trên, ô 1 mới trỏ vào một thẻ thật, còn ô 0 và ô 2 vẫn là null vì bạn chưa gán gì cho chúng.
Lời giải Bài 3. [Đã chạy, ba dòng khớp với kết quả mong đợi]
Card[] cards = new Card[3];
for (int i = 0; i < cards.length; i++) {
cards[i] = new Card();
cards[i].owner = "C" + i;
}
cards[1].points = 40;
// best chỉ giữ mũi tên tới thẻ đang dẫn đầu
Card best = cards[0];
for (Card card : cards) {
if (card.points > best.points) {
best = card;
}
}
best.points = best.points + 10;
| Dòng code | Máy làm gì | Lý do chọn cách này |
|---|---|---|
cards[i] = new Card(); trong vòng for | Tạo một thẻ thật cho mỗi ô | Mảng mới chỉ có ô null, không tạo sẵn thẻ |
Card best = cards[0]; | Cho best trỏ tới thẻ ở ô 0 | Điểm khởi đầu để so sánh, không tạo thẻ mới |
if (card.points > best.points) | So điểm của thẻ đang xét với thẻ dẫn đầu | Đọc field qua hai mũi tên khác nhau |
best = card; | Cho best đổi sang trỏ tới thẻ đang xét | Chỉ chép mũi tên, rất nhẹ |
best.points = best.points + 10; | Cộng 10 vào đúng object mà best trỏ tới | Vì best và ô mảng cùng trỏ vào một thẻ, thẻ trong mảng cũng đổi |
Lời giải Bài 4. [Đã chạy, in ra 15 rồi 15] bonus(an) đi theo mũi tên đã chép vào tham số card và cộng 10 vào thẻ thật, nên an.points từ 5 lên 15. replace(an) chỉ cho tham số card trỏ sang một thẻ mới có 999 điểm. Biến an ở main là mũi tên khác, vẫn trỏ vào thẻ cũ, nên in ra 15.
Đáp án Tự kiểm tra.
- Class có field (dữ liệu, ví dụ
points, đọc bằnga.points) và method (việc làm được, ví dụdescribe(), gọi bằnga.describe()). Kiểm tra mục tiêu 1. - In ra
8 19 19rồifalse true[Đã chạy]. Từng dòng: x và y cùng trỏ object 1, sau hai dòng đầu object 1 có 8. z trỏ object 2 nhận 8 rồi cộng 1 thành 9.y = zlàm y đổi sang object 2, cộng 10 thành 19. Object 1 còn 8, object 2 là 19. Kiểm tra mục tiêu 2 và 3. - Sau cùng còn một object được trỏ tới, và
u.points,v.points,w.pointsđều là 10 [Đã chạy]. Dòngv = ubỏ object thứ hai củav, và từ đó ba biến cùng trỏ vào một object: 3, rồi 9, rồi 10. Kiểm tra mục tiêu 2. cards[1]chưa được gán nên vẫn lànull, vàcards[1].pointsđi theo một mũi tên không có đích. Sửa bằng cách thêmcards[1] = new Card();trước dòng in. Kiểm tra mục tiêu 4.- Biến chỉ là mảnh giấy ghi số ngăn tủ cất thẻ.
Card b = a;chép mảnh giấy, nên hai mảnh cùng chỉ vào một tấm thẻ, và ai đổi thẻ thì người kia cũng thấy. Muốn bản sao thật, viếtCard b = new Card();rồi chép từng field:b.owner = a.owner;vàb.points = a.points;. Kiểm tra mục tiêu 2.