Java — Từ Zero đến Senior/OOP cơ bản — class, object, encapsulation/Field, local, static — ba loại biến, ba lifecycle
3/7
~17 phútOOP cơ bản — class, object, encapsulation

Field, local, static — ba loại biến, ba lifecycle

Phân biệt instance field, static field, local variable; thứ tự khởi tạo field; static initializer block; và vì sao static field thường là nguồn rò rỉ memory trong Java server-side.

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 fieldStatic fieldLocal / parameter
Khai báoTrong class, ngoài methodTrong class, có từ khóa staticTrong body method / block
Thuộc vềObject (instance)ClassMột lần gọi method
Có default value?✅ (0, false, null)✅ (0, false, null)❌ phải gán trước khi đọc
LifetimeBằng lifetime objectBằng lifetime class (JVM tắt)Stack frame push → pop
Truy cậpobj.fieldClass.fieldtê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ự:

  1. Zero-out tất cả field về default.
  2. Chạy field initializer theo thứ tự khai báo.
  3. 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 / WeakReference nế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:

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.
  • static block chạy 1 lần khi class load, trước mọi instance.
  • static final = constant, convention UPPER_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

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);

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);

In 3 / 1,2,3.

countstatic — 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.

Q3
Vì sao 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);
    }
}

Compile OK. Output:

static init
main: 100,101

Khi class Init được load (trước khi chạy main), JVM chạy khởi tạo static theo thứ tự nguồn:

  1. a = 100 — static field initializer.
  2. 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.

Q5
Khi nào dùng 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