Scope của một variable là vùng code mà trong đó cái tên của nó chỉ đúng variable ấy. Ra khỏi vùng đó, cái tên không phải là rỗng — nó đơn giản là không tồn tại, và javac từ chối compile. Scope được quyết định hoàn toàn bởi chỗ đặt khai báo, mà trong Java thì điều đó có nghĩa là: bởi cặp ngoặc nhọn bao quanh nó.
Bài này đi qua bốn loại variable mà một chương trình Java khai báo được, mỗi loại nhìn thấy được từ đâu, được tạo ra và mất đi lúc nào, cùng những lỗi hiện ra khi một lần đọc rơi ra ngoài vùng compiler cho phép. Mọi dòng output và mọi error message bên dưới đều lấy từ lần compile và chạy thật trên OpenJDK 21.0.6.
![]()
Một quy tắc gánh gần hết bài: một cái tên sống từ chỗ khai báo tới dấu ngoặc đóng của block chứa nó.
Java không có global variable
Rất nhiều tài liệu, và cả nhiều đề cương, đặt "biến cục bộ" cạnh "biến toàn cục". Java không có thứ thứ hai. Không có chỗ nào để đặt một variable nằm ngoài class, nên cũng không có cái tên nào mà mọi phần của chương trình với tới được mà không cần chỉ rõ chủ sở hữu.
Thứ gần nhất là một static field: variable khai báo thẳng trong class body kèm keyword static. Mỗi class có đúng một bản, mọi method của class đó gọi thẳng bằng tên, còn từ nơi khác thì gọi là ClassName.field.
public class NoGlobal {
// the nearest thing Java has to a "global variable": a static field
static int requestCount = 0;
static void handle() {
requestCount++; // no object needed, no parameter passed
}
public static void main(String[] args) {
handle();
handle();
handle();
System.out.println("NoGlobal.requestCount = " + NoGlobal.requestCount);
System.out.println("Counter.total = " + Counter.total);
Counter.total = 42;
System.out.println("Counter.total = " + Counter.total);
}
}
class Counter {
static int total = 7; // reachable from another class by its name
}
NoGlobal.requestCount = 3
Counter.total = 7
Counter.total = 42
Java chỉ "toàn cục" tới đó, mà vẫn có chủ. requestCount không trôi nổi tự do; nó thuộc về NoGlobal. Khi đề cương của series viết "cục bộ và toàn cục", hãy đọc là "local và static field", còn phần còn lại của bài nói về bốn chỗ mà một cái tên thật sự sống được.
Bốn loại scope của một variable
Mọi variable trong một chương trình Java đều rơi vào một trong bốn loại, và scope của nó suy ra từ loại đó.
| Loại | Khai báo ở đâu | Nhìn thấy được ở đâu | Mỗi bản ứng với |
|---|---|---|---|
| Local variable | trong một method, constructor hoặc block | từ chỗ khai báo tới dấu ngoặc đóng của block đó | một lần thực thi block |
| Parameter | trong parameter list | toàn bộ method body | một lần gọi |
| Instance field | trong class body, không static | mọi instance method của class | một object |
static field | trong class body, có static | mọi method của class | một class |
Hai loại đầu nằm trên stack, hai loại sau nằm trong object hoặc trong class. Một chương trình cho thấy cả bốn cùng lúc.
public class FourScopes {
static String appName = "scope-demo"; // static field: one copy, whole class
String user; // instance field: one copy per object
FourScopes(String user) {
this.user = user;
}
void report(int visits) { // visits: parameter, whole method body
String prefix = "[" + appName + "]"; // local: the method body
if (visits > 0) {
String note = visits + " visit(s)";// local: only this if block
System.out.println(prefix + " " + user + " -> " + note);
}
// note is gone here
System.out.println(prefix + " " + user + " reported");
}
public static void main(String[] args) {
FourScopes a = new FourScopes("ann");
FourScopes b = new FourScopes("bob");
a.report(2);
b.report(0);
System.out.println("appName = " + appName + ", a.user = " + a.user + ", b.user = " + b.user);
}
}
[scope-demo] ann -> 2 visit(s)
[scope-demo] ann reported
[scope-demo] bob reported
appName = scope-demo, a.user = ann, b.user = bob
appName là một giá trị dùng chung cho tất cả. user là hai giá trị, mỗi object một cái. visits và prefix chỉ tồn tại trong lúc report đang chạy, còn note chỉ tồn tại trong lúc thân if đang chạy. this.user xuất hiện trong constructor vì ở đó một parameter tên user đang che mất field; đó chính là chủ đề phần shadowing bên dưới, còn bản thân this sẽ được nói tử tế trong bài về class và object.
Từ static method chỉ đọc được field qua một object
Class body là một scope, nhưng một static method thì không có object nào, nên bên trong nó một instance field không có chỗ lưu trữ nào để đọc ra.
public class StaticContext {
static int shared = 1;
int perObject = 2;
public static void main(String[] args) {
System.out.println("shared = " + shared);
System.out.println("perObject = " + perObject);
}
}
StaticContext.java:7: error: non-static variable perObject cannot be referenced from a static context
System.out.println("perObject = " + perObject);
^
1 error
Cái tên perObject vẫn nằm trong scope — compiler tìm thấy nó, đó là lý do message không phải cannot find symbol. Thứ còn thiếu là một object để đọc nó ra. Chỉ rõ một object là xong:
StaticContextOk o = new StaticContextOk();
System.out.println("shared = " + shared);
System.out.println("perObject = " + o.perObject);
shared = 1
perObject = 2
Đây là bản dành cho variable của lỗi non-static method cannot be referenced from a static context đã nói trong bài về method.
Block scope: cái tên kết thúc ở dấu ngoặc đóng
Block là bất kỳ cặp ngoặc nhọn nào: thân method, thân if, thân loop, hay một cặp { } trơ trọi viết ra chỉ để gom nhóm. Một local khai báo trong một block sẽ hết hiệu lực ở dấu ngoặc đóng của block đó, và sau đó cái tên không còn tồn tại.

