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ường | Java |
|---|---|
| Phòng | Block { ... } |
| Nhãn trên kệ | Tên biến |
| Cùng tên nhãn ở phòng khác | Shadowing |
| Đồ còn/hết khi đóng phòng | Lifetime 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ến | Scope | Lifetime |
|---|---|---|
| Local / parameter | Block 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 class | Bằng lifetime của object (đến khi GC thu hồi) |
Field static | Truy xuất qua ClassName.field từ mọi nơi | Từ 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);
}
}
totalOrdersCreatedsống từ khi classOrderđược JVM load đến khi JVM tắt.idsống cùng mỗi objectOrder— object bị GC thìidbiến mất.prefixsống trong một lần gọiprint()— method return → frame pop →prefixbiế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-resources và catch
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 và 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:
- JLS §6.3 — Scope of a Declaration — định nghĩa chính xác scope của mọi loại declaration.
- JLS §6.4 — Shadowing and Obscuring — rules shadowing giữa declaration và reference.
- JLS §4.12.4 — final Variables — định nghĩa "effectively final".
- JLS §15.27.2 — Lambda Body — yêu cầu capture biến effectively final.
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-resourcesgiới hạn scope của resource trong try block — không reuse được ngoài.
10. 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);
▸
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);
}
▸
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;
}
}
▸
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++;
▸
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++ là 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.
Q5Phân biệt scope và lifetime 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