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 Card và gọi nó bằng new để thẻ có dữ liệu hợp lệ ngay từ lúc ra đời.
  • Giải thích được thứ tự khi new chạ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à this giú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.

java
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

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

text
An has 30 points
Binh has 0 points

Đọc code từng phần:

Dòng codeMáy làm gìVì sao viết như vậy
new Card("An", 30) trong mainTạo object với field mặc định, rồi chạy constructor hai tham số với "An" và 30Dữ 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ừngChặ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ểmCù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à 0Một chỗ kiểm tra duy nhất, và this(...) phải đứng dòng đầu
new Card("Binh") trong mainJava chọn constructor một tham số vì chỉ có một đối số StringJava 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ạyConstructor đang chạyownerpointsGhi chú
new Card("Binh") bắt đầuchưa cónull0Object có giá trị mặc định
vào Card(String owner)một tham sốnull0Tham số owner là Binh
this(owner, 0);hai tham sốnull0Chuyển sang constructor đầy đủ
kiểm tra ownerhai tham sốnull0Không ném lỗi
this.owner = owner;hai tham sốBinh0Ghi vào field
this.points = points;hai tham sốBinh0Ghi 0 vào field
cả hai constructor xongkhông cònBinh0new 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ố.

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

java
        try {
            new Card(" ", 5);
        } catch (IllegalArgumentException e) {
            System.out.println("step 2: " + e.getMessage());
        }
java
        // 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: :

java
    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ỗiThông báo lỗiNguyên nhân bằng lời thườngCách sửa
owner = owner; rồi points = points; trong constructorKhông báo lỗi, new Card("An", 30) cho thẻ null và 0Tê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ỗngBỏ 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ỗngTruyề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 constructorConstructor 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:

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

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

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

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

text
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).
  • new cấp vùng nhớ với giá trị mặc định trước, rồi chạy constructor; và this.owner là field còn owner đứ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àmCú phápKết quảGhi chú
Khai báo constructorCard(String owner, int points) { ... }Chạy khi có newKhông có kiểu trả về
Gán fieldthis.owner = owner;Ghi vào field của thẻCần this. khi tên trùng
Chặn dữ liệu saithrow new IllegalArgumentException("...")new ném lỗi, thẻ không được tạoViết ở đầu constructor đầy đủ
Constructor ngắnthis(owner, 0);Chuyển sang constructor đầy đủPhải ở dòng đầu
Tạo bản sao thậtnew 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
Hình: tóm tắt cần viết gì cho từng nhu cầu của constructor

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

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

java
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 codeMá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 throwNế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ắngBắ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ểmDù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ướcConstructor đang chạyDòng được in
1new cấp object với giá trị mặc địnhkhông có
2Card() bắt đầu, dòng đầu là this("guest")không có
3Card(String owner) bắt đầu, dòng đầu là this(owner, 10)không có
4Card(String owner, int points) gán hai fieldtwo-arg guest 10
5quay về Card(String owner), chạy dòng còn lạione-arg guest
6quay về Card(), chạy dòng còn lạino-arg
7main in hai field của cguest 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]

java
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 codeMáy làm gìLý do chọn cách này
hai khối if rồi throwChặn owner trống và điểm âm trước khi gánMọi thẻ ra đời đều hợp lệ
this.owner = owner; và this.points = points;Ghi dữ liệu đã kiểm traChỉ 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ớiTạ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:

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

  1. 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ệnh new. Kiểm tra mục tiêu 1.
  2. In ra An 0 [Đã chạy]. Dòng đầu có this. nên field owner nhận An, dòng hai thiếu this. nên chép tham số points vào chính nó và field giữ 0 mặc định. Kiểm tra mục tiêu 2.
  3. 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. Khi new trả về: Z và 5 [Suy luận]. Kiểm tra mục tiêu 2 và 3.
  4. Cách một: truyền đủ hai đối số, new Card("An", 0). Cách hai: tự viết thêm Card(String owner) { this(owner, 0); }. Kiểm tra mục tiêu 4.
  5. 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ên new Card() hết biên dịch. Để cả hai cùng chạy, tự viết thêm Card(), ví dụ Card() { this("guest", 0); }. Kiểm tra mục tiêu 1 và 4.