public class Scopes {
static int classLevel = 1;
static void run() {
int methodLevel = 2;
if (methodLevel > 0) {
int blockLevel = 3;
use(classLevel, methodLevel, blockLevel);
}
use(classLevel, methodLevel);
use(blockLevel);
}
static void other() {
use(classLevel);
use(methodLevel);
}
static void use(int... values) { }
public static void main(String[] args) {
run();
other();
}
}
Scopes.java:11: error: cannot find symbol
use(blockLevel);
^
symbol: variable blockLevel
location: class Scopes
Scopes.java:16: error: cannot find symbol
use(methodLevel);
^
symbol: variable methodLevel
location: class Scopes
2 errors
Hai lỗi, mà thật ra là cùng một lỗi. Dòng 11 nằm trong run, lệch ra ngoài if đúng một dấu ngoặc, nên blockLevel đã mất. Dòng 16 nằm trong other, một method hoàn toàn khác, nên methodLevel cũng đã mất. classLevel được đọc từ ba chỗ mà không ai kêu ca, vì scope của một field là toàn bộ class body.
cannot find symbol rất đáng đọc kỹ: compiler không nói variable đang rỗng, chưa khởi tạo hay sai giá trị. Nó nói ở chỗ này không hề có cái tên đó.
Counter khai báo trong for header
Lần chạm mặt block scope phổ biến nhất là một counter của loop. Variable khai báo ở phần khởi tạo của for thuộc về loop, cả header lẫn body, và chết cùng loop.
public class BlockScope {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
System.out.println("i = " + i);
}
System.out.println("after the loop, i = " + i);
}
}
BlockScope.java:6: error: cannot find symbol
System.out.println("after the loop, i = " + i);
^
symbol: variable i
location: class BlockScope
1 error
Nếu bạn cần counter đó sau loop — chẳng hạn để biết vòng tìm kiếm dừng ở đâu — hãy khai báo trước loop và để trống phần khởi tạo trong header:
int i = 0;
for (; i < 3; i++) {
System.out.println("i = " + i);
}
System.out.println("after the loop, i = " + i);
i = 0
i = 1
i = 2
after the loop, i = 3
Phần lớn trường hợp bạn không cần tới nó, và bản để i thuộc về loop mới là bản tốt hơn, đúng vì cái tên đó không rò ra ngoài được.
Variable khai báo trong if
Vẫn quy tắc đó, chỉ khác dấu ngoặc. Thân if là một block.
public class IfScope {
public static void main(String[] args) {
int age = 20;
if (age >= 18) {
String status = "adult";
}
System.out.println(status);
}
}
IfScope.java:7: error: cannot find symbol
System.out.println(status);
^
symbol: variable status
location: class IfScope
1 error
Cách sửa là tách khai báo khỏi phép gán: khai báo status ở scope cần đọc nó, còn phép gán thì nằm trong nhánh.
int age = 20;
String status = "minor"; // declared in the scope that needs to read it
if (age >= 18) {
status = "adult"; // assignment, not declaration
}
System.out.println(status);
adult
Một block trơ trọi cũng cư xử y hệt, và đó là minh chứng rõ nhất rằng thứ quyết định chỉ là cặp ngoặc:
{
int temp = 5;
System.out.println("inside the block, temp = " + temp);
}
System.out.println("outside the block, temp = " + temp);
BareBlock.java:7: error: cannot find symbol
System.out.println("outside the block, temp = " + temp);
^
symbol: variable temp
location: class BareBlock
1 error
Những chỗ khác cũng mở ra một scope
Còn ba cấu trúc nữa khai báo một variable có scope đúng bằng block mà nó mở ra. Cả ba cư xử y như counter của for. Đoạn dưới nằm trong một class có import java.util.Scanner.
int[] data = {1, 2, 3};
for (int n : data) { // n: the enhanced-for body
System.out.println("n = " + n);
}
try (Scanner sc = new Scanner("42")) { // sc: the try block
System.out.println("read " + sc.nextInt());
}
try {
throw new IllegalStateException("boom");
} catch (RuntimeException e) { // e: the catch block
System.out.println("caught " + e.getMessage());
}
n = 1
n = 2
n = 3
read 42
caught boom
Đọc n sau vòng enhanced for thì message vẫn là cái bạn đã quen:
OtherScopesBad.java:6: error: cannot find symbol
System.out.println(n);
^
symbol: variable n
location: class OtherScopesBad
1 error
Thứ tự khai báo: local phải đứng trước, field thì không
Với local variable, scope bắt đầu từ chỗ khai báo chứ không phải từ dấu ngoặc mở. Một lần đọc ở dòng phía trên là nằm ngoài scope.
public class LocalOrder {
public static void main(String[] args) {
System.out.println("total = " + total);
int total = 10;
}
}
LocalOrder.java:3: error: cannot find symbol
System.out.println("total = " + total);
^
symbol: variable total
location: class LocalOrder
1 error
Field thì khác. Scope của nó là toàn bộ class body, nên một method dùng được field khai báo bên dưới nó rất xa, mà file vẫn compile và chạy bình thường:
public class FieldOrder {
// show() reads a field that is declared 4 lines BELOW it
static void show() {
System.out.println("greeting = " + greeting);
}
static String greeting = "hello";
public static void main(String[] args) {
show();
}
}
greeting = hello
Đó là lý do thứ tự các thành phần trong một class là chuyện phong cách chứ không phải yêu cầu của compiler, còn thứ tự các dòng lệnh trong một method thì ngược lại.
Có đúng một ngoại lệ, và nó rất hẹp: phần khởi tạo của một field không đọc được field khai báo sau nó, vì các phần khởi tạo chạy lần lượt từ trên xuống.
public class FieldForward {
static int a = b + 1; // reads b before b is declared
static int b = 2;
public static void main(String[] args) {
System.out.println("a = " + a + ", b = " + b);
}
}
FieldForward.java:2: error: illegal forward reference
static int a = b + 1; // reads b before b is declared
^
1 error
Cái tên vẫn nằm trong scope — đó là lý do lỗi không phải cannot find symbol — nhưng giá trị thì chưa có. Bên trong thân method thì cùng một lệnh đọc đó lại hợp lệ, vì tới lúc bất kỳ method nào chạy thì mọi phần khởi tạo đã xong từ lâu.
Definite assignment cho local, default value cho field
Scope quyết định cái tên có tồn tại hay không. Một quy tắc khác mới quyết định nó có chứa gì hay không. Bài về variable và data type đã nói đủ về chuyện này; tóm lại là field được khởi tạo mặc định, local thì không.
public class FieldDefaultsOk {
static int staticCount; // no initializer
int instanceCount; // no initializer
String label; // no initializer
public static void main(String[] args) {
FieldDefaultsOk f = new FieldDefaultsOk();
System.out.println("staticCount = " + staticCount);
System.out.println("instanceCount = " + f.instanceCount);
System.out.println("label = " + f.label);
}
}
staticCount = 0
instanceCount = 0
label = null
Thêm đúng một local chưa khởi tạo vào cùng main đó thì chương trình hết compile được:
int local; // no initializer, and no default either
System.out.println("local = " + local);
FieldDefaults.java:13: error: variable local might not have been initialized
System.out.println("local = " + local);
^
1 error
Từ đáng chú ý là might. Compiler không theo dõi giá trị; nó đang chứng minh rằng mọi đường đi tới lệnh đọc đều đã ghi vào variable trước đó. Chỉ cần một nhánh quên là lời chứng minh đổ:
public class BranchAssign {
public static void main(String[] args) {
int score = 55;
String grade; // declared, deliberately not initialized
if (score >= 60) {
grade = "pass";
}
System.out.println(grade);
}
}
BranchAssign.java:8: error: variable grade might not have been initialized
System.out.println(grade);
^
1 error
Thêm một else có gán grade là lời chứng minh thành công và chương trình in ra fail. Đây cũng là lý do việc tách khai báo khỏi phép gán, như cách sửa if ở trên, vẫn cần phủ hết mọi nhánh — hoặc cho sẵn một giá trị ngay tại chỗ khai báo.
Shadowing: khi local che mất field
Hai variable được phép trùng tên nếu một cái là field còn cái kia là local hoặc parameter. Bên trong method, khai báo gần hơn thắng, và field trở nên không với tới được bằng tên trần. Đó là shadowing, hoàn toàn hợp lệ, đôi khi cố ý, và là nguồn của một trong những bug âm thầm phổ biến nhất trong Java.

