Constructor trong Java là gì? Mặc định và nạp chồng

Constructor trong Java là gì? Tìm hiểu constructor mặc định, có tham số, nạp chồng, từ khóa this và super kèm ví dụ chạy được.

6 phút

Constructor trong Java là một phương thức đặc biệt, tự chạy mỗi khi bạn tạo đối tượng bằng từ khóa new. Nhiệm vụ của nó là đưa đối tượng về trạng thái hợp lệ ngay từ đầu. Chúng tôi hay ví nó như bước lắp ráp: chưa lắp xong thì chưa nên đưa ra dùng.

Constructor là gì và khác phương thức thường ở đâu

Constructor là gì và khác phương thức thường ở đâu
Constructor là gì và khác phương thức thường ở đâu

Một constructor có tên trùng với tên lớp và không khai báo kiểu trả về, kể cả void. Nếu bạn vô tình thêm void, Java coi nó là phương thức thường và không gọi nó khi dùng new. Lỗi này khiến nhiều bạn mới học bối rối vì code vẫn biên dịch được.

public class SinhVien {
    String ten;
    int tuoi;

    SinhVien(String ten, int tuoi) {
        this.ten = ten;
        this.tuoi = tuoi;
    }
}

Hai dòng gán dùng this để phân biệt trường của đối tượng với tham số cùng tên. Thiếu this, bạn chỉ gán tham số cho chính nó và trường vẫn giữ giá trị mặc định.

Constructor mặc định

Constructor mặc định
Constructor mặc định

Nếu lớp không khai báo constructor nào, trình biên dịch tự thêm một constructor không tham số và thân rỗng. Các trường số được gán 0, boolean là false, còn tham chiếu là null.

public class Xe {
    String hang;
}

Xe x = new Xe();
System.out.println(x.hang);   // null

Hãy chú ý điểm sau. Ngay khi bạn tự viết một constructor có tham số, Java không còn tự thêm constructor mặc định nữa. Lúc đó lệnh new Xe() sẽ báo lỗi biên dịch trừ khi bạn tự viết thêm constructor không tham số.

Constructor có tham số và nạp chồng

Constructor có tham số và nạp chồng
Constructor có tham số và nạp chồng

Một lớp có thể có nhiều constructor miễn là danh sách tham số khác nhau về số lượng hoặc kiểu. Cách này gọi là nạp chồng (overloading). Nó cho phép người dùng lớp tạo đối tượng theo nhiều cách tiện lợi.

public class HinhChuNhat {
    double dai, rong;

    HinhChuNhat() { this(1, 1); }
    HinhChuNhat(double canh) { this(canh, canh); }
    HinhChuNhat(double dai, double rong) {
        this.dai = dai;
        this.rong = rong;
    }
}

Ở ví dụ trên, hai constructor đầu gọi lại constructor thứ ba bằng this(...). Cách này gom logic khởi tạo về một chỗ, nên khi cần thêm kiểm tra dữ liệu bạn chỉ sửa một nơi. Lệnh this(...) luôn phải là dòng đầu tiên trong constructor.

Từ khóa super và constructor trong kế thừa

Từ khóa super và constructor trong kế thừa
Từ khóa super và constructor trong kế thừa

Khi một lớp con được tạo, constructor của lớp cha chạy trước. Nếu bạn không chỉ định, Java tự chèn super() ở đầu constructor con. Nếu lớp cha chỉ có constructor có tham số, bạn bắt buộc phải gọi super(...) với đúng đối số.

class Nguoi {
    String ten;
    Nguoi(String ten) { this.ten = ten; }
}

class GiaoVien extends Nguoi {
    String monDay;
    GiaoVien(String ten, String monDay) {
        super(ten);
        this.monDay = monDay;
    }
}

Thứ tự này có lý do: phần của lớp cha phải sẵn sàng trước khi lớp con dùng đến nó. Nếu quên gọi super(ten), bạn sẽ nhận thông báo kiểu “constructor Nguoi cannot be applied”, và đây là một trong những lỗi biên dịch phổ biến nhất khi học kế thừa.

Lưu ý và lỗi hay gặp

  • Không thể khai báo constructor là static, final hay abstract.
  • Constructor không được kế thừa; lớp con phải tự khai báo constructor của mình.
  • Đừng gọi phương thức có thể bị ghi đè bên trong constructor, vì lớp con có thể chưa khởi tạo xong.

Constructor cũng là nơi thích hợp để kiểm tra dữ liệu đầu vào. Nếu tuổi âm hoặc tên rỗng, bạn có thể ném ngoại lệ ngay lập tức. Cách ném và bắt lỗi được trình bày trong bài xử lý ngoại lệ trong Java. Khi cần lưu nhiều đối tượng vừa tạo, hãy xem thêm bài collections trong Java.

Ví dụ tổng hợp: lớp tài khoản ngân hàng

Hãy xem một ví dụ gần với thực tế hơn. Lớp tài khoản cần số tài khoản bắt buộc, còn số dư ban đầu thì tùy chọn. Constructor là nơi kiểm tra dữ liệu để đối tượng không bao giờ ở trạng thái sai.

public class TaiKhoan {
    private final String so;
    private long soDu;

    public TaiKhoan(String so) {
        this(so, 0);
    }

    public TaiKhoan(String so, long soDu) {
        if (so == null || so.isBlank()) {
            throw new IllegalArgumentException("Số tài khoản trống");
        }
        if (soDu < 0) {
            throw new IllegalArgumentException("Số dư âm");
        }
        this.so = so;
        this.soDu = soDu;
    }
}

Trường so được khai báo final, nên chỉ constructor mới được gán nó một lần. Từ đó trở đi số tài khoản không đổi được. Kiểu thiết kế này giúp bạn bớt lo về việc dữ liệu bị sửa ở một chỗ nào đó trong chương trình.

Khối khởi tạo và thứ tự chạy

Ngoài constructor, Java còn cho phép viết khối khởi tạo (instance initializer) và giá trị gán trực tiếp ở khai báo trường. Thứ tự chạy là: trường tĩnh, constructor lớp cha, giá trị gán và khối khởi tạo của lớp hiện tại theo thứ tự xuất hiện, rồi mới đến thân constructor.

Bạn không cần thuộc lòng ngay, nhưng hãy biết rằng nó tồn tại để khỏi ngạc nhiên khi thấy dữ liệu đã được gán trước khi dòng đầu tiên của constructor chạy. Nếu muốn kiểm chứng, hãy thêm vài lệnh System.out.println vào từng chỗ rồi quan sát thứ tự in ra.

Mẫu Builder khi có quá nhiều tham số

Constructor với năm, sáu tham số cùng kiểu rất dễ truyền nhầm vị trí. Khi gặp tình huống này, nhiều dự án chuyển sang mẫu Builder: mỗi thuộc tính có một phương thức đặt giá trị với tên rõ ràng, cuối cùng gọi build(). Chúng tôi chỉ dùng khi lớp có từ bốn tham số trở lên, còn dưới đó constructor thường đã đủ rõ.

Một điểm cần thêm: nếu lớp của bạn được lưu trong collection, hãy xem lại vòng lặp for trong Java để duyệt các đối tượng vừa tạo cho gọn.

Bạn nên tự thử bằng cách viết một lớp có ba constructor nạp chồng và in ra thông báo ở mỗi constructor để quan sát chúng gọi nhau theo thứ tự nào.

Muốn tìm hiểu kỹ cú pháp, bạn có thể tra hướng dẫn về constructor của Oracle để đối chiếu với những gì chúng tôi viết ở đây.

Chuyên mục