Module 4 khép lại với một class NumberUtils chỉ có method tĩnh — class ở đó chỉ là "cái sọt chứa hàm". Từ bài này, class thay vai trò khác: nó là bản thiết kế cho một kiểu đối tượng mới với trạng thái riêng. Bạn không chỉ gọi method — bạn tạo ra các object, mỗi object có field riêng, sống trên heap, được reference trỏ đến.
Bài này giải thích class là gì, object là gì (và hai thứ đó không giống nhau), cơ chế JVM cấp phát object trên heap, và một lần nữa nhìn == vs .equals() — nhưng lần này bạn hiểu sâu vì sao chúng khác nhau.
1. Analogy — khuôn bánh và chiếc bánh
Bạn có khuôn bánh trung thu bằng gỗ — đó là class. Khuôn mô tả hình dạng, kích thước, hoa văn. Bạn ép bột vào khuôn, cho ra 1 chiếc bánh — đó là object. Khuôn chỉ có 1; bánh có thể đúc hàng ngàn chiếc, mỗi chiếc nhân khác nhau (đậu xanh, thập cẩm), thậm chí ăn dở dang khác nhau.
| Đời thường | Java |
|---|---|
| Khuôn bánh | class Cake |
| Một chiếc bánh đã đúc | Một Cake object |
| Nhân bánh (đậu xanh vs thập cẩm) | Field value của object |
| Hành động "cắn một miếng" | Method |
💡 💡 Cách nhớ
Class = template, object = instance đúc từ template. 1 class → N objects. Mỗi object có bản sao field riêng và có thể gọi method chung.
2. Khai báo class và tạo object
// Class khai bao 1 lan trong file
public class Book {
String title; // field
String author;
int year;
}
Dùng class — tạo object bằng new:
public class App {
public static void main(String[] args) {
Book b1 = new Book();
b1.title = "Clean Code";
b1.author = "Robert C. Martin";
b1.year = 2008;
Book b2 = new Book();
b2.title = "Effective Java";
b2.author = "Joshua Bloch";
b2.year = 2018;
System.out.println(b1.title); // Clean Code
System.out.println(b2.title); // Effective Java
}
}
b1 và b2 là 2 object khác nhau, cùng kiểu Book. Sửa b1.title không đụng b2.title — mỗi object có bản sao field riêng.
2.1 new làm gì bên dưới?
Khi bạn viết new Book(), JVM chạy 3 bước:
- Cấp phát memory — tìm chỗ trên heap đủ chứa tất cả field của
Book. Field chưa gán lấy giá trị mặc định (nullcho reference,0cho int,falsecho boolean). - Chạy constructor — method đặc biệt khởi tạo object (bài sau).
- Trả về reference — địa chỉ của object trên heap được gán vào biến
b1.
flowchart LR
subgraph Stack
b1["b1 (reference)"]
b2["b2 (reference)"]
end
subgraph Heap
obj1["Book obj<br/>title = 'Clean Code'<br/>author = 'Martin'<br/>year = 2008"]
obj2["Book obj<br/>title = 'Effective Java'<br/>author = 'Bloch'<br/>year = 2018"]
end
b1 -.-> obj1
b2 -.-> obj2Biến b1, b2 nằm trên stack frame — chúng chỉ giữ địa chỉ (reference). Object nằm trên heap. Đây là cơ chế đã học ở Module 2 — giờ nhìn lại ở tầng OOP.
3. Giá trị mặc định của field
Field không được gán nhận giá trị mặc định theo kiểu:
| Kiểu | Default |
|---|---|
int, long, short, byte, char | 0 |
double, float | 0.0 |
boolean | false |
Bất kỳ reference type (String, Book, int[], ...) | null |
Khác với local variable — local không có default, phải gán trước khi đọc, không compiler báo error:
int x;
System.out.println(x); // COMPILE ERROR — variable x might not have been initialized
public class Foo {
int x; // OK — field dong tu dong 0
}
Lý do: local nằm trong stack frame được cấp phát nhanh, không có bước zero-out. Field trong object được JVM zero-out khi cấp phát — spec yêu cầu (phần của "object initialization").
4. Object sống trên heap — hệ quả
4.1 Nhiều reference cùng trỏ 1 object
Book a = new Book();
a.title = "Java";
Book b = a; // copy reference
b.title = "Python";
System.out.println(a.title); // "Python" — a va b tro cung object
Đây là đúng pass-by-value: b = a copy reference (địa chỉ), không copy object. Sửa object qua reference nào cũng thấy qua reference khác.
4.2 Object không còn ai reference → GC thu hồi
Book a = new Book();
a = null; // khong ai giu reference den object cu nua
// JVM se don object cu o lan GC tiep theo
Bạn không gọi free() hay delete như C/C++. Garbage Collector (GC) tự phát hiện object không reachable và thu hồi memory. Chi tiết ở module Memory & GC.
4.3 == so địa chỉ, .equals() so nội dung
Book a = new Book(); a.title = "Java";
Book b = new Book(); b.title = "Java";
System.out.println(a == b); // false — 2 object khac, dia chi khac
System.out.println(a.equals(b)); // false — default equals() cua Object chi so == (dia chi)
Chú ý: equals() default không so nội dung. Muốn 2 Book cùng nội dung ra true, bạn phải override equals(). Sẽ đào sâu ở bài toString / equals / hashCode.
5. Nhiều class trong 1 file
Java cho phép nhiều class trong 1 file, nhưng chỉ 1 public class, và tên file phải trùng tên public class:
// File: App.java
public class App {
public static void main(String[] args) {
Book b = new Book();
b.title = "Java";
System.out.println(b.title);
}
}
class Book { // package-private, khong public
String title;
String author;
int year;
}
Rule này do compiler áp đặt — classpath / classloader dựa vào tên file khi tìm class bytecode. Thực tế, mỗi class quan trọng nên có file riêng (convention + dễ tìm).
6. Instance method — method thuộc về object
Module 4 dùng static hết. Giờ bạn viết method không static — gọi qua object:
public class Book {
String title;
int year;
public void printInfo() { // instance method
System.out.println(title + " (" + year + ")");
}
public int yearsOld() {
return java.time.Year.now().getValue() - year;
}
}
// Su dung:
Book b = new Book();
b.title = "Java";
b.year = 2010;
b.printInfo(); // goi method qua object
System.out.println(b.yearsOld());
Trong body của instance method, bạn truy cập field trực tiếp bằng tên (title, year) — Java hiểu ngầm "field của object đang gọi method này". Tương đương this.title, this.year — bài sau nói rõ this.
6.1 Instance method ≠ static method
public class Book {
String title;
public static void hello() { System.out.println("Hello"); } // static
public void printTitle() { System.out.println(title); } // instance
}
Book.hello(); // goi static qua class — OK
Book.printTitle(); // COMPILE ERROR — printTitle can 1 object
new Book().printTitle(); // OK — goi qua object
Book b = new Book();
b.hello(); // OK nhung KHONG nen — goi static qua instance gay hieu lam
Gọi static qua instance vẫn compile được nhưng IDE cảnh báo: nên gọi qua class để code rõ.
7. Pitfall tổng hợp
❌ Nhầm 1: Tưởng class là object.
Book.title = "Java"; // COMPILE ERROR — title la instance field, khong goi qua class
✅ new Book() để tạo object trước, rồi gán qua reference: b.title = "Java".
❌ Nhầm 2: new rồi quên gán vào biến.
new Book(); // object duoc tao roi mat luon — khong ai giu reference
✅ Lưu reference vào biến nếu cần dùng sau: Book b = new Book();.
❌ Nhầm 3: So sánh object bằng ==.
if (user1 == user2) { ... } // so dia chi — gan nhu luon false
✅ Override + dùng .equals(). Bài sau.
❌ Nhầm 4: Tưởng đọc null field cho crash ngay lúc khai báo.
public class User {
String name; // default null
int len = name.length(); // NPE tai runtime
}
✅ Khởi tạo field phụ thuộc nhau trong constructor hoặc lazy.
❌ Nhầm 5: Viết nhiều public class trong 1 file.
// File: App.java
public class App { ... }
public class Book { ... } // COMPILE ERROR
✅ Chỉ 1 public class trùng tên file. Các class khác bỏ public hoặc tách file.
8. 📚 Deep Dive Oracle
ℹ️ 📚 Deep Dive Oracle (optional)
Spec / reference chính thức:
- JLS §8 — Classes — toàn bộ rule về class declaration, field, method, constructor.
- JLS §4.12.5 — Initial Values of Variables — default value của field vs local (và tại sao chúng khác nhau).
- JLS §15.9 — Class Instance Creation Expressions —
newexpression, order of evaluation. - JVMS §2.5.3 — Heap — định nghĩa heap, GC trách nhiệm.
- JVMS §6.5 — new — bytecode instruction tạo object.
Ghi chú: JLS §15.9 mô tả chi tiết chuỗi bước của new: evaluate arguments → allocate → initialize fields to default → call constructor. Hiểu chuỗi này giúp bạn debug các bug kiểu "field == null trong constructor" (field được set default trước khi constructor chạy).
9. Tóm tắt
- Class = template. Object = instance đúc từ class. 1 class → N objects, mỗi object field riêng.
new ClassName()cấp phát object trên heap, trả reference về biến trên stack.- Field không gán lấy giá trị mặc định (
0,false,null). Local variable thì bắt buộc gán trước khi đọc. - Biến kiểu reference giữ địa chỉ, không giữ object. Gán
b = acopy reference → 2 biến trỏ cùng object. ==so địa chỉ..equals()default cũng so địa chỉ — cần override để so nội dung.- GC tự thu hồi object không còn reference. Bạn không gọi
free(). - Instance method gọi qua object (
obj.method());staticmethod gọi qua class (Class.method()). - 1 file Java chỉ có 1
public classvà tên file phải trùng.
10. Tự kiểm tra
Q1Đoạn sau in gì?Book a = new Book();
a.title = "A";
Book b = a;
b.title = "B";
System.out.println(a.title);
▸
Book a = new Book();
a.title = "A";
Book b = a;
b.title = "B";
System.out.println(a.title);In B.
Book b = a copy reference — a và b cùng trỏ đến một object trên heap. Sửa b.title là sửa object chung → a.title cũng thay đổi.
Nếu muốn 2 object độc lập, phải new Book() lần nữa hoặc tự copy field.
Q2Đoạn sau compile không?public class Book {
int year;
}
public class App {
public static void main(String[] args) {
int x;
System.out.println(x);
Book b = new Book();
System.out.println(b.year);
}
}
▸
public class Book {
int year;
}
public class App {
public static void main(String[] args) {
int x;
System.out.println(x);
Book b = new Book();
System.out.println(b.year);
}
}Không compile — int x local variable bị dùng trước khi gán → compile error "variable x might not have been initialized".
Còn b.year thì OK và in 0. Field của object được JVM zero-out khi cấp phát — int field mặc định 0. Local variable không có default.
Lý do khác biệt: field khởi tạo trong quá trình new (JVM chịu trách nhiệm zero memory); local nằm trong stack frame không có bước zero hóa → Java yêu cầu gán trước khi đọc để tránh đọc rác.
Q3Đoạn sau in gì và tại sao?Book a = new Book();
Book b = new Book();
System.out.println(a == b);
System.out.println(a.equals(b));
▸
Book a = new Book();
Book b = new Book();
System.out.println(a == b);
System.out.println(a.equals(b));Cả hai đều in false.
a == bso reference (địa chỉ).avàblà 2 lầnnewriêng → 2 object trên heap khác chỗ → địa chỉ khác →false.a.equals(b)gọi phiên bảnequalsdefault củaObject— bản này chỉ thực hiệnthis == other, không so nội dung. Vẫn rafalse.
Muốn so nội dung (vd cùng title/author/year → bằng nhau), phải override equals() trong class Book. Đi kèm phải override hashCode() — contract Java. Bài sau sẽ đào sâu.
Q4Đoạn sau có gì khác thường? Mỗi khi chạy main tạo bao nhiêu object?new Book();
new Book();
Book b = new Book();
▸
new Book();
new Book();
Book b = new Book();Tạo 3 object trên heap. Hai dòng đầu không lưu reference vào biến → object vừa tạo đã không còn ai trỏ đến → eligible GC ngay lập tức.
Dòng thứ 3 gán vào b → object đó sống cho đến khi b ra khỏi scope hoặc bị gán reference khác.
Pattern "new rồi vứt" hợp lệ khi new chỉ vì side-effect (vd trong constructor có ghi log, register listener) — hiếm và thường là design kém. Phổ biến hơn: gọi method trả về mới dùng kết quả, vd new StringBuilder().append("a").append("b").toString().
Q5Vì sao file App.java không thể chứa 2 class đều public?▸
App.java không thể chứa 2 class đều public?Java compiler + classloader dùng tên file để tìm class bytecode. Rule: trong 1 file, chỉ được 1 class public, và tên file phải trùng tên public class đó. Có 2 public class → compiler không biết đặt file nào → refuse compile.
Nhiều class non-public (package-private) trong 1 file thì được:
// App.java
public class App { ... }
class Book { ... } // OK — khong public
class User { ... } // OK
Bytecode vẫn sinh ra App.class, Book.class, User.class (mỗi class 1 file .class). Convention: mỗi class quan trọng nên có file .java riêng cho dễ tìm và tôn trọng IDE/tool.
Bài tiếp theo: Constructor và this — khởi tạo object đúng cách