Java 入门教程
成员变量与局部变量
本教程共 100 篇 · 第 31 篇 · 更新于 2026-08-05 · 约 5 分钟阅读
JavaJava 入门教程成员变量局部变量作用域变量默认值
31. 成员变量与局部变量
本节目标:分清写在类里的成员变量和写在方法里的局部变量,说清它们在默认值、作用域、生命周期上的差别,并能看懂相关的编译报错。
变量放的位置不同,脾气差很多。Java 主要区分两类:成员变量(写在类里、方法外)和局部变量(写在方法或代码块里)。
搞混这两个,会在最基础的地方卡住。下面一项项拆。
成员变量:属于对象
写在类里面、方法外面的变量叫成员变量,也叫字段。它跟着对象走,每 new 一个对象就多一份。
public class Person {
String name; // 成员变量
int age; // 成员变量
void intro() {
System.out.println(name + "," + age + " 岁");
}
}
```bash
类里所有方法都能直接用它,不用当参数传来传去。这是成员变量最实用的地方——它是这个对象内部的"公共数据区"。
上一章说过,成员变量有默认值:数字是 0,布尔是 false,引用是 null。不赋值也能直接读,不会报错。
> [!NOTE]
> 成员变量还细分为实例变量和静态变量。上面这种不带 static 的是**实例变量**,每个对象一份;加了 static 的叫**静态变量**,全类共用一份。static 留到第 35 章讲,本章说的成员变量都指实例变量。
## 局部变量:属于方法
写在方法内部、方法参数位置、或者某个代码块里的变量,都叫局部变量。它只在那一小块范围里活着。
```java
public class Demo {
public static void main(String[] args) { // args 也是局部变量(参数)
int x = 10; // 局部变量,整个 main 可见
if (x > 5) {
int y = 20; // 局部变量,只在 if 的花括号里活
System.out.println(y);
}
// System.out.println(y); // 这里 y 已经不存在了
}
}
把注释那行放开,编译直接失败:
error: cannot find symbol
System.out.println(y);
^
symbol: variable y
location: class Demo
```bash
"找不到符号"不是说你打错字,而是**这个名字在当前位置根本不存在**。花括号一闭合,里面声明的变量就注销了。
## 局部变量为什么必须先赋值
这是两类变量最扎眼的差别:局部变量**没有默认值**。
```java
public class Demo {
public static void main(String[] args) {
int x;
System.out.println(x); // 编译不通过
}
}
报错长这样:
error: variable x might not have been initialized
System.out.println(x);
^
```java
注意措辞是 **might not**(可能没有),不是"一定没有"。编译器做的是保守的流程分析,只要存在一条路径可能读到未赋值的变量,它就拦。
下面这种写法就能通过,因为两条分支都赋了值:
```java
int x;
if (args.length > 0) {
x = 1;
} else {
x = 2;
}
System.out.println(x); // 编译器确认所有路径都赋过值,放行
为什么成员变量能享受默认值、局部变量不行?因为成员变量属于对象,对象在堆上创建时有统一的清零动作;局部变量在栈帧上分配,进方法就有、出方法就没,为了性能不做清零。编译器强制你赋值,等于用编译期检查换掉了运行期的一整类”读到脏数据”的 bug。
Tip这个报错对初学者是好事。C 语言里读未初始化的局部变量不报错,拿到的是内存里的随机值,问题能潜伏很久。Java 直接在编译期堵死。
默认值速查
| 类型 | 成员变量默认值 | 局部变量 |
|---|---|---|
| byte / short / int / long | 0 | 无默认值,必须显式赋值 |
| float / double | 0.0 | 无默认值,必须显式赋值 |
| char | \u0000 | 无默认值,必须显式赋值 |
| boolean | false | 无默认值,必须显式赋值 |
| 引用类型 | null | 无默认值,必须显式赋值 |
五点区别总表
| 对比项 | 成员变量 | 局部变量 |
|---|---|---|
| 定义位置 | 类里、方法外 | 方法内 / 参数 / 代码块内 |
| 默认值 | 有,按类型自动初始化 | 无,用之前必须赋值 |
| 作用范围 | 整个类的所有实例方法 | 声明它的那对花括号内 |
| 生命周期 | 随对象创建而生,对象被回收才亡 | 进块而生,出块即亡 |
| 内存位置 | 堆(跟着对象) | 栈(方法栈帧里) |
| 能否加访问修饰符 | 能,如 private / public | 不能,只能加 final |
最后一行容易被忽略:给局部变量写 private int x; 会直接编译报错,因为访问控制只对类的成员有意义。
同名时:就近原则
局部变量和成员变量重名是允许的,方法里默认用离得近的那个:
public class Demo {
int num = 100; // 成员变量
void test() {
int num = 200; // 局部变量,同名,遮蔽了成员变量
System.out.println(num); // 200,局部优先
System.out.println(this.num); // 100,加 this 才是成员变量
}
}
```java
这种现象叫**遮蔽**。想在方法里明确指成员变量,前面加 `this.`,下一章会专门讲。
同一个方法里的两个局部变量则不能同名:
```java
void bad() {
int x = 1;
int x = 2; // 编译报错
}
error: variable x is already defined in method bad()
int x = 2;
^
```bash
外层块的变量在内层块里也不能被重新声明,规则是一样的。
## 循环变量的作用域
for 语句头里声明的变量,只在循环体内有效:
```java
for (int i = 0; i < 3; i++) {
System.out.println(i);
}
// System.out.println(i); // 报错:cannot find symbol
想在循环结束后还用这个值,就把变量提到循环外面声明。
var 只能用在局部变量上
Java 10 引入了 var,让编译器根据右边表达式推断类型:
var name = "小明"; // 推断为 String
var count = 10; // 推断为 int
var list = new int[3]; // 推断为 int[]
```java
它**只服务于局部变量**。写到成员变量上会被当场拒绝:
```java
public class Demo {
var name = "小明"; // 编译报错
}
error: 'var' is not allowed here
var name = "小明";
^
```bash
原因是成员变量要对整个类甚至外部可见,类型必须写死才能保证接口稳定;局部变量只在几行内活动,推断出来不影响别人。
> [!WARNING]
> var 不是"动态类型"。`var count = 10;` 之后 count 就是铁打的 int,再赋一个字符串照样报错。它只是省了你敲类型名的力气,类型仍然在编译期定死。
## 常见疑问
**Q:成员变量能不能也在声明时就赋值?**
A:能,`int age = 18;` 完全合法,这叫显式初始化,会在对象创建时执行,覆盖掉默认值 0。
**Q:既然成员变量哪里都能用,为什么不把变量全写成成员变量?**
A:作用域越大越难维护。成员变量一多,任何方法都能改它,出了问题得把整个类翻一遍才知道是谁动的。能用局部变量解决的,就别提升成成员变量。
**Q:方法的参数到底算哪一类?**
A:算局部变量,作用域是整个方法体。所以它同样没有"默认值"一说——调用方必须传值进来,编译器会替你把关。
> [!TIP]
> 命名上尽量别让局部变量和成员变量撞名,撞了就得靠 this 区分,读代码的人也费劲。构造方法和 setter 是唯一的例外:那里参数名和字段名保持一致反而更清楚,代价是必须记得写 this。