Module 2 đã phân biệt primitive vs reference. Module 4 giới thiệu local và parameter. Bài này hợp nhất bức tranh: có 3 loại biến trong Java theo nơi khai báo và tầng lifecycle khác nhau — instance field, static field, local. Mỗi loại có rule khởi tạo, scope, và lifetime riêng. Nhầm 3 loại này là nguồn của rất nhiều bug — đặc biệt memory leak trong app chạy lâu.
1. Ba loại biến — bảng tổng hợp
| Instance field | Static field | Local / parameter | |
|---|---|---|---|
| Khai báo | Trong class, ngoài method | Trong class, có từ khóa static | Trong body method / block |
| Thuộc về | Object (instance) | Class | Một lần gọi method |
| Có default value? | ✅ (0, false, null) | ✅ (0, false, null) | ❌ phải gán trước khi đọc |
| Lifetime | Bằng lifetime object | Bằng lifetime class (JVM tắt) | Stack frame push → pop |
| Truy cập | obj.field | Class.field | tên trực tiếp |
Có thể dùng this? | ✅ | ❌ (không thuộc object) | ❌ |
2. Instance field — mỗi object một bản
public class Counter {
int value; // instance field
}
Counter a = new Counter();
Counter b = new Counter();
a.value = 10;
b.value = 20;
System.out.println(a.value + " / " + b.value); // 10 / 20
Mỗi Counter object có ô nhớ riêng cho value. Thay đổi a.value không đụng b.value — chúng là 2 ô trên 2 object khác nhau trên heap.
Khởi tạo: có thể (1) gán default giá trị tại khai báo, (2) gán trong constructor, (3) cả hai — constructor ghi đè default nếu có.
public class Counter {
int value = 100; // default init
String name; // khong init -> null
public Counter(String name) {
this.name = name; // constructor set
}
}
Counter c = new Counter("c1");
System.out.println(c.value + " / " + c.name); // 100 / c1
2.1 Thứ tự khởi tạo field
Khi new chạy, JVM khởi tạo field theo thứ tự:
- Zero-out tất cả field về default.
- Chạy field initializer theo thứ tự khai báo.
- Chạy body constructor.
public class Order {
int total = 100; // 1
int discount = total / 10; // 2 — total da gan, discount = 10
int finalPrice; // 3
public Order() {
finalPrice = total - discount; // 4
}
}
Thứ tự này giải thích vì sao field khai báo sau không dùng được trong initializer field trước đó:
public class Bad {
int a = b; // COMPILE ERROR — b chua khai bao tai diem nay
int b = 10;
}
3. Static field — mỗi class một bản, không thuộc object
public class Counter {
static int totalCreated = 0; // static field — mot ban duy nhat cho class
int id; // instance field
public Counter() {
totalCreated++;
this.id = totalCreated;
}
}
Counter a = new Counter(); // totalCreated: 0 -> 1, a.id = 1
Counter b = new Counter(); // totalCreated: 1 -> 2, b.id = 2
System.out.println(Counter.totalCreated); // 2
System.out.println(a.id + " / " + b.id); // 1 / 2
totalCreated không nằm trong object — nó nằm trong class metadata, được JVM lưu một bản trong vùng nhớ đặc biệt (Metaspace/class area). Mọi object chung Counter nhìn thấy cùng giá trị.
Truy xuất static field nên qua tên class (Counter.totalCreated), không qua instance (a.totalCreated) — compile được nhưng misleading.
3.1 Khi nào dùng static?
✅ Nên khi field thực sự chia sẻ giữa mọi instance:
- Counter tạo object (như ví dụ trên).
- Configuration (
public static final int MAX_SIZE = 100). - Cache dùng chung (cẩn thận lifetime — xem phần rò rỉ).
- Singleton holder.
❌ Không nên khi field thực chất thuộc về từng object riêng:
class User {
static String name; // TE HAI — moi user la MOT name giong nhau, luc luot ghi de!
}
3.2 Static initializer block — khởi tạo phức tạp
Field initialization đơn giản dùng = value. Nếu cần logic (đọc file, lookup), dùng static block:
public class Config {
static Map<String, String> settings;
static {
settings = new HashMap<>();
settings.put("env", System.getenv("ENV"));
settings.put("version", loadVersion());
}
private static String loadVersion() { ... }
}
Static block chạy một lần khi class được JVM load — trước bất kỳ instance nào được tạo, trước bất kỳ static method nào được gọi.
4. Local variable — sống theo call
public void doWork() {
int x = 10; // local — sinh ra khi method bat dau
String s = "hello"; // cung local
if (x > 5) {
int y = 20; // local — scope trong block if
}
// y khong con ton tai o day
}
// x, s mat khi method return
Local variable nằm trong stack frame của method. Method return → frame pop → mọi local mất.
Khác với field, local không có giá trị mặc định:
public void bad() {
int x;
System.out.println(x); // COMPILE ERROR — variable x might not have been initialized
}
5. static final — constant
public class Config {
public static final int MAX_RETRY = 3;
public static final String API_URL = "https://api.example.com";
}
static = thuộc class; final = không gán lại. Kết hợp = constant. Convention đặt tên: UPPER_SNAKE_CASE.
Compiler có thể inline giá trị constant vào call site:
if (count > Config.MAX_RETRY) ... // compiler co the thay MAX_RETRY bang 3 luc compile
Hệ quả tinh tế: nếu bạn đổi MAX_RETRY = 5 rồi recompile Config.java nhưng không recompile file dùng nó, file cũ vẫn giữ giá trị 3. Fix: luôn rebuild full. Hiếm gặp trong dự án dùng Gradle/Maven đúng.
6. Rò rỉ memory — static field là thủ phạm số 1
Object trên heap bị GC khi không còn ai reference. Static field sống suốt đời class → giữ reference → giữ object → GC không dọn.
public class UserTracker {
static List<User> allUsers = new ArrayList<>(); // giu moi user mai mai
public void register(User u) {
allUsers.add(u);
}
}
Server chạy cả tháng, allUsers chỉ lớn lên — OutOfMemoryError lúc nào không biết. Đây là class of bug gây nhiều incident nổi tiếng.
Fix:
WeakHashMap/WeakReferencenếu chỉ cần reference phụ.- TTL cache (Caffeine, Guava Cache) — tự evict sau thời gian.
- Cleanup thủ công trong hook shutdown / logout.
Chi tiết ở module Memory & GC.
⚠️ ⚠️ ThreadLocal + thread pool = rò rỉ classic
ThreadLocal giữ giá trị per-thread. Khi dùng thread pool, thread tái dùng → ThreadLocal giữ reference giữa các request. Spring, web server có vô số bug kiểu này. Rule: try { ... } finally { threadLocal.remove(); }.
7. Pitfall tổng hợp
❌ Nhầm 1: Nhầm field static với instance.
class User { static String name; } // moi user chung mot ten !!
✅ Field cá nhân hóa → không static.
❌ Nhầm 2: Đọc field initializer tham chiếu field khai báo sau.
class Bad { int a = b; int b = 10; } // COMPILE ERROR
✅ Đảo thứ tự hoặc gán trong constructor.
❌ Nhầm 3: Sửa static field từ nhiều thread không đồng bộ.
static int counter = 0;
// thread A: counter++; thread B: counter++; -> mat update
✅ AtomicInteger hoặc synchronized. Module Concurrency.
❌ Nhầm 4: public static mutable collection — ai cũng sửa được.
public static List<String> items = new ArrayList<>(); // mutable shared state
✅ Collections.unmodifiableList(...) hoặc List.copyOf(...).
❌ Nhầm 5: Local shadow field nhưng quên this..
class Foo {
int x;
public Foo(int x) { x = x; } // shadow — field x van 0
}
✅ this.x = x;.
8. 📚 Deep Dive Oracle
ℹ️ 📚 Deep Dive Oracle (optional)
Spec / reference chính thức:
- JLS §8.3 — Field Declarations — distinction instance vs static field.
- JLS §8.3.3 — Forward References During Field Initialization — rule cấm tham chiếu field chưa khai báo.
- JLS §8.7 — Static Initializers — cú pháp và thời điểm chạy static block.
- JLS §12.4 — Initialization of Classes and Interfaces — thứ tự chính xác khi class được load.
- JVMS §2.5.4 — Method Area — static field lưu ở đâu trong JVM memory.
Ghi chú: JLS §12.4 là phần đáng đọc nhất — mô tả chính xác class initialization có thể bị trigger bởi: tạo instance, gọi static method, truy cập static field (trừ compile-time constant), và reflection. Biết điều này giúp hiểu vì sao đôi khi static block chạy "sớm hơn dự kiến".
9. Tóm tắt
- Instance field thuộc object — mỗi object 1 bản; default value tự động.
- Static field thuộc class — 1 bản duy nhất cho toàn JVM; default value tự động; lifetime = class loaded → JVM tắt.
- Local variable thuộc method call — không có default; phải gán trước khi đọc; stack frame pop là biến mất.
- Thứ tự khởi tạo object: default zero-out → field initializer (theo thứ tự khai báo) → constructor body.
staticblock chạy 1 lần khi class load, trước mọi instance.static final= constant, conventionUPPER_SNAKE_CASE, compiler có thể inline.- Static field giữ reference → GC không dọn → nguồn memory leak phổ biến trong server-side.
- Instance field cho state cá nhân hoá; static cho shared state (dùng cẩn thận).
10. Tự kiểm tra
Q1Đoạn sau in gì?public class Order {
int x = 10;
int y = x * 2;
int z;
public Order() {
z = y + 1;
}
}
Order o = new Order();
System.out.println(o.x + " / " + o.y + " / " + o.z);
▸
public class Order {
int x = 10;
int y = x * 2;
int z;
public Order() {
z = y + 1;
}
}
Order o = new Order();
System.out.println(o.x + " / " + o.y + " / " + o.z);In 10 / 20 / 21.
Thứ tự: (1) zero-out: x=0, y=0, z=0. (2) field initializer theo thứ tự khai báo: x = 10, rồi y = x * 2 = 20, z không có initializer giữ 0. (3) constructor body: z = y + 1 = 21.
Chú ý y = x * 2 đọc x — hợp lệ vì x khai báo trước, đã gán. Nếu đảo (y = x * 2 trước x = 10), y = 0 * 2 = 0 — bug tinh vi.
Q2Đoạn sau in gì?public class Counter {
static int count = 0;
int id;
public Counter() { count++; this.id = count; }
}
Counter a = new Counter();
Counter b = new Counter();
Counter c = new Counter();
System.out.println(Counter.count + " / " + a.id + "," + b.id + "," + c.id);
▸
public class Counter {
static int count = 0;
int id;
public Counter() { count++; this.id = count; }
}
Counter a = new Counter();
Counter b = new Counter();
Counter c = new Counter();
System.out.println(Counter.count + " / " + a.id + "," + b.id + "," + c.id);In 3 / 1,2,3.
count là static — 1 bản duy nhất cho toàn class. Mỗi lần new, count++ tăng bản chung, rồi this.id = count chụp giá trị tại thời điểm đó vào instance field.
Sau 3 lần new: count = 3; 3 object có id lần lượt 1, 2, 3. Đây là pattern "auto-increment ID" — đơn giản nhưng không thread-safe: 2 thread gọi constructor cùng lúc có thể đọc cùng count → 2 object trùng id. Fix: AtomicInteger.
Q3Vì sao static List<User> all có thể gây OutOfMemoryError?▸
static List<User> all có thể gây OutOfMemoryError?Static field tồn tại suốt lifetime class — từ khi class load đến khi JVM tắt. static List all giữ reference đến list; list giữ reference đến mọi User từng add vào.
Garbage Collector chỉ thu hồi object không còn ai reference. Vì all vẫn giữ → GC không đụng đến dù logic business đã "xong" với user đó.
Server chạy cả tháng, all chỉ lớn → heap đầy → OOM.
Fix: dùng WeakHashMap, cache với TTL (Caffeine), hoặc explicit cleanup khi user logout. Module Memory & GC sẽ đào sâu.
Q4Đoạn sau compile không? Nếu có, in gì?public class Init {
static int a = 100;
static int b;
static { b = a + 1; System.out.println("static init"); }
public static void main(String[] args) {
System.out.println("main: " + a + "," + b);
}
}
▸
public class Init {
static int a = 100;
static int b;
static { b = a + 1; System.out.println("static init"); }
public static void main(String[] args) {
System.out.println("main: " + a + "," + b);
}
}Compile OK. Output:
static init
main: 100,101Khi class Init được load (trước khi chạy main), JVM chạy khởi tạo static theo thứ tự nguồn:
a = 100— static field initializer.- Static block:
b = a + 1 = 101, in"static init".
Sau đó main chạy, in "main: 100,101". Static block luôn chạy trước bất kỳ code nào dùng class, và chạy chỉ một lần.
Q5Khi nào dùng static field, khi nào dùng instance field?▸
static field, khi nào dùng instance field?Instance field (không static) — khi dữ liệu cá nhân hóa cho từng object: id, name, balance, createdAt của User. Mỗi user có giá trị riêng.
Static field — khi dữ liệu chia sẻ giữa mọi object cùng class:
- Hằng số:
public static final int MAX_SIZE = 100; - Counter global:
static int totalOrders = 0;(cẩn thận thread-safety). - Cache/config dùng chung (cẩn thận memory leak).
- Singleton / factory holder.
Rule tay: nếu câu "mỗi object có một giá trị riêng" đúng → instance. Nếu câu "giá trị này không thuộc object nào, nó thuộc class" đúng → static. Nhầm lẫn phổ biến: đặt static cho field thuộc object → mọi user bị ghi đè chung một giá trị.
Bài tiếp theo: Encapsulation — private, public, protected và vì sao mọi field nên private