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 Card có field và method, tạo object bằng new, rồi đọc và ghi field bằng dấu chấm.
  • Giải thích được rằng biến kiểu Card giữ 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 NullPointerException do 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.

java
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

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]:

text
null 0
An has 50
true
false

Đọc code từng phần:

Dòng codeMá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ớiIn 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 objectDấ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 objectKhô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ớib 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 fieldTạo object thứ hai có giá trị giống object thứ nhấtGiá trị giống nhau không làm chúng thành một object
a == b và a == cSo hai mũi têntrue vì cùng object, false vì khác object
String describe() trong class CardGhép owner và points thành một chuỗiMethod 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ạyBiến trỏ vàoObject 1Object 2Ghi chú
Card a = new Card();a vào object 1null, 0chưa cóIn null 0
gán owner và points cho aa vào object 1An, 30chưa cóGhi qua mũi tên của a
Card b = a;a và b vào object 1An, 30chưa cóKhông có object mới
b.points = 50;a và b vào object 1An, 50chưa cóIn An has 50
Card c = new Card(); rồi gánc vào object 2An, 50An, 50Hai object giống nhau về giá trị
a == b và a == ckhông đổiAn, 50An, 50In 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.

java
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:

java
        // 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ẻ.

java
        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.

java
        // 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 đề.

java
        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ỗiThông báo lỗiNguyên nhân bằng lời thườngCách sửa
Card d = null; rồi d.pointsNullPointerException: Cannot read field "points" because "<local1>" is nulld không trỏ vào object nào, nên đi theo mũi tên là đi vào khoảng trốngGá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 nullMảng object mới tạo chỉ có các ô trốngcards[0] = new Card(); cho từng ô trước khi dùng
Card x; rồi x.points = 5;variable x might not have been initializedBiến cục bộ phải được gán trước khi dùng, Java không tự điềnViế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 dungChỉ 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:

java
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:

java
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:

text
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:

java
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?

java
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.

java
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 Card giữ một mũi tên tới object, nên Card 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, boolean false, 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ị null mà bạn đi theo bằng dấu chấm sẽ gây NullPointerException, 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àmCú phápKết quảGhi chú
Tạo objectCard a = new Card();Một object mới, a trỏ vàoField nhận giá trị mặc định
Ghi fielda.points = 30;Đổi object mà a trỏ tớiMọi biến cùng trỏ đều thấy
Đọc fielda.pointsGiá trị hiện tạiLỗi NullPointerException nếu a đang là null
Gọi methoda.describe()Chuỗi mô tảChạy trên đúng object của a
Thêm một tênCard b = a;Không có object mớiChép mũi tên
So cùng objecta == btrue hoặc falseKhông so giá trị bên trong
Mảng objectnew Card[3]Ba ô nullCần new Card() cho từng ô
Hình: tóm tắt mỗi dòng code làm gì với thẻ và mũi tên

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):

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]:

java
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 codeMá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 fieldPhả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ứ haiMuố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ạyBiến trỏ vàoĐiểm object 1Điểm object 2
Card p = new Card();p vào 10chưa có
p.points = 10;p vào 110chưa có
Card q = p;p và q vào 110chưa có
q.points = q.points + 5;p và q vào 115chưa có
Card r = new Card();r vào 2150
r.points = p.points;r vào 21515
r.points = r.points + 1;r vào 21516
q = r;p vào 1, q và r vào 21516
q.points = 0;p vào 1, q và r vào 2150

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]:

java
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]

java
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 codeMáy làm gìLý do chọn cách này
cards[i] = new Card(); trong vòng forTạ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étChỉ 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ớiVì 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.

  1. Class có field (dữ liệu, ví dụ points, đọc bằng a.points) và method (việc làm được, ví dụ describe(), gọi bằng a.describe()). Kiểm tra mục tiêu 1.
  2. In ra 8 19 19 rồi false 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 = z là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.
  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òng v = u bỏ object thứ hai của v, 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.
  4. 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êm cards[1] = new Card(); trước dòng in. Kiểm tra mục tiêu 4.
  5. 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ết Card 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.