Constructor
Xem thửTình huống mở đầu
Ngày khai trương chi nhánh mới, quán Cà Phê Gió phát 40 thẻ tích điểm cho khách đến sớm. Để mỗi thẻ có chủ ngay lúc được tạo, bạn viết một constructor và gán owner = owner;. Chạy thử: cả 40 thẻ in ra dòng null has 0 points. Không có lỗi biên dịch nào, không có dòng chữ đỏ nào, và dòng gán kia đọc lên nghe hoàn toàn hợp lý.
Dữ liệu An và 30 điểm bạn truyền vào đã đi đâu? Hãy giữ câu hỏi này, vì bạn sẽ gặp lại nó ở phần Thử phá nó đi và tự sửa nó chỉ bằng một từ.
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 3 trên 8). Đến bước 2, Card có field owner và points, nhưng bạn phải gán từng field bằng dòng riêng nên rất dễ quên chủ thẻ. Bài này thêm constructor để mọi thẻ có chủ ngay lúc được dập ra.
Ôn nhanh bài trước: viết Card b = a; có tạo ra một thẻ mới không?
Đáp án: không. Phép gán chỉ chép tham chiếu, nên a và b là hai cái tên của cùng một thẻ. Muốn có thẻ thứ hai thì phải có new.
Mục tiêu bài học
Sau bài này, bạn có thể:
- Viết được constructor cho
Cardvà gọi nó bằngnewđể thẻ có dữ liệu hợp lệ ngay từ lúc ra đời. - Giải thích được thứ tự khi
newchạy: object được cấp vùng nhớ với giá trị mặc định trước, rồi mới chạy thân constructor; vàthisgiúp phân biệt field với tham số cùng tên. - Viết được nhiều constructor cho một class và cho các constructor ngắn gọi chung một constructor đầy đủ bằng
this(...). - Nhận ra và sửa được ba lỗi: tên tham số trùng tên field,
new Card()không còn biên dịch sau khi tự viết constructor, vàthis(...)không đứng ở dòng đầu.
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 điều xảy ra trong khoảnh khắc ngắn giữa lúc bạn gõ new và lúc biến nhận được thẻ.
Cách đọc hình: đọc theo số bước. Bước 1 là dòng code của bạn, bước 2 là Java cấp vùng nhớ cho thẻ với giá trị mặc định (ô vàng), bước 3 là constructor kiểm tra và điền dữ liệu, bước 4 là thẻ hoàn chỉnh (ô xanh lục), bước 5 là biến nhận mũi tên tới thẻ. Mỗi ô đều có chữ, màu chỉ giúp bạn tìm nhanh.
Điều cần thấy: thẻ rỗng ở bước 2 tồn tại rất ngắn, và constructor là trạm cuối cùng trước khi biến của bạn nhận thẻ. Nhờ vậy bạn có thể bảo đảm mọi thẻ ra khỏi new đều đã hợp lệ.
Ẩn dụ xuyên chương: xưởng dập thẻ của quán. Bài 21 nói class là khuôn dập thẻ và object là tấm thẻ dập ra, bài 22 nói biến là mảnh giấy ghi số ngăn tủ cất thẻ. Bài này thêm khâu kiểm tra thẻ ngay tại xưởng: thẻ vừa dập xong phải qua kiểm tra, thẻ không đạt thì không được giao ra. Dự án vẫn là hệ thống thẻ thành viên của quán Cà Phê Gió, và Card lần này có hai field owner và points.
Ẩn dụ có chỗ không còn đúng: khâu kiểm tra ở xưởng thật có thể gửi trả thẻ lỗi lại cho khuôn, còn ở Java, thẻ không đạt thì new ném lỗi và biến không nhận gì cả.
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: constructor là đoạn code chạy khi new
Constructor là một khối code đặc biệt trong class, chạy đúng một lần cho mỗi lệnh new, và có nhiệm vụ đưa object vào trạng thái hợp lệ. Nó trông giống method nhưng có hai điểm khác: tên trùng tên class, và không có kiểu trả về, kể cả void. Tham số là các giá trị bạn đưa vào trong ngoặc khi gọi new, ví dụ "An" và 30.
Cách đọc hình: cột bên trái, ô xanh lục là constructor Card(String owner, int points), với hai ô con cho biết nó không có kiểu trả về và chỉ chạy được qua new. Cột bên phải, ô xám là method describe() để so sánh: có kiểu trả về và gọi trên thẻ đã có.
Nếu bạn không viết constructor nào, Java tự thêm một constructor rỗng không tham số, nên new Card() vẫn chạy ở bài 22. Nhưng ngay khi bạn tự viết bất kỳ constructor nào, Java không thêm nữa. Bạn sẽ thấy hệ quả này ở phần Thử phá nó đi.
Khái niệm 2: this phân biệt field với tham số cùng tên
Rất tự nhiên là đặt tham số cùng tên với field, như owner và owner. Khi đó trong constructor, tên owner đứng một mình chỉ tham số, vì Java chọn tên gần nhất. this là từ khoá chỉ chính object đang được tạo, nên this.owner là field của thẻ, còn owner là tham số bạn vừa truyền vào. Hiện tượng tên gần che tên xa này gọi là che khuất (shadowing).
Cách đọc hình: ô trắng ở trên trái là tham số owner, ô vàng ở trên phải là field this.owner. Ô xanh lục ở giữa là dòng đúng this.owner = owner;: đọc từ tham số rồi ghi vào field. Ô đỏ ở dưới là dòng sai owner = owner;: đọc tham số rồi ghi lại vào chính tham số, field không được chạm tới.
Đâ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: 40 thẻ null has 0 points là vì mọi dòng gán đều đi vòng quanh tham số.
Concept Check 1. Constructor sau, rồi chạy Card c = new Card("An", 30);. Hỏi c.owner và c.points lần lượt là gì? Hãy chọn trước khi đọc tiếp.
Card(String owner, int points) {
owner = owner;
this.points = points;
}
- A. An và 30, vì owner = owner gán tham số vào field cùng tên.
- B. null và 30, vì owner = owner chỉ chép tham số vào chính tham số.
- C. Lỗi biên dịch, vì không được viết hai tên giống nhau trong một constructor.
- D. An và 0, vì dòng đầu tiên của constructor luôn bị Java bỏ qua.
Đáp án: B. Dòng đầu chép tham số vào chính nó nên field owner vẫn là null mặc định, còn dòng hai có this. nên field points nhận 30. 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 sai vì trình biên dịch cho phép và không cảnh báo, và đó chính là chỗ nguy hiểm. D bịa ra một quy tắc không tồn tại: mọi dòng của constructor đều chạy.
Khái niệm 3: nhiều constructor dùng chung một constructor đầy đủ
Một class có thể có nhiều constructor miễn là danh sách tham số khác nhau, Java chọn cái khớp với các đối số bạn đưa vào. Để các constructor khỏi viết lại cùng một đoạn kiểm tra, constructor ngắn dùng this(...) để chuyển sang constructor đầy đủ và tự điền các giá trị mặc định. Luật bắt buộc: lời gọi this(...) phải là dòng đầu tiên của constructor.
Cách đọc hình: ô xanh lục ở dưới là constructor đầy đủ Card(String owner, int points), nơi duy nhất chứa phần kiểm tra và gán. Hai ô trắng ở trên là hai constructor ngắn: Card(String owner) chuyển về nó bằng this(owner, 0), còn Card(Card other) chuyển về nó bằng this(other.owner, other.points) để tạo một bản sao thật. Nhãn trên mũi tên là lời gọi viết trong code.
Constructor thứ ba này trả lời câu hỏi để dành ở cuối bài 22: muốn bản sao thật của thẻ thì không dùng Card b = a; mà dùng new Card(a).
Ví dụ đầu tiên (Worked Example) và "Xem từng bước"
Chương trình dưới đây dùng đủ ba khái niệm: constructor hai tham số có kiểm tra, this để phân biệt field với tham số, và constructor một tham số gọi this(...).
CardBirth.java
public class CardBirth {
public static void main(String[] args) {
Card a = new Card("An", 30);
Card b = new Card("Binh");
System.out.println(a.describe());
System.out.println(b.describe());
}
}
class Card {
String owner;
int points;
// Constructor đầy đủ: nơi duy nhất kiểm tra và gán dữ liệu
Card(String owner, int points) {
if (owner == null || owner.isBlank()) {
throw new IllegalArgumentException("owner must not be blank");
}
// this.owner là field, owner đứng một mình là tham số
this.owner = owner;
this.points = points;
}
Card(String owner) {
// Phải là dòng đầu tiên, chuyển sang constructor đầy đủ
this(owner, 0);
}
String describe() {
return owner + " has " + points + " points";
}
}
Predict: hãy dừng lại, lấy giấy nháp và đoán: chương trình in ra hai dòng nào? Và nếu new Card("Binh") được thay bằng new Card(""), chuyện gì xảy ra? Đoán xong rồi hãy đọc tiếp.
Kết quả mong đợi [Đã chạy, JDK 17.0.15]:
An has 30 points
Binh has 0 points
Đọc code từng phần:
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
new Card("An", 30) trong main | Tạo object với field mặc định, rồi chạy constructor hai tham số với "An" và 30 | Dữ liệu vào ngay lúc tạo, không cần các dòng gán rời |
Card(String owner, int points) | Khai báo constructor: tên trùng class, không có kiểu trả về | Không có void hay kiểu nào đứng trước tên |
Câu lệnh if đầu constructor và throw new IllegalArgumentException(...) | Nếu owner là null hoặc chỉ có khoảng trắng thì ném lỗi và dừng | Chặn thẻ không hợp lệ ngay tại cửa, nên new không bao giờ trả về một thẻ hỏng |
this.owner = owner; | Đọc tham số owner rồi ghi vào field của thẻ | this. để chỉ rõ vế trái là field, không phải tham số |
this.points = points; | Tương tự cho điểm | Cùng quy tắc, cùng một lý do |
Card(String owner) rồi this(owner, 0); | Chuyển sang constructor hai tham số, điểm khởi đầu là 0 | Một chỗ kiểm tra duy nhất, và this(...) phải đứng dòng đầu |
new Card("Binh") trong main | Java chọn constructor một tham số vì chỉ có một đối số String | Java chọn theo số lượng và kiểu của đối số |
Bây giờ xem từng bước. Mỗi bước nêu dòng đang chạy, constructor nào đang làm việc và trạng thái của 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 | Constructor đang chạy | owner | points | Ghi chú |
|---|---|---|---|---|
new Card("Binh") bắt đầu | chưa có | null | 0 | Object có giá trị mặc định |
vào Card(String owner) | một tham số | null | 0 | Tham số owner là Binh |
this(owner, 0); | hai tham số | null | 0 | Chuyển sang constructor đầy đủ |
| kiểm tra owner | hai tham số | null | 0 | Không ném lỗi |
this.owner = owner; | hai tham số | Binh | 0 | Ghi vào field |
this.points = points; | hai tham số | Binh | 0 | Ghi 0 vào field |
| cả hai constructor xong | không còn | Binh | 0 | new trả tham chiếu cho b |
Bạn vừa làm được: đọc một lệnh new có hai constructor nối nhau và nói đúng giá trị của từng field ở mỗi thời điểm.
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. Constructor một tham số có hai dòng: this(owner, 0); rồi System.out.println("one-arg");. Constructor hai tham số chỉ có một dòng in "two-arg" cùng các dòng gán. Chạy new Card("Binh") sẽ in gì?
- A. two-arg rồi one-arg, vì this(...) chạy trọn constructor kia rồi mới tới dòng kế tiếp.
- B. one-arg rồi two-arg, vì dòng in nằm ngay sau this nên được chạy trước.
- C. Chỉ one-arg, vì this(...) chỉ chép dữ liệu chứ không chạy phần in của constructor kia.
- D. Lỗi biên dịch, vì một constructor không được gọi constructor khác trong cùng class.
Đáp án: A. Lời gọi this(...) chạy hết thân của constructor được gọi rồi mới quay về, nên dòng in của constructor hai tham số đứng trước. Bài tập 1 cho bạn kiểm chứng thứ tự này bằng chương trình thật [Đã chạy, JDK 17.0.15]. B đảo ngược thứ tự. C nhầm this(...) với phép sao chép giá trị: nó là một lời gọi thật. D sai vì đó chính là cách dùng đúng của this(...).
Concept Check 3. Class Card có đúng một constructor là Card(String owner, int points). Chạy Card c = new Card(); thì sao?
- A. Chạy bình thường, vì Java luôn giữ sẵn constructor không tham số cho mọi class.
- B. Chạy được nhưng owner là null và points là 0, vì không có đối số nào.
- C. Ném NullPointerException lúc chạy, vì thẻ được tạo mà không có owner.
- D. Lỗi biên dịch, vì Java chỉ tự thêm constructor rỗng khi bạn chưa viết constructor nào.
Đáp án: D. Thông báo là constructor Card in class Card cannot be applied to given types; [Đã chạy, JDK 17.0.15]. A là hiểu lầm hay gặp nhất: constructor rỗng chỉ được tự thêm khi class không có constructor nào. B và C đều giả định rằng lệnh new Card() chạy được, trong khi trình biên dịch đã dừng chương trình trước khi chạy.
Làm cùng mình (Guided Practice)
Bạn sẽ dựng dần class Card có constructor qua bốn bước nhỏ. Mỗi bước thêm vào bước trước, và hỗ trợ giảm dần: bước 1 gần như làm sẵn, bước 4 chỉ còn đề. Gõ code vào file CardShop.java rồi chạy bằng java CardShop.java.
Bước 1: constructor hai tham số.
public class CardShop {
public static void main(String[] args) {
Card a = new Card("An", 30);
System.out.println("step 1: " + a.describe());
}
}
class Card {
String owner;
int points;
// Constructor: tên trùng class, không có kiểu trả về
Card(String owner, int points) {
// TODO 1: gán hai tham số vào hai field, dùng this
}
String describe() {
return owner + " has " + points;
}
}
Chạy ngay khi chưa điền, bạn sẽ thấy step 1: null has 0, đúng như tình huống mở đầu. Điền xong, bạn sẽ thấy: step 1: An has 30.
Bước 2: chặn thẻ không có chủ. Thêm đoạn sau vào cuối main, rồi viết phần kiểm tra ở đầu constructor:
try {
new Card(" ", 5);
} catch (IllegalArgumentException e) {
System.out.println("step 2: " + e.getMessage());
}
// TODO 2: ném IllegalArgumentException("owner must not be blank") nếu owner là null hoặc chỉ có khoảng trắng
Chạy xong bạn sẽ thấy: step 2: owner must not be blank. Gợi ý: phương thức isBlank() của String cho biết chuỗi có chỉ gồm khoảng trắng không.
Bước 3: constructor một tham số. Thêm vào class Card, và thêm ở cuối main hai dòng tạo new Card("Binh") rồi in describe() với nhãn step 3: :
Card(String owner) {
// TODO 3: chuyển sang constructor hai tham số với điểm khởi đầu là 0
}
Chạy xong bạn sẽ thấy: step 3: Binh has 0.
Vẽ ra giấy: vẽ hai hộp constructor và một hộp object. Ghi thứ tự chúng chạy khi gọi new Card("Binh"), và giá trị owner, points của object sau mỗi hộp. Đối chiếu: object bắt đầu null và 0, hộp một tham số chỉ chuyển giao, hộp hai tham số mới điền Binh và 0. Hình chuỗi constructor ở khái niệm 3 là cùng ý.
Bước 4: thử phá nó đi. Bạn tự quyết cách viết. Thêm vào main một dòng tạo new Card() không có đối số, chạy và đọc thông báo lỗi, rồi xoá dòng đó. Chạy xong bạn sẽ thấy lỗi biên dịch nói constructor Card in class Card cannot be applied to given types;. Gợi ý: lỗi này xuất hiện vì lớp đã có constructor do bạn viết. Lời giải đầy đủ nằm ở cuối bài.
Bạn vừa làm được: tự viết hai constructor, tự chặn dữ liệu sai và tự thấy hệ quả của việc tự viết constructor. Toàn bộ mã nguồn của bài cũng nằm trong thư mục 06-oop/03-constructor 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 constructor. Mỗi dòng đã được chạy hoặc biên dịch thật [Đã chạy, JDK 17.0.15; 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 |
|---|---|---|---|
owner = owner; rồi points = points; trong constructor | Không báo lỗi, new Card("An", 30) cho thẻ null và 0 | Tên tham số che khuất tên field, nên mỗi dòng chép tham số vào chính nó | this.owner = owner; và this.points = points; |
void Card(String owner, int points) | constructor Card in class Card cannot be applied to given types; khi gọi new Card("An", 5) | Có void đứng trước nên nó là method bình thường tên Card, và Java vẫn tự thêm constructor rỗng | Bỏ void, để tên đứng ngay trước ngoặc |
new Card() khi class chỉ có Card(String owner, int points) | constructor Card in class Card cannot be applied to given types; | Tự viết constructor thì Java không còn thêm constructor rỗng | Truyền đủ đối số, hoặc tự viết thêm Card() |
System.out.println(...) rồi mới this(owner, 0); | call to this must be first statement in constructor | Constructor khác phải được gọi trước mọi việc khác | Đưa this(...) lên dòng đầu tiên |
Lỗi đầu tiên đáng sợ nhất vì nó không báo gì cả. Hình dưới gom hiểu lầm hay gặp nhất của bài về một chỗ.
Cách đọc hình: hàng trên màu đỏ nhạt là mô hình sai: nhiều người tưởng field và tham số cùng tên là một, nên đoán thẻ chứa An và 30. Hàng dưới màu xanh lục là mô hình đúng: tham số che khuất field, dòng gán chỉ chép tham số vào chính nó, thẻ chứa null và 0 mà không có lỗi nào. Chữ trong ô đủ để hiểu, màu chỉ giúp bạn tìm nhanh.
Cách sửa chỉ một từ: viết this.owner = owner;.
Bạn vừa làm được: đọc bốn lỗi phổ biến của constructor 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ẽ thứ tự chạy. Dữ liệu vào là class Card sau và dòng Card c = new Card(); trong main:
class Card {
String owner;
int points;
Card(String owner, int points) {
this.owner = owner;
this.points = points;
System.out.println("two-arg " + owner + " " + points);
}
Card(String owner) {
this(owner, 10);
System.out.println("one-arg " + owner);
}
Card() {
this("guest");
System.out.println("no-arg");
}
}
Điền bảng gồm ba cột: bước, constructor đang chạy, dòng được in nếu có. Kết quả mong đợi: một bảng bảy dòng. Tiêu chí nghiệm thu: ba dòng in ra đúng thứ tự khi chạy thật, và bạn giải thích được vì sao chúng in theo thứ tự ngược với thứ tự gọi. Gợi ý: mỗi this(...) chờ constructor được gọi chạy xong rồi mới đi tiếp.
Bài 2. Sửa lỗi che khuất. Dữ liệu vào là constructor sau, chạy new Card("An", 30) rồi in owner và points:
Card(String owner, int points) {
owner = owner;
points = points;
}
Kết quả mong đợi: trước khi sửa in null 0, sau khi sửa in An 30. Tiêu chí nghiệm thu: bạn sửa bằng hai chỗ thêm this. và nói được vì sao trước khi sửa không có lỗi biên dịch. Gợi ý: this. đi trước tên field, không đi trước tên tham số.
Mức Vận dụng
Bài 3. Bản sao thật cho thẻ. Viết class Card có constructor hai tham số kiểm tra owner không trống và points không âm, và constructor Card(Card other) dùng this(...) để tạo bản sao. Dữ liệu vào là hàm main sau:
Card a = new Card("An", 30);
Card copy = new Card(a);
copy.points = 99;
System.out.println(a.describe());
System.out.println(copy.describe());
System.out.println(a == copy);
Sau đó thêm hai khối try thử new Card(" ", 5) và new Card("An", -1), mỗi khối in ra thông điệp lỗi. Kết quả mong đợi:
An has 30 points
An has 99 points
false
owner must not be blank
points must not be negative: -1
Tiêu chí nghiệm thu: năm dòng khớp từng ký tự, và phần kiểm tra chỉ được viết ở một constructor. Gợi ý: Card(Card other) chỉ cần một dòng.
Mức Thử thách (tuỳ chọn)
Bài 4. Thêm hạng thẻ. Thêm field String tier (hạng thẻ) vào Card, với ba constructor: ba tham số (owner, points, tier) chứa mọi phần kiểm tra, hai tham số cho hạng mặc định "bronze", một tham số cho 0 điểm. Dữ liệu vào là ba lệnh new Card("An", 30, "gold"), new Card("Binh", 5), new Card("Chi") và một lệnh thử new Card("Dung", 1, " "). Kết quả mong đợi:
An has 30 points, tier gold
Binh has 5 points, tier bronze
Chi has 0 points, tier bronze
tier must not be blank
Tiêu chí nghiệm thu: bốn dòng khớp, và nơi kiểm tra owner chỉ xuất hiện một lần trong cả class. Gợi ý: hai constructor ngắn nối thành chuỗi, mỗi cái gọi cái kế tiếp.
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). Nêu hai điểm khác nhau giữa constructor và method thường về cú pháp, và nói constructor chạy khi nào.
Câu 2 (dự đoán kết quả code, mục tiêu 2). Class Card có hai field String owner và int points, và một constructor duy nhất viết là Card(String owner, int points) { this.owner = owner; points = points; }. Chạy Card c = new Card("An", 30); rồi in c.owner + " " + c.points thì ra gì?
Câu 3 (dự đoán trạng thái sau nhiều bước, mục tiêu 2 và 3). Card(String owner) là this(owner, 5); còn Card(String owner, int points) gán hai field bằng this.. Viết giá trị của owner và points của object ở bốn thời điểm khi chạy new Card("Z"): ngay sau khi được cấp vùng nhớ, khi vừa vào Card(String owner), khi vừa vào constructor hai tham số, và khi new trả về.
Câu 4 (tìm lỗi, mục tiêu 4). Class Card chỉ có Card(String owner, int points). Dòng Card c = new Card("An"); không biên dịch được. Nêu hai cách sửa khác nhau.
Câu 5 (giải thích cho bạn chưa học, mục tiêu 1 và 4). Bạn của bạn hỏi: "Hôm qua new Card() chạy được, hôm nay mình thêm một constructor có tham số thì nó báo lỗi. Sao vậy?" Hãy giải thích và nói cách để cả hai kiểu gọi cùng chạy được.
Điều cần nhớ
- Constructor có tên trùng class, không có kiểu trả về, và chạy một lần cho mỗi
newđể đưa thẻ vào trạng thái hợp lệ (mục tiêu 1). newcấp vùng nhớ với giá trị mặc định trước, rồi chạy constructor; vàthis.ownerlà field cònownerđứng một mình là tham số (mục tiêu 2).- Constructor ngắn gọi
this(...)ở dòng đầu để chuyển về constructor đầy đủ, nên luật kiểm tra chỉ viết một chỗ (mục tiêu 3). - Ba lỗi cần nhận ra:
owner = owner;im lặng,new Card()hết biên dịch khi tự viết constructor, vàthis(...)không đứng dòng đầu (mục tiêu 4).
Cheat sheet một trang.
| Việc cần làm | Cú pháp | Kết quả | Ghi chú |
|---|---|---|---|
| Khai báo constructor | Card(String owner, int points) { ... } | Chạy khi có new | Không có kiểu trả về |
| Gán field | this.owner = owner; | Ghi vào field của thẻ | Cần this. khi tên trùng |
| Chặn dữ liệu sai | throw new IllegalArgumentException("...") | new ném lỗi, thẻ không được tạo | Viết ở đầu constructor đầy đủ |
| Constructor ngắn | this(owner, 0); | Chuyển sang constructor đầy đủ | Phải ở dòng đầu |
| Tạo bản sao thật | new Card(other) | Một thẻ mới cùng giá trị | Cần constructor Card(Card other) |
Cần cả new Card() | Tự viết thêm Card() { ... } | Cho phép gọi không đối số | Java không tự thêm nữa |
Cách đọc hình: mỗi hàng là một nhu cầu bên trái và thứ cần viết bên phải. Đọc theo hàng ngang: cần dữ liệu ngay từ đầu thì viết constructor có tham số, tên trùng thì dùng this., có giá trị mặc định thì viết constructor ngắn gọi this(...), và nhớ rằng tự viết constructor thì new Card() không còn tự có.
Câu hỏi để dành cho ngày mai: constructor đã chặn được new Card(" ", 5), nhưng sau khi thẻ ra đời, ai đó vẫn viết được a.points = -500; từ bên ngoài. Có cách nào ngăn điều đó không? Hãy nghĩ 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 lệnh new. Thứ tự thật có thêm một khâu mình đã bỏ qua: nếu field có giá trị khởi tạo ngay chỗ khai báo, ví dụ int points = 5;, giá trị đó được gán trước khi thân constructor chạy. Một lần thử của người biên soạn với class có int points = 5; và constructor in ra owner cùng points ở dòng đầu cho null 5, rồi sau khi points = points + 1; thì c.points là 6 [Đã chạy, JDK 17.0.15]. Vậy thứ tự đầy đủ là: giá trị mặc định, rồi giá trị khởi tạo tại chỗ khai báo, rồi thân constructor.
So với lựa chọn khác. Khi nhiều constructor có cùng kiểu tham số khó phân biệt, ví dụ hai số nguyên, người ta hay viết một method static có tên rõ nghĩa để tạo object. Bài này chưa dùng static, nên bạn chỉ cần biết lựa chọn đó tồn tại.
Hiểu lầm nâng cao. Nhiều người nghĩ constructor là thứ tạo ra object. Chính xác hơn, new tạo object và cấp vùng nhớ, còn constructor chỉ khởi tạo nó. Bằng chứng là ngay đầu constructor, this.owner đã là null chứ không phải "chưa tồn tại" như đã thấy ở phần trên. Một chi tiết liên quan: field khai báo final phải được gán trong mọi constructor, nếu không thì có lỗi variable owner might not have been initialized [Đã chạy, JDK 17.0.15].
Giới hạn của bài. Bài này chưa nói về constructor của class cha và từ khoá super(...) (bài 25), về cách giấu field để không ai gán -500 từ ngoài (bài 24), và về khối khởi tạo (initializer block).
Đọc thêm (tài liệu chính thống, Java SE 17):
- The Java Tutorials: Providing Constructors for Your Classes
- The Java Tutorials: Using the this Keyword
- The Java Language Specification, Java SE 17: Constructor Declarations
Bài tiếp theo
Bài sau là Đóng gói: bạn sẽ khoá field points bằng private và chỉ cho đổi điểm qua các method có luật, để không ai gán điểm âm từ bên ngoài.
Mini-dự án nối tiếp: giữ lại class Card có constructor bạn vừa viết. Ở bài sau bạn sẽ đổi field của nó thành private và thêm hai method earn và redeem.
Dự án chạy xuyên chương: đến cuối bài này Card có constructor hai tham số kiểm tra dữ liệu, constructor một tham số cho điểm khởi đầu 0 và constructor sao chép. Bài 24 đặt điểm vào két sau quầy: field points thành private, chỉ đổi được qua earn và redeem.
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 ba dòng step ở trên [Đã chạy, JDK 17.0.15]:
this.owner = owner;
this.points = points;
// Chặn dữ liệu sai trước khi gán
if (owner == null || owner.isBlank()) {
throw new IllegalArgumentException("owner must not be blank");
}
// Dòng đầu của constructor một tham số
this(owner, 0);
| Dòng code | Máy làm gì | Lý do chọn cách này |
|---|---|---|
this.owner = owner; và this.points = points; | Chép hai tham số vào hai field của thẻ | Có this. để vế trái là field, tránh lỗi che khuất |
if (owner == null ...) rồi throw | Nếu owner không dùng được thì ném lỗi và dừng constructor | Đặt trước hai dòng gán để thẻ lỗi không bao giờ có dữ liệu |
owner.isBlank() | Cho true nếu chuỗi rỗng hoặc chỉ có khoảng trắng | Bắt được cả " " mà owner == null bỏ sót |
this(owner, 0); trong Card(String owner) | Chuyển sang constructor hai tham số với 0 điểm | Dùng lại phần kiểm tra, và phải ở dòng đầu |
Bước 4: dòng cần viết là new Card(); trong main. Trình biên dịch dừng với constructor Card in class Card cannot be applied to given types; [Đã chạy].
Lời giải Bài 1. [Đã chạy, in ra two-arg guest 10, one-arg guest, no-arg rồi guest 10]
| Bước | Constructor đang chạy | Dòng được in |
|---|---|---|
| 1 | new cấp object với giá trị mặc định | không có |
| 2 | Card() bắt đầu, dòng đầu là this("guest") | không có |
| 3 | Card(String owner) bắt đầu, dòng đầu là this(owner, 10) | không có |
| 4 | Card(String owner, int points) gán hai field | two-arg guest 10 |
| 5 | quay về Card(String owner), chạy dòng còn lại | one-arg guest |
| 6 | quay về Card(), chạy dòng còn lại | no-arg |
| 7 | main in hai field của c | guest 10 |
Dòng in nằm sau this(...) nên chỉ chạy khi constructor được gọi đã xong, vì vậy thứ tự in ngược với thứ tự gọi.
Lời giải Bài 2. [Đã chạy, trước khi sửa in null 0, sau khi sửa in An 30]
Sửa thành this.owner = owner; và this.points = points;. Trước khi sửa, hai dòng gán đều chép tham số vào chính nó, và trình biên dịch cho phép vì đó là phép gán hợp lệ.
Lời giải Bài 3. [Đã chạy, năm dòng khớp với kết quả mong đợi]
Card(String owner, int points) {
if (owner == null || owner.isBlank()) {
throw new IllegalArgumentException("owner must not be blank");
}
if (points < 0) {
throw new IllegalArgumentException("points must not be negative: " + points);
}
this.owner = owner;
this.points = points;
}
Card(Card other) {
this(other.owner, other.points);
}
| Dòng code | Máy làm gì | Lý do chọn cách này |
|---|---|---|
hai khối if rồi throw | Chặn owner trống và điểm âm trước khi gán | Mọi thẻ ra đời đều hợp lệ |
this.owner = owner; và this.points = points; | Ghi dữ liệu đã kiểm tra | Chỉ chạy khi cả hai khối if đều qua |
Card(Card other) rồi this(other.owner, other.points); | Lấy hai giá trị của thẻ cũ để tạo thẻ mới | Tạo object mới thật, nên đổi copy không đổi a, và phần kiểm tra không phải viết lại |
Lời giải Bài 4. [Đã chạy, bốn dòng khớp với kết quả mong đợi] Constructor ba tham số có hai khối kiểm tra, một cho owner giống bài 3 và một cho tier, rồi ba dòng gán this.. Hai constructor còn lại chỉ có một dòng:
Card(String owner, int points) {
this(owner, points, "bronze");
}
Card(String owner) {
this(owner, 0);
}
Chuỗi Card(String) gọi Card(String, int) rồi gọi Card(String, int, String), nên mọi kiểm tra chỉ nằm ở constructor ba tham số.
Đáp án Tự kiểm tra.
- Constructor có tên trùng class và không có kiểu trả về (kể cả
void), còn method thường có kiểu trả về và tên bất kỳ. Constructor chạy đúng một lần cho mỗi lệnhnew. Kiểm tra mục tiêu 1. - In ra
An 0[Đã chạy]. Dòng đầu cóthis.nên fieldownernhận An, dòng hai thiếuthis.nên chép tham sốpointsvào chính nó và field giữ 0 mặc định. Kiểm tra mục tiêu 2. - Ngay sau cấp vùng nhớ: owner null, points 0. Khi vừa vào
Card(String owner): vẫn null và 0. Khi vừa vào constructor hai tham số: vẫn null và 0, tham số là Z và 5. Khinewtrả về: Z và 5 [Suy luận]. Kiểm tra mục tiêu 2 và 3. - Cách một: truyền đủ hai đối số,
new Card("An", 0). Cách hai: tự viết thêmCard(String owner) { this(owner, 0); }. Kiểm tra mục tiêu 4. - Khi class không có constructor nào, Java tự thêm constructor rỗng nên
new Card()chạy. Khi bạn tự viết bất kỳ constructor nào, Java không thêm nữa nênnew Card()hết biên dịch. Để cả hai cùng chạy, tự viết thêmCard(), ví dụCard() { this("guest", 0); }. Kiểm tra mục tiêu 1 và 4.