变量作用域

变量作用域

课程:Java 语言程序设计
教师:王建民
班级:信2505-2 学号:20253909 姓名:刘家瑞


一、一句话本质

变量作用域 = 这个变量能在哪几行代码里被合法引用。

它在哪一层 {} 里定义,它的「户口」就落在哪一层,出了这层 {} 编译器就不认识它了。

Java 的作用域是词法作用域(也叫静态作用域):编译器只看代码的嵌套结构决定变量归谁,跟运行时调用顺序无关。这一点是理解后面一切的根——所以下面所有结论,都能靠"看括号"而不是"想运行过程"推出来。


二、四种作用域

1. 块级作用域(最内层,最常用)

for (int i = 0; i < 3; i++) {
    int sum = i + 1;
    System.out.println(sum);
}
// 这一行之外,写 i 或 sum 都会编译报错

用法要点:

  • 只要用 {} 包起来就是一个作用域:if / for / while / switch / 甚至裸写的 { } 都算。
  • 变量从"定义那一行"开始生效,不是从 {} 开头生效。 所以下面这段是错的:
int a = b + 1;   // 编译错误:b 在这一行还不存在
int b = 5;
  • 越小越好。变量只声明在真正需要它的那块里,出了块自动失效,既方便回收,也避免误用。

2. 方法(局部)作用域

int sumTo(int n) {
    int total = 0;              // 整个方法体内有效
    for (int k = 1; k <= n; k++) {
        total += k;             // 内层块能看见外层变量
    }
    return total;
}

用法要点:

  • 局部变量没有默认值,不初始化就直接用是编译错误。这点和成员变量正好相反。
  • 形参也是局部变量,作用域是整个方法体。
  • 同层不能重名;内层也不能重名(这条和很多人的印象相反,见第四节)。

3. 成员变量(实例级)

class Student {
    private String name;        // 不初始化默认是 null
    private int age;            // 默认是 0

    void setName(String name) { // 形参 name 屏蔽了成员 name
        this.name = name;       // 用 this 取回成员变量
    }
}

用法要点:

  • 有效范围:整个类体(写在前面的方法能看见后面的成员,跟顺序无关)。
  • 访问方式:类内部直接用名字,类外部用 对象.变量。
  • 每个对象各持一份,对象创建时诞生、被回收时消失。
  • 会被形参和局部变量屏蔽,但可以靠 this. 找回。

4. 静态变量(类级)

class Counter {
    static int total = 0;       // 所有对象共享一份

    void tick() {
        total++;                // 每调用一次就加 1
    }
}

用法要点:

  • 有效范围:整个类体,其他类可 类名.变量 访问(前提是访问权限允许)。
  • 只有一份,类加载时初始化,程序结束才销毁。
  • 适合做计数器、配置常量、共享状态。
  • 会被局部变量屏蔽,可以靠 类名. 找回。

三、把四种作用域跑起来看

光讲规则容易混,下面这段程序把四种作用域放在一起,并把「屏蔽」和「找回」都演示出来:

public class ScopeDemo {

    static int shared = 0;          // 静态变量(类级):全类一份
    private String name;            // 成员变量(实例级):每个对象一份,默认 null

    void tick() { shared++; }       // 实例方法可以访问静态成员

    void setName(String name) {     // 形参 name 屏蔽了成员 name
        this.name = name;           // 用 this 找回
    }

    String getName() { return name; }

    int sumTo(int n) {
        int total = 0;              // 局部变量:整个方法体内有效
        for (int k = 1; k <= n; k++) {
            total += k;             // 内层块能看见外层局部变量
        }
        return total;
    }

