JAVA 面向对象 成员变量和局部变量

时间:2023-02-15 13:36:48

本页面更新日期: 2016年07月20日

前言

在 Java语言中, 根据定义变量位置的不同,可以将变量分成两大类:

  • 成员变量
  • 局部变量

成员变量和局部变量的运行机制存在很大差异,下面我们看看差异在哪.

成员变量

成员变量指的是在类里定义的变量.
局部变量指的是在方法里定义的变量.

下面我给出Java程序中的变量划分图:

JAVA 面向对象 成员变量和局部变量

成员变量被分为类变量和实例变量两种.
定义成员变量时没有 static 修饰符的就是实例变量.
有static修饰符的就是类变量.

其中, 类变量从该类的准备阶段起开始存在.
直到系统完全销毁这个类,类变量的作用域与这个类的生存范围相同.
而实例变量则从该类的实例被创建起开始存在,知道系统完全销毁这个实例.
实例变量的作用域与对应实例的生存范围相同.

小知识: 一个类在使用之前需要经过 类加载 / 类验证 / 类准备 / 类解析 / 类初始化 等几个阶段.
关于类的生命周期, 我们以后会详细介绍.

正是基于以上原因, 可以把类变量和实例变量统称为成员变量.
其中, 类变量可以理解为 类成员变量, 它作为类本身的一个成员, 与类本身共存亡.
实例变量则可以理解为 实例成员变量, 它作为实例的一个成员, 与实例共存亡.

只要类存在, 程序就可以访问该类的类变量.
在程序中通过如下语法访问:

类.类变量

只要实例存在, 程序就可以访问该实例的实例变量.
在程序中通过如下语法访问:

实例.实例变量

当然, 类变量也可以让该类的实例来访问.
通过实例访问类变量的语法如下:

实例.类变量

但由于这个实例并不拥有这个类变量.
因此它访问的并不是这个实例的变量,依然是访问它对应类的类变量.
也就是说, 如果通过一个实例修改了类变量的值, 由于这个类变量并不属于它.
而是属于它对应的类. 因此, 修改的依然是类的类变量.
与通过该类来修改类变量的结果完全相同.
这会导致该类的其它实例来访问这个类变量时也获得这个被修改过的值.

上面啰嗦了半天, 不过其内容我们之前已经体验过, 你能明白就行.

下面我写个程序, 定义了一个 Person 类, 在这个 Person 类中定义两个成员变量.
一个类变量: eyeNum
一个实例变量: name
程序通过 PersonTest 类来创建 Person 实例.
并分别通过 Person 类 和 Person 实例来访问实例变量和类变量.

class Person
{
//定义一个类变量
public static int eyeNum;
//定义一个实例变量
public String name;
}

public class PersonTest
{

public static void main(String[] args)
{
//第一次主动使用 Person 类, 该类自动初始化, 则 eyeNum 变量开始起作用, 输出 0
System.out.println("Person 的 eyeNum 类变量的值:" + Person.eyeNum);
//创建 Person 对象
Person p = new Person();
//通过 Person 对象的引用 p 来访问 Person 对象的 name 实例变量
//并通过实例访问 eyeNum 类变量
System.out.println("p 对象的 name 变量的值是:" + p.name + "p 对象的 eyeNum 变量的值是:" + p.eyeNum);
//直接为 name 实例变量赋值
p.name = "孙悟空";
//通过 p 访问 eyeNum 类变量, 依然是访问 Person 的 eyeNum 类变量
p.eyeNum = 2;
//再次通过 Person 对象来访问 name 实例变量 和 eyeNum 类变量
System.out.println("p 对象的 name 变量值是:" + p.name + "p 对象的 eyeNum 变量值是:" + p.eyeNum);
//前面通过 p 修改了 Person 的 eyeNum, 此处的 Person.eyeNum 将输出 2
System.out.println("Person 的 eyeNum 类变量值:" + Person.eyeNum);
Person p2 = new Person();
//p2 访问的 eyeNum 类变量依然引用 Person 类的, 因此依然输出 2
System.out.println("p2 对象的 eyeNum 类变量值:" + p2.eyeNum);
}
}