Setter hỏng
public class BrokenSetter {
String name = "unset";
void setName(String name) {
name = name; // assigns the parameter to itself
}
public static void main(String[] args) {
BrokenSetter b = new BrokenSetter();
System.out.println("before setName: b.name = " + b.name);
b.setName("ada");
System.out.println("after setName: b.name = " + b.name);
}
}
before setName: b.name = unset
after setName: b.name = unset
Cả hai chữ name trên dòng đó đều trỏ về parameter, nên câu lệnh chép parameter vào chính nó và field không hề nhúc nhích. Đây không phải compile error, mà cũng không phải warning — chạy javac -Xlint:all BrokenSetter.java thì không in ra dòng nào. Bằng chứng duy nhất là output.
this.name gọi thẳng tên field, và đó là cách sửa:
void setName(String name) {
this.name = name; // field on the left, parameter on the right
}
before setName: f.name = unset
after setName: f.name = ada
this là receiver của instance method đang chạy, và nó có phần riêng trong bài về class và object. Tạm thời cứ coi this.x là cách nói "field ấy, không phải local".
Shadowing không chỉ xảy ra với parameter. Bất kỳ local nào trùng tên field cũng làm y như vậy:
public class Shadowing {
int count = 100; // the field
void demo() {
System.out.println("field = " + count);
int count = 5; // a local that shadows the field
System.out.println("local = " + count);
System.out.println("field via this = " + this.count);
count++; // touches the local only
System.out.println("local after ++ = " + count);
System.out.println("field after ++ = " + this.count);
}
public static void main(String[] args) {
new Shadowing().demo();
}
}
field = 100
local = 5
field via this = 100
local after ++ = 6
field after ++ = 100
Để ý dòng đầu tiên: trước chỗ khai báo local, count vẫn nghĩa là field. Cái tên đổi nghĩa ở giữa method. Điều đó hợp lệ, và cũng là lý do rất chính đáng để đừng bao giờ viết như vậy.
Shadow một field một cách cố ý chỉ có đúng một trường hợp thông dụng, là parameter của constructor hay setter đặt tên theo field, và ở đó this. nói rõ ý định. Mọi chỗ khác, hãy đổi tên local.
Không thể shadow một local bằng một local khác
Sự dễ dãi dừng lại ở field. Hai local trùng tên trong cùng một method bị từ chối, dù cái thứ hai nằm cùng block hay trong một block lồng bên trong.
int x = 1;
int x = 2;
RedeclareSame.java:4: error: variable x is already defined in method main(String[])
int x = 2;
^
1 error
int x = 1;
if (x > 0) {
int x = 2; // a nested block cannot shadow an enclosing local
System.out.println(x);
}
RedeclareNested.java:5: error: variable x is already defined in method main(String[])
int x = 2; // a nested block cannot shadow an enclosing local
^
1 error
Parameter cũng tính là local với quy tắc này:
static void f(int n) {
int n = 3; // a local cannot shadow a parameter either
System.out.println(n);
}
RedeclareParam.java:3: error: variable n is already defined in method f(int)
int n = 3; // a local cannot shadow a parameter either
^
1 error
Hai block ngang hàng thì dùng lại tên được, vì cái thứ nhất đã kết thúc trước khi cái thứ hai bắt đầu:
public class SiblingBlocks {
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
System.out.println("first loop, i = " + i);
}
for (int i = 10; i < 12; i++) { // legal: the first i no longer exists
System.out.println("second loop, i = " + i);
}
if (args.length == 0) {
int n = 1;
System.out.println("then branch, n = " + n);
} else {
int n = 2; // legal: a sibling block, not a nested one
System.out.println("else branch, n = " + n);
}
}
}
first loop, i = 0
first loop, i = 1
second loop, i = 10
second loop, i = 11
then branch, n = 1
Chạy không có argument nào thì nhánh else không bao giờ thực thi, nhưng nó vẫn compile: hai khai báo n nằm ở hai block khác nhau, và không cái nào bao cái nào.
Lifetime không phải là scope
Scope là tính chất của văn bản source: những dòng nào được phép gọi tên variable. Lifetime là tính chất của chương trình đang chạy: chỗ lưu trữ đó tồn tại bao lâu. Hai thứ hay bị lẫn vì với local chúng trùng nhau, còn với field thì không.

