Java — Từ Zero đến Senior/Phương thức (method)/Scope, shadowing và lifetime — biến sống ở đâu, đến khi nào
5/6
~16 phútPhương thức (method)

Scope, shadowing và lifetime — biến sống ở đâu, đến khi nào

Block scope trong Java, shadowing biến cùng tên, lifetime của local vs field, effectively final, và vì sao closure (lambda) bắt biến theo rule đặc biệt.

Bạn khai báo int x = 10 ở đầu method, rồi ở giữa có block { int x = 20; }. Compile được? Đúng hay sai? Nó in gì? Còn nếu lambda bên trong đọc x, phiên bản nào được lấy?

Những câu hỏi trên đều liên quan đến scope — phạm vi mà một cái tên biến có ý nghĩa, và lifetime — khoảng thời gian biến tồn tại trên memory. Hiểu hai khái niệm này giúp bạn tránh bug tinh vi: biến bị shadow, closure bắt sai giá trị, NPE do dùng biến ngoài scope.

1. Analogy — phòng trong nhà

Căn nhà có nhiều phòng. Bạn dán nhãn "remote TV" trên kệ phòng khách — chỉ phòng khách hiểu nhãn này, phòng bếp không biết. Nếu phòng ngủ cũng dán nhãn "remote TV" (trên remote điều hòa), ở phòng ngủ bạn gọi "remote TV" = điều hòa, ở phòng khách = TV. Cùng cái tên, phòng khác, nghĩa khác.

Đời thườngJava
PhòngBlock { ... }
Nhãn trên kệTên biến
Cùng tên nhãn ở phòng khácShadowing
Đồ còn/hết khi đóng phòngLifetime biến

💡 💡 Cách nhớ

Mỗi cặp { } mở một scope mới. Biến khai báo trong scope chỉ thấy được trong scope đó và các scope con bên trong. Ra khỏi scope, tên biến không còn ý nghĩa.

2. Block scope — phạm vi đi theo { }

Scope trong Java bám theo block (cặp { }):

public static void demo() {
    int a = 1;          // scope: toan method

    if (true) {
        int b = 2;      // scope: trong block if
        System.out.println(a + b);   // OK — thay ca a (outer) va b (inner)
    }

    // System.out.println(b);  // COMPILE ERROR — b khong con thay
    System.out.println(a);       // OK
}

Scope lồng nhau theo cây — biến ngoài thấy trong; biến trong không lan ra ngoài.

2.1 Scope của parameter và for-counter

Parameter có scope = toàn body method:

public static int doubled(int x) {
    return x * 2;   // x thay trong het body
}

Biến khai báo trong for (int i = 0; ...) có scope = chỉ trong for:

for (int i = 0; i < 10; i++) { ... }
// System.out.println(i);   // COMPILE ERROR

Biến khai báo với for-each (for (int x : arr)), try-with-resources, catch (Exception e) — cùng nguyên tắc, scope hẹp trong block tương ứng.

3. Shadowing — biến trong che biến ngoài

Java cấm khai báo lại biến local cùng tên trong cùng scope:

int x = 1;
int x = 2;   // COMPILE ERROR

Nhưng cho phép biến trong block con che (shadow) biến ngoài:

int x = 1;
if (condition) {
    int x = 2;    // hop le — shadow bien x o ngoai
    System.out.println(x);   // 2
}
System.out.println(x);   // 1 — bien ngoai khong bi thay doi

Kể cả parameter có thể bị shadow bởi biến local trong block con:

public static void f(int x) {
    for (int x = 0; x < 10; x++) { ... }   // COMPILE ERROR — dung scope voi parameter
    {
        int x = 99;   // COMPILE ERROR — scope giua cua method
    }
}

Java cấm shadowing trong cùng method body (giữa parameter và local top-level). Chỉ được shadow khi tạo block con rõ ràng — rất hẹp và hầu như không ai làm cố ý.

3.1 Shadow field bằng local — chuyện thường hơn

Khi học OOP, bạn sẽ gặp pattern này liên tục:

class Person {
    private String name;

    public void setName(String name) {   // parameter 'name' shadow field 'name'
        this.name = name;                 // dung this de tro field
    }
}

name trong body của setName ám chỉ parameter, không phải field. Muốn truy cập field bị shadow → dùng this.. Đây là pattern tiêu chuẩn, không phải anti-pattern.

⚠️ ⚠️ Shadow bug khó thấy

Bug phổ biến: vô tình khai báo local cùng tên field → code ghi vào local, field không đổi.

class Counter {
    private int count = 0;
    public void inc() {
        int count = this.count;   // shadow
        count++;                   // chi sua local
        // quen gan lai this.count = count
    }
}

IDE sáng sủa sẽ warning "local variable hides a field". Bật cảnh báo đó lên.