从上面程序可以看出, 成员变量无须显式初始化.
只要为一个类定义了 类变量 或 实例变量.
系统就会在这个类的准备阶段或创建该类的实例时进行默认初始化.
成员变量默认初始化的赋值规则与我们之前讲的 数组动态初始化 时数组元素的赋值规则完全相同.

我们还可以得知, 类变量的作用域 比 实例变量的作用域 更大.
实例变量随实例的存在而存在.
而类变量则随类的存在而存在.
实例也可以访问类变量, 同一个类的所有实例访问类变量时.
实际*问的是该类本身的同一个变量, 也就是说, 访问了同一片内存区.

注意!!!
Java 允许实例访问 static 修饰的类变量本身就是一个错误.
因此建议你以后看到通过实例来访问 static 修饰的类变量时, 都可以将它替换成通过类本身来访问. 这样程序的可读性 / 明确性 都会大大提高!

局部变量

局部变量根据定义形式的不同, 可以分为如下三种:

  • 形参: 在定义方法签名时定义的变量, 形参的作用域在这个方法内有效.
  • 方法局部变量: 在方法体内定义的局部变量, 它的作用域就是从定义该变量的地方生效, 到该方法结束时失效.
  • 代码块局部变量: 在代码块中定义的局部变量, 这个局部变量的作用域从定义该变量的地方生效, 到该代码块结束时失效.

与成员变量不同的是, 局部变量除了形参之外, 都必需显式初始化.
也就是说, 必需先给方法局部变量和代码块局部变量指定初始值, 否则不可以访问它们.
下面我写个定义代码块局部变量的程序.

public class BlockTest
{

public static void main(String[] args)
{
{
//定义一个代码块局部变量 a
int a;
//下面代码将会出现错误, 因为 a 变量没有初始化
System.out.println("代码块局部变量 a 的值:" + a);
}
//下面试图访问 a 变量, 但 a 变量的作用域根本无法涉及这里
System.out.println(a);
}
}

上面的代码是一个错误示例, 如果你写出来还要运行的话, 只能说你 too yang to simple.
从上面代码可以看出, 只要离开了 代码块局部变量 所在的代码块, 这个局部变量就没法用了.

对于方法局部变量, 其作用域从定义该变量开始, 直到该方法结束.
下面我写个 方法局部变量的作用域 示例.

public class MethodLocalVariableTest
{

public static void main(String[] args)
{
//定义一个方法局部变量 a
int a;
//下面代码将会出现错误, 因为 a 变量没有初始化
System.out.println("方法局部变量 a 的值:" + a);
}
}

下面我们说形参.
形参的作用域时整个方法体内有效, 而且形参也无须显式初始化.
形参的初始化在调用该方法时由系统完成, 形参的值由方法的调用者负责指定.

在同一个类里, 成员变量的作用域是整个类内有效.
一个类里不能定义两个同名的成员变量.
就算一个是类变量, 一个是实例变量也不行.

一个方法里不能定义两个同名的成员变量.
方法局部变量与形参名也不能同名.
同一个方法中不同代码块内的代码块局部变量可以同名.

如果先定义代码块局部变量, 后定义方法局部变量.
前面定义的代码块局部变量与后面定义的方法局部变量是可以同名的.

Java允许局部变量和成员变量同名.
如果方法里的局部变量和成员变量同名, 局部变量会覆盖成员变量.
如果需要在这个方法里引用被覆盖的成员变量.
可以使用 this (对于实例变量) 或 类名(对于类变量) 来作为调用者.
下面, 我写个程序.