| Loại | Được tạo lúc | Bị huỷ lúc | Số bản sống cùng lúc |
|---|---|---|---|
| Local variable | khi luồng chạy tới chỗ khai báo | khi block kết thúc, cùng với stack frame | một cho mỗi block đang chạy |
| Parameter | khi method được gọi | khi method return | một cho mỗi lần gọi đang chạy |
| Instance field | khi object được tạo | khi object hết reachable | một cho mỗi object |
static field | khi class được load | khi class được unload, thực tế là lúc JVM thoát | đúng một |
Ba counter trong một class làm khác biệt đó hiện ra.
public class Lifetime {
static int staticCalls = 0; // lives from class load until the JVM exits
int instanceCalls = 0; // lives as long as this object is reachable
void tick() {
int localCalls = 0; // created and destroyed on every call
localCalls++;
instanceCalls++;
staticCalls++;
System.out.println("local " + localCalls
+ " | instance " + instanceCalls
+ " | static " + staticCalls);
}
public static void main(String[] args) {
Lifetime a = new Lifetime();
a.tick();
a.tick();
a.tick();
System.out.println("-- a second object --");
Lifetime b = new Lifetime();
b.tick();
b.tick();
}
}
local 1 | instance 1 | static 1
local 1 | instance 2 | static 2
local 1 | instance 3 | static 3
-- a second object --
local 1 | instance 1 | static 4
local 1 | instance 2 | static 5
Ba hình dạng trong một lần in. localCalls không bao giờ quá 1, vì mỗi lần gọi lại được một slot mới tinh khởi tạo bằng 0. instanceCalls đếm tới 3 với a rồi quay lại 1 với b, vì mỗi object mang bản của riêng nó. staticCalls leo tới 5, vì cả class chỉ có một bản và cả hai object cùng tăng nó.
Recursion còn làm trường hợp local rõ hơn nữa: một method tự gọi chính nó có nhiều frame sống cùng lúc, và mỗi frame có bản riêng của mọi local. Cái tên thì một, chỗ lưu trữ thì mỗi frame một cái.
Local trong loop body được tạo lại mỗi vòng
Thân loop là một block, nên variable khai báo trong đó được tạo ra và huỷ đi ở mỗi lượt. Nó không tích luỹ được gì cả.
public class LoopLocal {
public static void main(String[] args) {
for (int i = 1; i <= 3; i++) {
int sum = 0; // re-created every iteration
sum += i;
System.out.println("inside i=" + i + " sum=" + sum);
}
int total = 0; // declared outside, so it survives the loop
for (int i = 1; i <= 3; i++) {
total += i;
System.out.println("outside i=" + i + " total=" + total);
}
}
}
inside i=1 sum=1
inside i=2 sum=2
inside i=3 sum=3
outside i=1 total=1
outside i=2 total=3
outside i=3 total=6
sum cho ra giá trị của đúng một lượt, không bao giờ là tổng đang chạy, vì int sum = 0; chạy lại ở mỗi vòng. total tích luỹ được vì chỗ khai báo của nó nằm ngoài loop và chỉ chạy một lần. Dời một dòng khai báo qua một dấu ngoặc là đổi luôn ý nghĩa của chương trình.
Đọc sum sau loop lại rơi vào lỗi block scope, nên cái sai đó thường tự khai báo:
LoopLocalRead.java:7: error: cannot find symbol
System.out.println("sum = " + sum);
^
symbol: variable sum
location: class LoopLocalRead
1 error
Bản nguy hiểm là bản mà bên ngoài loop đã có sẵn một variable trùng tên, nên lệnh đọc compile ngon lành rồi lặng lẽ báo về lượt cuối cùng thay vì tổng.
Block lồng nhau, và chuyện lambda thêm vào
Một block bên trong nhìn thấy mọi thứ block bao ngoài đã khai báo trước đó, và ghi vào chúng được. Lồng block chỉ thêm tên vào, không bao giờ che tên bên ngoài, vì khai báo lại là bị từ chối thẳng.
public class NestedBlock {
public static void main(String[] args) {
int outer = 1;
{
int inner = 2;
outer += inner; // an inner block reads and writes the outer local
System.out.println("inner block: outer=" + outer + " inner=" + inner);
}
System.out.println("after block: outer=" + outer);
}
}
inner block: outer=3 inner=2
after block: outer=3
Block thường thì đơn giản như vậy. Anonymous class và lambda thì không: chúng sống lâu hơn cả method tạo ra chúng, nên một local mà chúng capture phải là final hoặc effectively final — gán đúng một lần và không bao giờ gán lại. Gán lại n sau khi một lambda đã capture nó là bị từ chối:
LambdaCapture.java:4: error: local variables referenced from a lambda expression must be final or effectively final
Runnable r = () -> System.out.println("n = " + n);
^
1 error
Quy tắc capture thuộc về phần lambda và được nói kỹ ở khoá nâng cao. Lý do thì đáng gói trong một câu ngay đây: lambda copy cái local đã capture, nên nếu cho phép gán lại thì sẽ có hai giá trị âm thầm lệch nhau.
Khai báo ở scope hẹp nhất có thể
Quy tắc thực dụng suy ra từ mọi thứ ở trên: hãy khai báo mỗi variable sát chỗ dùng đầu tiên nhất có thể, trong block nhỏ nhất chứa hết các lần dùng. Không phải vì scope hẹp thì gọn mắt, mà vì đó là thứ làm code dời đi được.
// wide: every local lives to the end of the method, so nothing is separable
static void wide(int[] data) {
int sum = 0;
int max = data[0];
int i = 0;
for (i = 0; i < data.length; i++) {
sum += data[i];
if (data[i] > max) {
max = data[i];
}
}
System.out.println("wide: sum=" + sum + " max=" + max + " i=" + i);
}
Mọi cái tên trong wide đều sống tới hết method, nên người đọc phải giữ năm cái trong đầu cùng lúc, và muốn tách phần tính giá trị lớn nhất ra thành method riêng thì phải dò từng cái xem còn được dùng ở dưới không. Thu hẹp scope lại thì các mảnh tự rời ra:
static int sum(int[] data) {
int total = 0;
for (int n : data) {
total += n;
}
return total;
}
static int max(int[] data) {
int best = data[0];
for (int n : data) {
if (n > best) {
best = n;
}
}
return best;
}
wide: sum=21 max=9 i=4
narrow: sum=21 max=9
Kết quả như nhau. Khác biệt là total và best không đọc được ở đâu ngoài bốn dòng sở hữu chúng, nên dời mấy dòng đó đi là một thao tác cơ học chứ không phải một cuộc điều tra. Một variable khai báo ở đầu một method dài là một món nợ chưa trả: sớm muộn có người phải ngồi tính xem tầm ảnh hưởng của nó tới đâu.
Từ đó có ba thói quen. Khai báo trong loop khi giá trị thuộc về từng lượt, và khai báo ngoài loop khi nó tích luỹ. Ưu tiên đặt counter ngay trong for header nếu sau loop bạn không cần tới nó. Và khi thấy mình khai báo một variable trước chỗ dùng đầu tiên vài dòng, hãy dời khai báo xuống.
Lỗi thường gặp và error message tương ứng
| Sai ở đâu | Chuyện gì xảy ra | Message |
|---|---|---|
Đọc counter của for sau loop | compile error | cannot find symbol |
Khai báo trong if rồi đọc ở ngoài | compile error | cannot find symbol |
| Đọc một local ở phía trên chỗ khai báo của nó | compile error | cannot find symbol |
Đọc parameter của catch hay biến của enhanced for sau block | compile error | cannot find symbol |
Đọc một instance field từ main | compile error | non-static variable x cannot be referenced from a static context |
| Đọc một local mà có đường đi không gán nó | compile error | variable x might not have been initialized |
| Phần khởi tạo field đọc field khai báo bên dưới | compile error | illegal forward reference |
| Khai báo lại local trong cùng block hoặc block lồng | compile error | variable x is already defined in method ... |
Viết name = name; trong setter | compile được, không làm gì, không warning | không có |
| Khai báo biến cộng dồn ngay trong thân loop | compile được, reset mỗi vòng | không có |
| Vô tình shadow một field rồi đọc phải local | compile được, sai giá trị | không có |
Ba dòng cuối không sinh ra bất kỳ cảnh báo nào, và chúng có chung một nguyên nhân: cái tên có trỏ tới thứ gì đó, chỉ là không phải variable bạn định nói. Khi giá trị sai chứ không phải thiếu, câu hỏi đầu tiên nên là cái tên đó thật sự trỏ về khai báo nào.
FAQ
Khác biệt giữa local variable và global variable trong Java là gì?
Java không có global variable. Local variable được khai báo trong một method hoặc một block và chỉ tồn tại ở đó; thứ gần với "global" nhất là static field, khai báo trong class body, mỗi class một bản, và với tới được bằng ClassName.field từ bất cứ đâu nhìn thấy class đó. Nó vẫn thuộc về class ấy, nên là có namespace chứ không phải toàn cục.
Vì sao Java báo cannot find symbol với counter của loop?
Vì variable khai báo trong for header thuộc về loop và hết tồn tại ở dấu ngoặc đóng, nên sau loop cái tên đó không được định nghĩa. Hoặc khai báo variable trước loop rồi để trống phần khởi tạo trong header, hoặc viết lại để không cần tới counter sau loop.
Local variable có được trùng tên với field không?
Được. Local sẽ shadow field trong suốt scope của nó, và từ đó field chỉ với tới được bằng this.field với instance field, hoặc ClassName.field với static field. Chuyện này compile mà không có warning, nên hãy shadow một cách cố ý, hoặc đừng shadow.
Vì sao name = name không phải là lỗi trong Java?
Vì cả hai lần xuất hiện đều trỏ về khai báo gần nhất, tức là parameter, nên câu lệnh là một phép tự gán hợp lệ của parameter vào chính nó. Field không hề bị đụng tới, javac không nói gì, và -Xlint:all cũng không nói gì. Hãy viết this.name = name;.
Local variable bị huỷ lúc nào trong Java?
Ở dấu ngoặc đóng của block khai báo ra nó, khi stack frame của nó bị bỏ đi. Variable khai báo trong thân loop bị huỷ rồi tạo lại ở mỗi lượt, đó là lý do nó không cộng dồn được qua các vòng lặp.
Có khai báo hai variable trùng tên trong các block lồng nhau được không?
Không. javac từ chối với variable x is already defined in method ..., và quy tắc này tính cả parameter. Hai block ngang hàng, không cái nào bao cái nào, thì dùng lại tên được, vì khai báo thứ nhất đã ra khỏi scope trước khi cái thứ hai xuất hiện.
Kết luận
Scope trong Java do cặp ngoặc nhọn quyết định, không do gì khác. Một local chạy từ chỗ khai báo tới dấu ngoặc đóng của block; một parameter phủ toàn bộ method body; một field phủ toàn bộ class body, và đó là lý do một method dùng được field khai báo bên dưới nó. cannot find symbol nghĩa là cái tên không tồn tại ở điểm đó, non-static variable ... cannot be referenced from a static context nghĩa là nó tồn tại nhưng không có object nào để sống trong, còn variable x might not have been initialized nghĩa là nó tồn tại và đang chứa thứ mà compiler không dám bảo đảm.
Lifetime là câu hỏi tách biệt về việc chỗ lưu trữ sống bao lâu: một slot cho mỗi lần gọi với local, một slot cho mỗi object với instance field, một slot cho mỗi class với static field. Shadowing là chỗ hai ý đó đụng nhau — cái tên vẫn trỏ tới đâu đó, chỉ là sai slot, và name = name; là trường hợp làm người ta mất cả buổi chiều. Khai báo ở scope hẹp nhất có thể là thứ chặn được phần lớn những chuyện đó.
Bài tiếp theo trong series: recursion — một method tự gọi chính nó, base case dừng nó lại, mỗi lần gọi có bản riêng của mọi local ra sao, và vì sao thiếu base case thì kết thúc bằng StackOverflowError.