4. Lifetime — biến sống bao lâu

Scope quyết định ở đâu dùng được tên biến. Lifetime quyết định bao lâu ô nhớ tồn tại.

Loại biếnScopeLifetime
Local / parameterBlock chứa nóTừ lúc khai báo đến khi thoát block (stack frame pop)
Field (instance)Mọi instance method của classBằng lifetime của object (đến khi GC thu hồi)
Field staticTruy xuất qua ClassName.field từ mọi nơiTừ khi class được load đến khi JVM tắt

Ví dụ:

class Order {
    static int totalOrdersCreated = 0;   // static field
    private int id;                       // instance field

    public Order(int id) {
        this.id = id;
        totalOrdersCreated++;
    }

    public void print() {
        String prefix = "Order-";        // local — song trong print()
        System.out.println(prefix + id);
    }
}
  • totalOrdersCreated sống từ khi class Order được JVM load đến khi JVM tắt.
  • id sống cùng mỗi object Order — object bị GC thì id biến mất.
  • prefix sống trong một lần gọi print() — method return → frame pop → prefix biến mất.

5. Effectively final — điều kiện để biến dùng trong lambda / inner class

Lambda và anonymous inner class được phép đọc biến local của method bao ngoài, nhưng biến đó phải effectively final — khai báo rồi không bị gán lại trước/sau lambda.

public static void demo() {
    int x = 10;
    Runnable r = () -> System.out.println(x);   // OK — x effectively final

    int y = 20;
    y = 21;
    Runnable r2 = () -> System.out.println(y);   // COMPILE ERROR — y khong effectively final
}

Lý do: lambda có thể chạy sau khi method bao đã return (vd submit vào thread pool). JVM bắt giá trị của biến tại lúc tạo lambda — nếu biến tiếp tục bị thay đổi, giá trị bắt sẽ lệch với giá trị hiện tại. Java chọn giải pháp an toàn: cấm.

5.1 Workaround khi cần "biến thay đổi" trong lambda

// Cach 1: array 1 phan tu (hack co dien)
int[] counter = {0};
list.forEach(x -> counter[0]++);

// Cach 2: AtomicInteger (dung cho multi-thread)
AtomicInteger c = new AtomicInteger(0);
list.forEach(x -> c.incrementAndGet());

// Cach 3: reduce thay vi side-effect (functional style)
long count = list.stream().filter(x -> x > 0).count();

Trong đa số trường hợp, cách 3 — chuyển side-effect thành reduce/stream — là sạch nhất và đúng tinh thần functional.

ℹ️ 📚 Vì sao array hack hoạt động?

Biến counter là reference tới array — reference đó không đổi (effectively final). Việc sửa counter[0] là sửa object qua reference, không phải gán lại biến. Lambda chỉ yêu cầu reference effectively final, không quan tâm nội dung object.

6. Scope của try-with-resourcescatch

try (BufferedReader r = new BufferedReader(new FileReader("a.txt"))) {
    // r thay trong day
} catch (IOException e) {
    // e thay trong day
    System.out.println(e.getMessage());
}
// r, e ra ngoai scope — khong dung duoc

Đây là lý do bài "Variables" Module 2 nhấn mạnh try-with-resources — nó tự đóng r làm r ra khỏi scope luôn, tránh reuse biến đã đóng.

7. Pitfall tổng hợp

Nhầm 1: Cố dùng biến của for sau khi vòng kết thúc.

for (int i = 0; i < 10; i++) { ... }
System.out.println(i);   // COMPILE ERROR

✅ Khai báo i trước for nếu cần dùng sau — nhưng thường thiết kế lại sạch hơn.

Nhầm 2: Shadow field bằng local mà quên this..

void setAge(int age) { age = age; }   // tu gan local cho chinh no

this.age = age;. Hoặc đổi tên parameter (newAge) để rõ ý.

Nhầm 3: Cố gán lại biến dùng trong lambda.

int x = 0;
list.forEach(item -> x++);   // COMPILE ERROR

✅ Dùng reduce/count, hoặc AtomicInteger nếu thực sự cần mutable.

Nhầm 4: Khai báo biến trong vòng loop nhưng tưởng giữ giá trị giữa các vòng.

for (int i = 0; i < 5; i++) {
    int sum = 0;    // reset moi vong — co the khong phai y do
    sum += i;
}

✅ Nếu muốn tích luỹ, khai báo sum ngoài loop.

Nhầm 5: Static field bị "rò rỉ" memory vì tham chiếu object không release.

class Registry {
    static List<User> all = new ArrayList<>();   // giu moi User song mai
}

✅ Static collection cần chiến lược xoá (weak reference, TTL, cleanup). Sẽ đào ở module Memory & GC.

8. 📚 Deep Dive Oracle

ℹ️ 📚 Deep Dive Oracle (optional)