public class VariableOverrideTest
{

//定义一个 name 实例变量
private String name = "猪八戒";
//定义一个 price 类变量
private static double price = 78.0;
//主方法, 程序的入口
public static void main(String[] args)
{
//方法里的局部变量, 局部变量覆盖成员变量
int price = 65;
//直接访问 price 变量, 将输出 price 局部变量的值.
System.out.println(price);
//使用类名作为 price 变量的调用者, 访问被覆盖的 类变量
System.out.println(VariableOverrideTest.price);
//运行 info 方法
new VariableOverrideTest().info();
}
public void info()
{
//方法里的局部变量, 局部变量覆盖成员变量
String name = "孙悟空";
//直接访问 name 变量, 将输出 孙悟空
System.out.println(name);
//使用 this 来作为 name 的调用者, 访问 实例变量
System.out.println(this.name);
}
}

从上面代码可以看出, 当局部变量覆盖成员变量时.
依然可以在方法中显式指定 类名和this 作为调用者来访问被覆盖的成员变量, 这使得变成更加*.
不过, 不过, 不过 . 你应该尽量避免这种局部变量和成员变量同名的情形. (想个名字真的有那么难么 - - )

成员变量的初始化和内存中的运行机制

当系统加载类或创建该类的实例时.
系统将自动为成员变量分配内存空间.
并在分配内存空间后, 自动为成员变量指定初始值.

下面通过代码来创建两个实例(非完整代码,能明白就行).
同时配合示意图来说明 Java 成员变量的初始化和内存中的运行机制.

//创建第一个 Person 对象
Person p1 = new Person();
//创建第二个 Person 对象
Person p2 = new Person();
//分别为两个 Person 对象的 name 实例变量赋值
p1.name = "孙悟空";
p2.name = "皮卡丘";
//分别为两个 Person 对象的 eyeNum 类变量赋值
p1.eyeNum = 2;
p2.eyeNum = 3;

下面开始解读:
当程序执行第一行代码 Person p1 = new Person(); 时
如果这行代码是第一次使用 Person 类.
则系统会加载并初始化这个类.
在类的准备阶段.
系统将会为该类的类变量分配内存空间,并指定默认初始值.
当 Person 类初始化完成后, 系统内存中的存储示意图如下:

JAVA 面向对象 成员变量和局部变量

从上图可以看出.
当 Person 类初始化完成后.
系统将在堆内存中为 Person 类分配一块内存区.
在这块内存区中, 包含了 保存 eyeNum 类变量的内存.
并设置 eyeNum 的默认初始值为: 0

系统接着创建了一个 Person 对象.
并把这个 Person 对象赋给 p1 变量.
Person 对象里包含了名为 name 的实例变量.
实例变量是在创建实例时分配内存空间并指定初始值的.
当创建了第一个 Person 对象后, 系统内存中的存储示意图如下:

JAVA 面向对象 成员变量和局部变量

从上图可以看出, eyeNum 类变量并不属于 Person 对象.
它是属于 Person 类的.
所以创建第一个 Person 对象时并不需要为 eyeNum 类变量分配内存(废话…)
系统只为 name 实例变量分配了内存空间.
并指定默认初始值: null

接着执行 Person p2 = new Person();
代码创建第二个 Person 对象.
此时因为 Person 类已经存在于堆内存了.
所以不需要对 Person 类进行初始化(废话…Java 会那么傻么…)
创建第二个 Person 对象 与 创建第一个 Person 对象并没有什么不同.

当程序执行 p1.name = “孙悟空”; 时
将为 p1 的 name 实例变量赋值.
也就是让堆内存中的 name 指向 “孙悟空” 字符串.
我们之前说过, 字符串也是一种引用变量. 所以你懂的.
执行完成后, 两个 Person 对象在内存中的存储示意图如下:

JAVA 面向对象 成员变量和局部变量

从上图可以看出, name 实例变量是属于单个 Person 实例的.
因此, 修改第一个 Person 对象的 name 实例变量时仅仅与 p1 对象有关.
与 Person 类和其它 Person 对象没有任何关联.
同理, 修改第二个 Person 对象 p2 的 name 实例变量时, 也与 Person 类和其它 Person 对象无关.

