1. Thứ tự khởi tạo đối tượng trong Java: ai trước, ai sau?
Tạo một đối tượng trong Java không chỉ đơn giản là “cấp phát bộ nhớ và ghi giá trị vào đó”. Đây là cả một nghi thức với thứ tự hành động rõ ràng. Nếu hình dung quá trình khởi tạo như việc phóng tên lửa, thì mỗi tầng đều có vị trí nghiêm ngặt của nó.
Khi bạn viết:
Person vasya = new Person("Vasya", 30);
Java thực hiện các bước sau:
- Cấp phát bộ nhớ cho đối tượng và gán giá trị mặc định cho tất cả các trường.
- Thực hiện khởi tạo tường minh cho các trường (nếu bạn đặt giá trị ngay khi khai báo).
- Thực thi tất cả các khối khởi tạo không tĩnh (thường) — theo đúng thứ tự chúng được viết trong lớp.
- Thực thi phần thân constructor mà bạn gọi thông qua new.
Hãy xem chi tiết từng bước.
2. Khởi tạo các trường
Giá trị mặc định
Khi đối tượng vừa mới được tạo (trước khi bạn gán gì một cách tường minh), tất cả các trường của nó nhận các giá trị mặc định:
| Kiểu trường | Giá trị mặc định |
|---|---|
|
|
|
|
|
|
|
'\u0000' (ký tự null) |
| Kiểu tham chiếu (String, các đối tượng khác) | |
Khởi tạo tường minh
Nếu bạn gán giá trị cho trường ngay tại thời điểm khai báo, giá trị đó sẽ được thiết lập sau các giá trị mặc định, nhưng trước khi thực thi constructor.
public class Person {
private String name = "Không tên";
private int age = 18;
}
Nếu bạn tạo đối tượng bằng constructor rỗng, các giá trị này sẽ được giữ nguyên. Nếu bằng constructor có tham số — nhiều khả năng chúng sẽ bị ghi đè.
3. Các khối khởi tạo: để làm gì và hoạt động thế nào
Trong Java, bạn có thể khai báo các khối khởi tạo không tĩnh (thường). Chúng được thực thi mỗi lần khi tạo đối tượng, ngay sau khi khởi tạo các trường, nhưng trước constructor.
Cú pháp:
public class Person {
{
System.out.println("Đang chạy khối khởi tạo không tĩnh!");
}
}
Nếu trong lớp có vài khối như vậy, chúng sẽ được thực thi theo thứ tự được viết.
Ví dụ với nhiều khối
public class Person {
private String name = "Không tên";
{
System.out.println("Khối 1: name = " + name);
name = "Bí ẩn";
}
{
System.out.println("Khối 2: name = " + name);
}
public Person() {
System.out.println("Constructor: name = " + name);
}
}
Nếu tạo đối tượng:
Person p = new Person();
Kết quả sẽ là:
Khối 1: name = Không tên
Khối 2: name = Bí ẩn
Constructor: name = Bí ẩn
Khối khởi tạo dùng để làm gì?
Trong thực tế, các khối khởi tạo ít khi được dùng. Thông thường mọi thứ có thể làm trong khối đều có thể làm ngay khi khai báo trường hoặc trong constructor. Nhưng đôi khi (ví dụ bạn có nhiều constructor và một phần logic khởi tạo cần dùng chung cho tất cả) thì khối khởi tạo có thể tiện lợi.
4. Constructor: bước cuối cùng của quá trình khởi tạo
Sau khi tất cả các trường đã nhận giá trị (mặc định hoặc tường minh) và mọi khối khởi tạo đã chạy, constructor sẽ được gọi — chính là constructor bạn chỉ định sau new.
Trong constructor, bạn thường gán giá trị cuối cùng cho các trường, nhận tham số và thực hiện các khởi tạo khác phụ thuộc vào dữ liệu đầu vào.
public class Person {
private String name;
private int age;
public Person(String name, int age) {
System.out.println("Constructor: name = " + name + ", age = " + age);
this.name = name;
this.age = age;
}
}
5. Minh họa thứ tự khởi tạo đầy đủ
Hãy viết một lớp minh họa rõ ràng thứ tự khởi tạo diễn ra như thế nào.
public class Person {
private String name = "Không tên";
private int age = 18;
{
System.out.println("Khối khởi tạo: name = " + name + ", age = " + age);
age = 21;
}
public Person() {
System.out.println("Constructor không tham số: name = " + name + ", age = " + age);
}
public Person(String name, int age) {
System.out.println("Constructor có tham số: name = " + name + ", age = " + age);
this.name = name;
this.age = age;
}
public void printInfo() {
System.out.println("Person: name = " + name + ", age = " + age);
}
}
Và bây giờ trong lớp chính của chúng ta (ví dụ, Main):
public class Main {
public static void main(String[] args) {
System.out.println("Tạo đối tượng p1:");
Person p1 = new Person();
p1.printInfo();
System.out.println("\nTạo đối tượng p2:");
Person p2 = new Person("Petya", 30);
p2.printInfo();
}
}
Đầu ra sẽ tương tự như sau:
Tạo đối tượng p1:
Khối khởi tạo: name = Không tên, age = 18
Constructor không tham số: name = Không tên, age = 21
Person: name = Không tên, age = 21
Tạo đối tượng p2:
Khối khởi tạo: name = Không tên, age = 18
Constructor có tham số: name = Không tên, age = 21
Person: name = Petya, age = 30
Lưu ý: trong constructor có tham số, các giá trị trường vẫn còn là giá trị cũ (những gì sau khối khởi tạo), và chỉ sau constructor các trường mới nhận giá trị cuối cùng.
6. Sơ đồ thứ tự khởi tạo
Đây là một sơ đồ nhỏ (lưu đồ) để trực quan hóa:
flowchart TD
A[Cấp phát bộ nhớ, giá trị mặc định] --> B[Khởi tạo tường minh cho các trường]
B --> C[Thực thi các khối khởi tạo không tĩnh]
C --> D[Thực thi constructor]
D --> E[Đối tượng sẵn sàng để sử dụng]
7. Đặc điểm và lưu ý
Trường tĩnh và khối tĩnh
Trong bài này, chúng ta chỉ nói về các trường và khối không tĩnh (thường). Trường tĩnh và khối tĩnh được khởi tạo một lần khi lớp được nạp, chứ không phải mỗi lần tạo đối tượng. Sẽ nói kỹ hơn ở các chủ đề về đóng gói.
Khi nào cái gì được khởi tạo?
- Các trường — mỗi lần tạo đối tượng.
- Các khối khởi tạo — mỗi lần tạo đối tượng.
- Trường tĩnh và khối tĩnh — chỉ một lần khi nạp lớp.
Có thể truy cập các trường trong các khối khởi tạo không?
Có thể! Nhưng cần nhớ rằng nếu trường được khai báo phía dưới khối, nó đã tồn tại, và nếu bạn truy cập nó trước khi khởi tạo tường minh, bạn sẽ nhận giá trị mặc định.
8. Những lỗi thường gặp khi khởi tạo đối tượng
Lỗi số 1: Kỳ vọng rằng các trường được khởi tạo trước khi khai báo.
Trong Java, thứ tự khai báo các trường và các khối khởi tạo trong lớp có ý nghĩa. Nếu trong khối khởi tạo bạn truy cập một trường được khai báo bên dưới, trường đó đã tồn tại nhưng chưa được khởi tạo tường minh — nó sẽ có giá trị mặc định.
Lỗi số 2: Lặp lại khởi tạo ở khối và trong constructor.
Người mới thường lặp lại cùng một logic cả trong khối và trong constructor. Tốt hơn là dùng hoặc khối, hoặc constructor, hoặc gọi một constructor từ constructor khác thông qua this(...).
Lỗi số 3: Nghĩ rằng các trường tĩnh được khởi tạo mỗi lần tạo đối tượng.
Trường tĩnh và khối tĩnh hoạt động khác — chúng chỉ được khởi tạo một lần khi nạp lớp.
Lỗi số 4: Sử dụng các trường tham chiếu chưa khởi tạo.
Nếu bạn quên khởi tạo tường minh một trường tham chiếu (ví dụ, String name;), thì nó sẽ bằng null cho đến khi bạn gán giá trị cho nó một cách tường minh trong khối khởi tạo hoặc trong constructor.
GO TO FULL VERSION