Java — Từ Zero đến Senior/OOP cơ bản — class, object, encapsulation/Constructor và `this` — khởi tạo object đúng cách
2/7
~16 phútOOP cơ bản — class, object, encapsulation

Constructor và `this` — khởi tạo object đúng cách

Constructor là gì, default constructor, overload constructor, this() chaining, this. để phân biệt field với parameter, và vì sao không return được trong constructor.

Bài trước bạn tạo object bằng new Book() rồi gán từng field một dòng. Nhưng bắt người dùng class phải nhớ gán đủ 5 field mỗi lần là thiết kế kém — dễ quên, dễ để object ở trạng thái dở. Constructor là cơ chế Java đảm bảo object luôn khởi tạo đúng, đầy đủ, ngay khi new.

Bài này giải thích cú pháp constructor, default constructor mà Java cấp miễn phí (và khi nào nó biến mất), overload + chaining bằng this(...), và vì sao this.field = field là pattern phổ biến ai cũng gặp.

1. Analogy — đơn đặt hàng máy tính

Khi mua máy tính lắp ráp, bạn điền form đặt hàng: CPU, RAM, ổ cứng, card đồ họa. Cửa hàng không giao máy "rỗng" rồi bảo bạn tự cắm từng linh kiện — họ lắp xong trả máy hoàn chỉnh. Form = tham số constructor; máy hoàn chỉnh = object vừa tạo.

Đời thườngJava
Form đặt hàngParameter constructor
Cửa hàng lắp rápBody constructor
Máy đã hoàn chỉnh trao tayObject vừa new xong

💡 💡 Cách nhớ

Constructor = method đặc biệt chạy một lần khi object được tạo. Mục đích: đảm bảo object ra đời ở trạng thái hợp lệ, sẵn sàng dùng.

2. Cú pháp constructor

public class Book {
    String title;
    String author;
    int year;

    // Constructor — khong return type, ten trung ten class
    public Book(String title, String author, int year) {
        this.title = title;
        this.author = author;
        this.year = year;
    }
}

// Su dung
Book b = new Book("Clean Code", "Robert C. Martin", 2008);
System.out.println(b.title);   // Clean Code

Đặc điểm của constructor:

  • Không có return type (không void, không int, không gì cả).
  • Tên trùng tên class — kể cả chữ hoa/chữ thường.
  • Gọi bằng new ClassName(args...).
  • Có thể có nhiều constructor (overload — bài sau).

2.1 Vì sao không có return type?

Constructor không return giá trị — nó chỉ khởi tạo object mà new sẽ trả về. Nếu bạn viết public void Book(...), Java hiểu đó là method bình thường tên Book, không phải constructor. Compile được nhưng object tạo qua new Book(...) sẽ không chạy method đó, dễ bug khó lần.

public class Book {
    // Khong phai constructor — day la method ten "Book"
    public void Book(String title) {
        this.title = title;
    }
}

Book b = new Book();        // goi default constructor, title van null
b.Book("Java");              // goi method Book — gan duoc nhung phai goi tay

3. Default constructor — Java cho không

Nếu class không khai báo constructor nào, Java tự động thêm một constructor public không tham số:

public class Empty { }

// Ngam gan voi:
public class Empty {
    public Empty() { }
}

Empty e = new Empty();   // OK

Constructor mặc định không làm gì — chỉ để bạn có thể new object. Field vẫn lấy giá trị default (0, false, null).

Nhưng nếu bạn khai báo bất kỳ constructor nào — dù chỉ có 1 phiên bản — Java không thêm default nữa:

public class Book {
    String title;

    public Book(String title) { this.title = title; }
}

Book a = new Book("Java");   // OK
Book b = new Book();          // COMPILE ERROR — khong co constructor khong tham so

Muốn cả 2 form, khai báo cả 2:

public Book() { }
public Book(String title) { this.title = title; }

⚠️ ⚠️ Default constructor biến mất là lỗi phổ biến khi refactor

Bạn viết class có default constructor, framework dùng (vd Jackson JSON deserialize yêu cầu no-arg constructor). Rồi bạn thêm constructor public Book(String title) — Java không generate default nữa — framework fail với "no default constructor". Luôn thêm lại public Book() {} nếu cần.

4. this — reference tới object đang khởi tạo / đang chạy method

Trong constructor và instance method, từ khóa this trỏ tới chính object đang chạy.

public class Book {
    String title;

    public Book(String title) {
        this.title = title;   // this.title = field; title = parameter
    }
}

Parameter title shadow field title. Trong body, viết title ám chỉ parameter. Dùng this.title để ép hiểu "field của object này".

Pattern this.x = x cực kỳ phổ biến trong Java — đặt parameter cùng tên field cho rõ ý, dùng this. phân biệt. Không phải anti-pattern.

4.1 Có thể bỏ this. khi không bị shadow

public class Book {
    String title;