直到执行 p1.eyeNum = 2 时
此时呢, 就是犯大忌了. 你拿 对象来操作类变量了. 不过为了教学演示, 我拿自己当典型.
从我们看过的图当中, 可以知道.
Person 的对象根本没有保存 eyeNum 这个变量.
通过 p1 访问的 eyeNum 类变量.
其实还是 Person 类的 eyeNum 类变量.
因此, 此时修改的是 Person 类的 eyeNum 类变量.
修改成功后, 内存中的存储示意图如下:

JAVA 面向对象 成员变量和局部变量

从上图可以看出.
不管通过哪个 Person 实例来访问 eyeNum 类变量.
它们访问的其实都是同一块内存.
所以就再次提醒你.
当程序需要访问 类变量时.
尽量使用类作为主调, 而不要使用对象作为主调.
这样可以避免歧义, 提高程序的可读性.

局部变量的初始化和内存中的运行机制

局部变量定义后.
必需经过显式初始化后才能使用.
系统不会为局部变量执行初始化.
这意味着,定义局部变量之后,系统并未为这个变量分配内存控件.
直到等程序为这个变量赋初始值时.
系统才会为局部变量分配内存,并将初始值保存到这块内存中去.

与成员变量不同,局部变量不属于任何类或实例.
因此它总是保存在其所在的方法的栈内存中.
如果局部变量是基本类型变量,则直接把这个变量的值保存在该变量对应的内存中.
如果局部变量是引用类型的变量,则这个变量里存放的就是地址.
通过该地址引用到该变量实际引用的对象或数组.

变量的使用规则

对于新手来说.
什么时候使用类变量?
什么时候使用实例变量?
什么时候使用方法局部变量?
什么时候使用代码块局部变量?
这种选择比较困难,如果仅仅从程序的运行结果来看,大部分时候都可以直接使用类变量或实例变量来解决问题.无须使用局部变量.
实际上这种做法非常错误.
因为定义一个成员变量时,成员变量将被放置到堆内存中.
成员变量的作用域将扩大到类存在范围或对象存在范围,这种返回的扩大有两个害处.

  • 增大了变量的生存时间,这将导致更大的内存开销(如果你以后开发Android,大内存开销会让你的APP无情被用户卸载)
  • 扩大了变量的作用域,这不利于提高程序的内聚性. (什么是内聚性?)

让我们做个对比, 这样更好理解.

public class ScopeTest01
{

//定义一个类成员变量作为循环变量
static int i;
public static void main(String[] args)
{
for(i = 0; i < 10; i++)
{
System.out.println("Hello");
}
}
}
public class ScopeTest02
{

public static void main(String[] args)
{
//定义一个方法局部变量作为循环变量
int i;
for(i = 0; i < 10; i++)
{
System.out.println("Hello");
}

}
}
public class ScopeTest03
{

public static void main(String[] args)
{
//定义一个代码块局部变量作为循环变量
for(int i = 0; i < 10; i++)
{
System.out.println("Hello");
}
}
}

从程序的运行结果上来看, 这三者完全相同.
但程序的效果则有很大差异.
第三个程序最符合软件开发规范:
对于一个循环变量而言, 只需要它在循环体内有效即可.
因此只需要把这个变量放在循环体内(也就是在代码块内定义), 从而保证这个变量的作用域仅仅在该代码块内.

我们要根据实际需要,来决定变量的作用域.例如下面的例子:

  • 如果需要定义的变量是用于描述某个类或对象的固有信息的.例如人的身高 / 体重等信息. 它们时人对象的固有信息, 每个人对象都具有这些信息. 这种变量应定义为成员变量.如果这种信息对于这个类的所有实例完全相同,或者说它是类相关的,例如人类的眼睛数量,这种类相关的信息就应该定义为类变量.如果信息和实例相关,例如人的身高 / 体重等, 每个人实例的身高 / 体重可能不同, 这种信息是实例相关的, 就应该定义为实例变量.

即使在程序中使用局部变量, 也应该尽可能的缩小局部变量的作用范围.
局部变量的作用范围越小, 它在内存里停留的时间就越短, 程序性能就越好.
因此, 能用代码块局部变量的地方,就坚决不要使用方法局部变量.