    public static void main(String[] args) {
        ScopeDemo d1 = new ScopeDemo();
        ScopeDemo d2 = new ScopeDemo();

        System.out.println("d1.name = " + d1.getName());

        d1.setName("刘家瑞");
        System.out.println("d1.name = " + d1.getName());

        d1.tick(); d1.tick(); d2.tick();
        System.out.println("ScopeDemo.shared = " + ScopeDemo.shared);

        d1.setName("d1 的名字");
        d2.setName("d2 的名字");
        System.out.println("d1.name = " + d1.getName() + ",d2.name = " + d2.getName());

        int shared = 100;           // 局部变量屏蔽了同名的静态成员
        System.out.println("裸写 shared        = " + shared);
        System.out.println("ScopeDemo.shared   = " + ScopeDemo.shared);

        System.out.println("sumTo(5) = " + d1.sumTo(5));
    }
}

运行结果:

========== 1. 成员变量的默认值 ==========
d1.name = null   (String 引用类型默认 null)

========== 2. 用 this 找回被形参屏蔽的成员 ==========
d1.name = 刘家瑞

========== 3. 静态变量共享一份 vs 实例变量各一份 ==========
ScopeDemo.shared = 3   (2 + 1 = 3,全类共享)
d1.name = d1 的名字,d2.name = d2 的名字   (各一份)

========== 4. 局部变量屏蔽静态成员后怎么找回 ==========
裸写 shared        = 100
ScopeDemo.shared   = 3   ← 用 类名. 找回

========== 5. 内层块能看见外层局部变量 ==========
sumTo(5) = 15

三处值得停下来看的:

  1. d1.name 一开始是 null。成员变量有默认值(引用类型为 null,数值类型为 0,布尔为 false),局部变量没有。这是两类变量最容易记混的差别。
  2. shared 出现了两个值:裸写是 100(局部变量),ScopeDemo.shared 是 3(静态成员)。局部变量把静态成员屏蔽了,但成员本身没被销毁,只是"看不见"了。
  3. d1 和 d2 的 name 互不影响,而 shared 是共享的——这就是"实例级"和"类级"的字面差别。

四、被屏蔽之后怎么找回原变量

同名变量内层会屏蔽外层,但外层变量没被销毁,只是"看不见"了。显式写全名就能拿回来:

想访问的变量 写法 能否找回
被局部变量 / 形参屏蔽的静态成员 类名.变量名 能
被局部变量 / 形参屏蔽的实例成员 this.变量名 能
被内层块屏蔽的外层局部变量 —— 根本不会发生,见下节

前两行是关键:成员(实例 / 静态)可以靠 this. 或 类名. 找回,局部变量之间则连"重名"这一步都不被允许。


五、一个必须澄清的点:内层块到底能不能重名?

很多资料(包括一些课堂笔记)会说"内层块可以和外层重名,重名后外层就访问不到了"。这个说法是错的。

Java 的规则是:同一个方法(或构造方法、初始化块)内部,局部变量之间根本不允许互相屏蔽。 不管是在内层 for 体里、还是在 if 块里,只要外层已经声明过同名局部变量,内层再声明一次就是编译错误。

我把四种"想重名"的写法都试了一遍,javac 全部拒绝:

// ① 同一层重复声明
int total = 1;
int total = 2;

// ② 内层块重新声明外层的局部变量
int total = 1;
for (int k = 0; k < 1; k++) {
    int total = 2;
}

// ③ for 头部变量与循环体重名
for (int i = 0; i < 3; i++) {
    int i = 9;
}

// ④ 形参与局部变量重名
static void f(int a) {
    int a = 1;
}

报错原文:

# ① 同一层重复声明
ErrSameLevel.java:4: 错误: 已在方法 main(String[])中定义了变量 total
        int total = 2;      // 同一个 {} 里重复声明
            ^

# ② 内层块重新声明外层的局部变量
ErrInnerShadow.java:5: 错误: 已在方法 main(String[])中定义了变量 total
            int total = 2;  // 内层块想重新声明外层的局部变量
                ^

# ③ for 头部变量与循环体重名
ErrForHead.java:4: 错误: 已在方法 main(String[])中定义了变量 i
            int i = 9;
                ^