Spec / reference chính thức:

Ghi chú: JLS §6.4 phân biệt "shadow" (tên hẹp hơn che tên rộng) và "obscure" (một identifier bị hiểu nhầm giữa type/package/variable). Shadow gặp thường xuyên; obscure chỉ gặp khi đặt tên biến trùng tên class — tránh luôn. §4.12.4 quan trọng: "effectively final" = "không bị gán lại sau khi khai báo", compiler sinh flag để lambda capture được.

9. Tóm tắt

  • Scope của biến local = block ({ }) chứa nó. Ra khỏi block → tên biến không còn ý nghĩa.
  • Shadowing = biến trong block con cùng tên với biến ngoài → trong block con, tên chỉ biến mới. Biến ngoài vẫn tồn tại, dùng this. hoặc re-declare để truy cập.
  • Java cấm re-declare cùng tên trong cùng scope (chặn nhầm).
  • Lifetime: local ~ stack frame; instance field ~ object; static field ~ class loaded.
  • Lambda / inner class capture biến local → biến phải effectively final (không gán lại).
  • Muốn mutable trong lambda: reduce/stream (sạch), hoặc AtomicInteger / array 1 phần tử (hack).
  • try-with-resources giới hạn scope của resource trong try block — không reuse được ngoài.

10. Tự kiểm tra

Tự kiểm tra
Q1
Đoạn sau in gì?
int x = 1;
if (true) {
    int x = 2;
    System.out.println(x);
}
System.out.println(x);

In 2 rồi 1.

Block if mở scope mới, khai báo int x = 2 — biến này shadow biến x ở scope ngoài trong phạm vi block. Print trong block dùng biến trong → 2. Ra khỏi block, biến trong hết scope — print ngoài lại dùng biến ngoài 1.

Biến ngoài không bị thay đổi bởi block trong — đó chỉ là tên tạm trùng.

Q2
Đoạn sau compile không?
public static void f(int x) {
    int x = 10;
    System.out.println(x);
}

Không. Parameter x và local x nằm trong cùng scope (method body level) → vi phạm rule "không re-declare trong cùng scope" → compile error.

Nếu khai báo trong block con thì OK:

public static void f(int x) {
    {
        int x = 10;   // shadow trong block con
    }
}

Nhưng pattern này hầu như không ai dùng cố ý — nó gợi ra refactor: đổi tên parameter hoặc rút method.

Q3
Đoạn sau có bug gì?
class Person {
    private String name;
    public void setName(String name) {
        name = name;
    }
}

name = name gán parameter name cho chính nó — không đụng đến field name của object. Method chạy xong, field vẫn giá trị cũ.

Fix:

this.name = name;

this. ép truy cập field bị shadow bởi parameter. Đây là pattern chuẩn trong Java — đổi tên parameter thành newName cũng được, nhưng convention giữ cùng tên cho rõ ý "parameter đúng là giá trị mới của field".

Q4
Đoạn sau compile không? Vì sao?
int counter = 0;
Runnable r = () -> counter++;

Không compile. Lambda yêu cầu biến local được capture phải effectively final (JLS §4.12.4) — không bị gán lại sau khi khai báo. counter++counter = counter + 1 → gán lại → vi phạm.

Lý do: lambda có thể chạy sau khi method bao return (vd submit vào thread pool). Java không cho phép "biến local" tồn tại sau khi stack frame pop — giải pháp là capture value tại lúc tạo lambda, và cấm mutation để không lệch giữa "giá trị đã capture" và "giá trị hiện tại".

Fix: dùng AtomicInteger (reference effectively final, nội dung mutable), hoặc thiết kế lại bằng stream().count() / reduce.

Q5
Phân biệt scopelifetime của một biến, lấy ví dụ minh họa.

Scope = ở đâu tên biến có ý nghĩa (phạm vi source code mà compiler cho phép tham chiếu).

Lifetime = bao lâu ô nhớ của biến tồn tại ở runtime.

Ví dụ:

class Foo {
    static int counter = 0;   // scope: moi noi co the truy qua Foo.counter
                              // lifetime: tu khi class load den khi JVM tat
    private int id;           // scope: moi instance method cua Foo
                              // lifetime: bang lifetime cua object instance

    void hello() {
        int n = 10;           // scope: trong body hello()
                              // lifetime: tu khi frame push den khi hello() return
    }
}

Hai khái niệm độc lập: biến có thể out-of-scope nhưng vẫn alive (vd biến local đã thoát block nhưng stack frame chưa pop — không xảy ra trong Java, nhưng có ngôn ngữ cho phép). Ngược lại biến có thể in-scope nhưng chưa khởi tạo → NPE hoặc "variable might not have been initialized" compile error.


Bài tiếp theo: Mini-challenge: thư viện NumberUtils với method tĩnh