变量作用域
变量作用域
课程: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
三处值得停下来看的:
d1.name一开始是null。成员变量有默认值(引用类型为null,数值类型为 0,布尔为false),局部变量没有。这是两类变量最容易记混的差别。shared出现了两个值:裸写是 100(局部变量),ScopeDemo.shared是 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。
八、实际编码的三条纪律
- 用完即止:变量声明尽量贴近第一次使用的位置,别在方法开头堆一堆声明。
- 避免重名:除非是
setXxx(String x)这种公认写法(配合this.),否则别让局部变量和成员变量同名。 - 注意循环变量:
for (int i = ...)的i出了循环就没了;如果循环结束后还需要这个值,要提前声明在外层。
小结:作用域是编译器帮你划的边界。它不让你做的事(局部变量重名),
往往正是容易出 bug 的地方;它允许你做的事(局部屏蔽成员),
则给了你 `this.` / `类名.` 两个明确的找回方式。

浙公网安备 33010602011771号