# ④ 形参与局部变量重名
ErrParamLocal.java:3: 错误: 已在方法 f(int)中定义了变量 a
        int a = 1;
            ^

四处报错信息一模一样,都是「已在方法 X 中定义了变量 Y」——说明在编译器眼里它们属于同一类问题。

所以正确的表述是:

  • 局部变量之间:不论同层还是跨层,一律不能重名。
  • 局部变量 / 形参 屏蔽成员变量:允许,并且可以用 this. 或 类名. 找回。
  • 一旦重名,被屏蔽的是成员,不是局部变量;局部变量之间不存在"屏蔽后找不回"的问题,因为压根不允许。

这个差别很实际:它意味着你永远不会写出"外层局部变量被内层同名变量悄悄遮住、结果读到意外的值"的 bug——编译器直接拦住了。


六、另外几个常踩的编译错误

同样是实测出来的报错原文:

# 变量在定义之前被使用
ErrForward.java:3: 错误: 找不到符号
        int a = b + 1;      // b 在这一行还不存在
                ^
  符号:   变量 b
  位置: 类 ErrForward

# 局部变量没有默认值,不初始化就用
ErrUninit.java:4: 错误: 可能尚未初始化变量x
        System.out.println(x);
                           ^

# for 循环变量出了循环就没了
ErrLoopVar.java:7: 错误: 找不到符号
        System.out.println(i);      // i 出了 for 就没了
                           ^
  符号:   变量 i
  位置: 类 ErrLoopVar

# 静态方法里直接使用实例成员
ErrStaticCtx.java:5: 错误: 无法从静态上下文中引用非静态 变量 count
        System.out.println(count);      // 静态方法里直接用实例成员
                           ^

# 静态字段的前向引用
ErrStaticField.java:2: 错误: 非法前向引用
    static int a = b + 1;   // b 在后面几行才声明
                   ^

这里有一处很容易混:同样是"用到了还没声明的变量",报错信息却因位置不同而不同。

场景 报错
局部变量引用后面才声明的局部变量 找不到符号:变量 b
静态字段引用后面才声明的静态字段 非法前向引用
实例字段引用后面才声明的实例字段 非法前向引用

区别在于「前向引用」这条规则是专门针对成员字段的(因为字段的书写顺序不等于初始化顺序,而初始化顺序又是按源码顺序来的);局部变量则是直接当作"这个名字还不存在"处理。

以上报错对应的源码文件都在 Program/编译报错示例/ 下,可逐个用 javac -encoding UTF-8 文件名.java
复现(这些文件本来就是故意写错的,编译失败是预期结果)。
能正常编译运行的示例在 Program/ScopeDemo.java。


七、选哪个作用域?决策规则

场景 该用
临时算中间结果、循环计数 局部变量(块级,越小越好)
描述对象的属性(姓名、余额) 实例成员变量
所有对象共享的常量 / 计数器 static 变量(常量再加 final)
方法接收的输入 形参

一句话口诀:能放局部就别放成员,能放成员就别放静态。 作用域越窄,耦合越低,越不容易出 bug。


八、实际编码的三条纪律

  1. 用完即止:变量声明尽量贴近第一次使用的位置,别在方法开头堆一堆声明。
  2. 避免重名:除非是 setXxx(String x) 这种公认写法(配合 this.),否则别让局部变量和成员变量同名。
  3. 注意循环变量:for (int i = ...) 的 i 出了循环就没了;如果循环结束后还需要这个值,要提前声明在外层。
小结:作用域是编译器帮你划的边界。它不让你做的事(局部变量重名),
往往正是容易出 bug 的地方;它允许你做的事(局部屏蔽成员),
则给了你 `this.` / `类名.` 两个明确的找回方式。
posted @ 2026-09-25 14:05  liujerry  阅读(2)  评论(0)    收藏  举报