Command Palette

Search for a command to run...

[Java Basics] Phạm vi biến trong Java: biến cục bộ, field và static

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.

Ba cặp ngoặc lồng nhau, mỗi cấp khai báo một variable

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ạiKhai báo ở đâuNhìn thấy được ở đâuMỗi bản ứng với
Local variabletrong một method, constructor hoặc blocktừ chỗ khai báo tới dấu ngoặc đóng của block đómột lần thực thi block
Parametertrong parameter listtoàn bộ method bodymột lần gọi
Instance fieldtrong class body, không staticmọi instance method của classmột object
static fieldtrong class body, có staticmọi method của classmộ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. visitsprefix 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.

Ba scope lồng nhau trên một file source, kèm hai lần đọc bị javac từ chố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 ghi parameter slot ngược về chính nó, đối lại this.name ghi sang field slot

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.

Ba counter trên cùng một trục thời gian: một local, một instance field, một static field

LoạiĐược tạo lúcBị huỷ lúcSố bản sống cùng lúc
Local variablekhi luồng chạy tới chỗ khai báokhi block kết thúc, cùng với stack framemột cho mỗi block đang chạy
Parameterkhi method được gọikhi method returnmột cho mỗi lần gọi đang chạy
Instance fieldkhi object được tạokhi object hết reachablemột cho mỗi object
static fieldkhi class được loadkhi 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à totalbest 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 ở đâuChuyện gì xảy raMessage
Đọc counter của for sau loopcompile errorcannot find symbol
Khai báo trong if rồi đọc ở ngoàicompile errorcannot find symbol
Đọc một local ở phía trên chỗ khai báo của nócompile errorcannot find symbol
Đọc parameter của catch hay biến của enhanced for sau blockcompile errorcannot find symbol
Đọc một instance field từ maincompile errornon-static variable x cannot be referenced from a static context
Đọc một local mà có đường đi không gán nócompile errorvariable x might not have been initialized
Phần khởi tạo field đọc field khai báo bên dướicompile errorillegal forward reference
Khai báo lại local trong cùng block hoặc block lồngcompile errorvariable x is already defined in method ...
Viết name = name; trong settercompile được, không làm gì, không warningkhông có
Khai báo biến cộng dồn ngay trong thân loopcompile được, reset mỗi vòngkhông có
Vô tình shadow một field rồi đọc phải localcompile đượ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.

Bài viết liên quan

[Java Basics] Câu lệnh điều kiện trong Java: if, else if, else và switch

Câu lệnh điều kiện trong Java: chuỗi if / else if / else, vì sao phải có ngoặc nhọn, bẫy dangling else, điều kiện chỉ nhận boolean, switch với break và fall-through, label mũi tên, switch expression cùng yield và pattern matching của Java 21.

[Java Basics] Tính kế thừa trong Java: extends và super

extends hoạt động thế nào, subclass kế thừa gì và không kế thừa gì, vì sao constructor không bao giờ được kế thừa, super(...) xâu chuỗi constructor lên tới java.lang.Object rồi chạy ngược xuống ra sao, field hiding khác overriding chỗ nào, protected qua package, final class, vấn đề fragile base class, và khi nào composition là lựa chọn đúng.

[Java Basics] Phương thức trong Java: khai báo và gọi method

Cách khai báo và gọi method trong Java: các thành phần của một declaration, static method so với instance method và lỗi non-static method cannot be referenced from a static context, return statement, call stack và StackOverflowError, cách đọc stack trace, Javadoc, cùng những lỗi compiler thật mà người mới hay gặp.

[Java Basics] Ép kiểu dữ liệu trong Java: implicit và explicit

Ép kiểu dữ liệu trong Java: widening và narrowing, tại sao (byte) 300 in ra 44, tại sao byte cộng byte không compile được, cast ẩn bên trong += và vì sao cast không phải là parsing.