    public Book(String t) {
        title = t;         // OK — khong co shadow, title hieu la field
        this.title = t;    // cung OK nhung du thua
    }
}

Nhưng nhiều team có rule "luôn dùng this. cho field access" để code nhất quán và tránh nhầm với local. Convention tuỳ codebase.

5. Overload constructor

Có nhiều constructor với signature khác nhau:

public class Book {
    String title;
    String author;
    int year;

    public Book(String title) {
        this.title = title;
        this.author = "Unknown";
        this.year = 0;
    }

    public Book(String title, String author) {
        this.title = title;
        this.author = author;
        this.year = 0;
    }

    public Book(String title, String author, int year) {
        this.title = title;
        this.author = author;
        this.year = year;
    }
}

Người gọi chọn constructor phù hợp với lượng thông tin họ có:

new Book("Java");
new Book("Java", "Bloch");
new Book("Java", "Bloch", 2018);

Vấn đề: 3 constructor có code lặp. Constructor 1 và 2 thực chất chỉ là "gọi constructor 3 với default cho phần thiếu". Java cho phép gọi constructor khác bằng this(...).

6. this(...) — constructor chaining

public class Book {
    String title;
    String author;
    int year;

    public Book(String title) {
        this(title, "Unknown", 0);         // goi constructor 3
    }

    public Book(String title, String author) {
        this(title, author, 0);             // goi constructor 3
    }

    public Book(String title, String author, int year) {
        this.title = title;
        this.author = author;
        this.year = year;
    }
}

Rule:

  • this(...) phải là statement đầu tiên trong constructor.
  • Chỉ 1 this(...) mỗi constructor (không chain vòng).
  • Cấm gọi this(...) sau code khác, cấm gọi nhiều lần.

Lý do rule "phải đầu tiên": Java đảm bảo chain constructor chạy theo thứ tự từ cha đến con (khi có kế thừa — module sau). Cho phép code chạy trước this(...) sẽ phá đảm bảo đó.

7. Cấm return <value> trong constructor

public class Book {
    public Book(String title) {
        if (title == null) return;   // OK — return; (void return) hop le
        // return false;              // COMPILE ERROR — constructor khong tra gia tri
        this.title = title;
    }
}

return; (không giá trị) được phép để thoát constructor sớm. return <value>; thì không. Vì constructor không có return type — không có gì để trả.

Nhưng pattern "return sớm để bỏ qua khởi tạo" hay dẫn đến object dở dang — object đã được cấp memory, chạy constructor nửa chừng, rồi về với reference hợp lệ nhưng field chưa gán đủ. Thay vì return sớm, nên ném exception:

public Book(String title) {
    if (title == null) throw new IllegalArgumentException("title must not be null");
    this.title = title;
}

Constructor ném exception → new không trả reference → caller nhận exception, object không bao giờ "tồn tại".

8. Cơ chế bên dưới — constructor compile thành method <init>

Bytecode không có khái niệm "constructor" riêng. Compiler sinh method tên đặc biệt <init> cho mỗi constructor. new = cấp memory + gọi invokespecial <init>.

new Book
dup
ldc "Java"
invokespecial Book.<init>(String)V
  • new — cấp memory, để reference trên operand stack.
  • dup — nhân đôi reference (1 để trả về, 1 để pass vào <init>).
  • invokespecial — gọi <init> với argument "Java". Return type V = void.

Hiểu điều này giải thích tại sao constructor không là instance method đầy đủ (không override được, không polymorphic) — invokespecial dispatch tĩnh, khác invokevirtual.

9. Pitfall tổng hợp

Nhầm 1: Thêm void vào trước tên class.

public void Book(String title) { this.title = title; }   // day la method, khong phai constructor

✅ Constructor không có return type. Xóa void.

Nhầm 2: Thêm constructor rồi quên giữ default.

public Book(String title) { ... }
// Framework Jackson goi new Book() -> crash

✅ Thêm public Book() { } lại nếu cần.

Nhầm 3: Gán parameter cho chính nó.

public Book(String title) { title = title; }   // tu gan — field van null

this.title = title;.

Nhầm 4: this(...) không phải statement đầu tiên.

public Book(String t) {
    System.out.println("init");
    this(t, 0);   // COMPILE ERROR
}

✅ Chuyển this(...) lên đầu.

Nhầm 5: Throw trong constructor rồi field ở trạng thái dở.

public Book(String title) {
    this.title = title;
    if (title.length() > 100) throw new IllegalArgumentException(...);
    // neu throw, object khong duoc return -> khong van de voi caller
    // nhung neu co this register vao static list truoc, se leak
}

✅ Validate trước khi gán/register. Constructor ném exception → caller không lấy được reference → an toàn.

10. 📚 Deep Dive Oracle

ℹ️ 📚 Deep Dive Oracle (optional)

Spec / reference chính thức:

Ghi chú: JLS §8.8.9 nói default constructor kế thừa access modifier từ classpublic class Foo → default constructor public; class Foo (package-private) → default constructor package-private. Chi tiết quan trọng khi viết library công khai.

11. Tóm tắt

  • Constructor = method đặc biệt khởi tạo object. Cùng tên class, không return type.
  • Gọi bằng new ClassName(args...). JVM cấp memory → gọi constructor → trả reference.
  • Nếu không khai báo constructor nào, Java cho default no-arg. Khai báo constructor khác → default biến mất.
  • this trỏ tới object hiện tại. this.field = field là pattern chuẩn để phân biệt field với parameter cùng tên.
  • Overload constructor được — signature khác nhau.
  • this(...) gọi constructor khác cùng class. Phải là statement đầu tiên.
  • return; void được phép; return <value>; không được.
  • Validate trong constructor bằng throw exception, không return sớm — tránh object dở dang.
  • Bytecode: constructor compile thành <init>, gọi bằng invokespecial.

12. Tự kiểm tra

Tự kiểm tra
Q1
Đoạn sau có chạy đúng ý không?
public class User {
    String name;
    public void User(String name) {
        this.name = name;
    }
}

User u = new User("Alice");
System.out.println(u.name);

Không chạy đúng — in null.

public void User(String name)void → compiler hiểu đó là method bình thường tên User, không phải constructor. Class User không khai báo constructor nào thực sự → Java thêm default public User().

new User("Alice") gọi constructor với 1 String → Java báo compile error "cannot find constructor User(String)". Tức là đoạn trên không compile, chứ chưa đến runtime.

Fix: xóa void → đúng thành constructor.

Q2
Đoạn sau in gì?
public class Box {
    int x;
    int y;
    public Box() { this(10, 20); }
    public Box(int x, int y) { this.x = x; this.y = y; }
}

Box a = new Box();
Box b = new Box(5, 6);
System.out.println(a.x + "," + a.y + " / " + b.x + "," + b.y);

In 10,20 / 5,6.

new Box() gọi constructor no-arg → chain sang this(10, 20) → gán x=10, y=20.

new Box(5, 6) gọi constructor 2-arg trực tiếp → gán x=5, y=6.

Pattern này — "default chain sang constructor đầy đủ" — giảm code lặp và đảm bảo mọi constructor dẫn về **một** chỗ gán field, nên thêm validation sau này chỉ cần sửa 1 constructor.

Q3
Đoạn sau compile không?
public class Foo {
    int x;
    public Foo(int x) {
        System.out.println("init");
        this(0);
    }
    public Foo() { this.x = 0; }
}

Không compile. this(...) phải là statement đầu tiên trong constructor (JLS §8.8.7). Ở đây println đứng trước → error: "call to this must be first statement in constructor".

Lý do rule: Java phải đảm bảo constructor chain chạy đúng thứ tự (đặc biệt khi có kế thừa — sẽ học ở module sau, super(...) cùng rule). Cho phép code chạy trước this(...) sẽ phá đảm bảo đó.

Fix: chuyển println xuống sau, hoặc gọi trong method tách riêng sau khi object đã khởi tạo xong.

Q4
Vì sao đoạn sau không giúp caller "chọn bỏ" object?
public class Order {
    int total;
    public Order(int total) {
        if (total < 0) return;
        this.total = total;
    }
}

Order o = new Order(-5);
System.out.println(o.total);

In 0 — object vẫn được tạo, chỉ có total ở giá trị default (0 cho int). Caller cầm về một object ở trạng thái không hợp lệ, không biết có gì sai.

return; trong constructor chỉ thoát ra khỏi body — nó không huỷ object. Reference vẫn được new trả về.

Đúng cách: ném exception để caller biết input sai và không lấy được reference:

public Order(int total) {
    if (total < 0) throw new IllegalArgumentException("total must be non-negative, got " + total);
    this.total = total;
}

new Order(-5) giờ ném exception → o không nhận reference → caller phải xử lý, object vô hiệu "biến mất" về GC.

Q5
Một class Jackson dùng để deserialize JSON cần constructor thế nào? Code sau có crash không?
public class User {
    public String name;
    public User(String name) { this.name = name; }
}

// Jackson: objectMapper.readValue("{\"name\":\"A\"}", User.class)

Crash với runtime exception kiểu InvalidDefinitionException: Cannot construct instance of User — no Creators, like default constructor, exist.

Jackson default cần no-arg constructor để newInstance() rồi set field qua reflection. Class User khai báo User(String) → Java không sinh default no-arg → Jackson không có cách tạo object.

Fix:

  • Thêm public User() — constructor no-arg (giản đơn nhất).
  • Hoặc đánh dấu @JsonCreator + @JsonProperty cho constructor String để Jackson biết map.
  • Hoặc dùng record User(String name) {} — Jackson 2.12+ hỗ trợ record tự động.

Bài tiếp theo: Field vs local — lifecycle và initialization order