Người dịch: Lê Trung Kiên lớp java 08
Bài viết gốc: https://www.baeldung.com/java-constructors

1. Mở đầu

Contructors là một loại phương thức đặc biệt, quan trọng trong lập trình hướng đối tượng.
Trong bài viết này, chúng ta sẽ xem cách chúng khởi tạo trạng thái bên trong đối tượng đang được tạo.
Hãy bắt đầu lấy ví dụ, tạo một đối tượng đơn giản như là tài khoản ngân hàng của bạn.

2. Tạo tài khoản ngân hàng

Ta sẽ tạo ra class có các trường dữ liệu cơ bản đại diện cho tài khoản ngân hàng của bạn như là tên (name), ngày mở tài khoản (opened), số dư hiện tại (balance).
Thêm phương thức toString() để in thông tin vào console:

class BankAccount {
    String name;
    LocalDateTime opened;
    double balance;
    
    @Override
    public String toString() {
        return String.format("%s, %s, %f", 
          this.name, this.opened.toString(), this.balance);
    }
}

Vậy là chúng ta đã có các trường dữ liệu cần thiết để lưu thông tin về tài khoản ngân hàng. Nhưng lại không có hàm khởi tạo (constructor), vậy điều gì sẽ xảy ra?
Khi chúng ta khởi tạo một đối tượng mới, các trường dữ liệu sẽ không được khởi tạo

BankAccount account = new BankAccount();
account.toString();

Chạy chương trình, phương thức toString() ở trên sẽ ném ra exception:

java.lang.NullPointerException
    at com.baeldung.constructors.BankAccount.toString(BankAccount.java:12)
    at com.baeldung.constructors.ConstructorUnitTest
      .givenNoExplicitContructor_whenUsed_thenFails(ConstructorUnitTest.java:23)

3. Default Constructor hoặc no-arg Constructor (Hàm khởi tạo mặc định)

Giờ ta sẽ thêm hàm khởi tạo vào:

class BankAccount {
    public BankAccount() {
        this.name = "";
        this.opened = LocalDateTime.now();
        this.balance = 0.0d;
    }
}

Lưu ý một vài điều về hàm tạo mà chúng ta vừa viết. Tuy là một phương thức, nhưng nó không có kiểu trả về. Bởi vì một hàm tạo sẽ trả về kiểu đối tượng mà nó tạo ra một cách ngầm định. Khởi tạo một đối tượng mới new BankAccount() sẽ gọi đến hàm khởi tạo vừa viết.

Default Constructor là hàm khởi tạo không có tham số. Đây là lý do tại sao nó còn được gọi là no-arg Constructor.

*Nếu chưa biết về từ khóa this trong ví dụ để làm gì, hãy truy cập bài viết này để tìm hiểu thêm nhé.

4. Parameterized Constructor (Hàm khởi tạo có tham số)

Bất kỳ hàm Constructor nào có tham số được gọi là Parameterized Constructor.
Để đảm bảo tính đóng gói của của OOP, ta cần sử dụng hàm khởi tạo có tham số. Vậy nên chúng ta sẽ thêm các tham số vào hàm khởi tạo:

class BankAccount {
    public BankAccount() { ... }
    public BankAccount(String name, LocalDateTime opened, double balance) {
        this.name = name;
        this.opened = opened;
        this.balance = balance;
    }
}

Giờ ta có thể thêm dữ liệu cho đối tượng ngay khi khởi tạo đối tượng đó:

LocalDateTime opened = LocalDateTime.of(2018, Month.JUNE, 29, 06, 30, 00);
BankAccount account = new BankAccount("Tom", opened, 1000.0f); 
account.toString();

Lưu ý là chúng ta đã có hai hàm khởi tạo: hàm mặc định và hàm có tham số. Bạn có thể tạo ra nhiều hàm khởi tạo trong một đối tượng, nhưng chúng ta không nên làm vậy, sẽ rất khó quản lý các hàm khởi tạo.

5. Copy Constructor

Ta có thể sao chép các giá trị từ một đối tượng Java sang đối tượng khác bằng cách sử dụng copy constructor, “newAccount” sẽ có cùng tên với tài khoản cũ, ngày tạo hôm nay, số dư bằng không.
Trước hết tạo ra một copy constructor:

public BankAccount(BankAccount other) {
    this.name = other.name;
    this.opened = LocalDateTime.now();
    this.balance = 0.0f;
}

Sau đó truyền account vào:

LocalDateTime opened = LocalDateTime.of(2018, Month.JUNE, 29, 06, 30, 00);
BankAccount account = new BankAccount("Tim", opened, 1000.0f);
BankAccount newAccount = new BankAccount(account);

assertThat(account.getName()).isEqualTo(newAccount.getName());
assertThat(account.getOpened()).isNotEqualTo(newAccount.getOpened());
assertThat(newAccount.getBalance()).isEqualTo(0.0f);

6. Chained Constructor

Ta có thể chỉ cần truyền một tham số, các tham số còn lại có giá trị mặc định.
Ví dụ, ta sẽ viết ra hàm tạo chỉ cần truyền vào tên, các tham số còn lại giá trị mặc định như sau:

public BankAccount(String name, LocalDateTime opened, double balance) {
    this.name = name;
    this.opened = opened;
    this.balance = balance;
}
public BankAccount(String name) {
    this(name, LocalDateTime.now(), 0.0f);
}

Trong khi phương thức this() được sử dụng để gọi cùng một hàm constructor của class, thì phương thức super() được sử dụng để gọi hàm constructor của class bậc trên. Nhưng dù là this hay super thì cũng phải luôn là câu lệnh đầu tiên trong hàm tạo.

7. Kiểu giá trị (Value types)

Trong các kiểu tham số truyền vào hàm khởi tạo, bạn cũng có thể truyền vào kiểu đối tượng (Object). Tham số có giá trị là đối tượng sẽ không thay đổi giá trị sau khi khởi tạo.

class Transaction {
    final BankAccount bankAccount;
    final LocalDateTime date;
    final double amount;

    public Transaction(BankAccount account, LocalDateTime date, double amount) {
        this.bankAccount = account;
        this.date = date;
        this.amount = amount;
    }
}

Ở ví dụ trên ta dùng final cho các trường dữ liệu của class Transaction, điều này có nghĩa mỗi trường dữ liệu đó chỉ có thể khởi tạo bên trong phương thức khởi tạo của class, không thể định nghĩa lại bên trong bất kì phương thức nào khác.
Nếu chúng ta viết nhiều hàm khởi tạo cho class Transaction, thì mỗi hàm khởi tạo đó sẽ cần tạo các biến final. Không làm như vậy sẽ dẫn đến lỗi biên dịch.

8. Kết luận

Hi vọng qua bài viết này bạn đã biết thêm về các hàm khởi tạo thường thấy khi tạo một đối tượng.