Java Review (八、面向对象----成员变量和局部变量)
@
在Java语言中,根据定义变量位置的不同,可以将变量分成两大类:成员变量和局部变量。成员变量和局部变量的运行机制存在较大差异。
成员变量和局部变量是什么
成员变量指的是声明在一个类中,但在方法、构造方法和语句块之外的变量, 局部变量指的是声明在方法、构造方法或者语句块中里的变量。
不管是成员变量还是局部变量,都应该遵守相同的命名规则:从语法角度来看,只要是一个合法的标识符即可;但从程序可读性角度来看,应该遵循驼峰命名法的原则:首个单词首字母小写,后面每个单词首字母大写。
成员变量
Java 程序中的变量划分如图一:变量被分为类变量和实例变量两种,定义成员变量时没有 static 修饰的就是实例变量,有 static 修饰的就是类变量。
- 类变量从该类的准备阶段起开始存在, 直到系统完全销毁这个类,类变的作用域与这个类的生存范围相同;
- 实例变量从该类的实例被创建起开始存在,直到系统完全销毁这个实例,实例变量的作用域与对应实例的生存范围相同;
正是基于这个原因,可以把类变量和实例变量统称为成员变量。
只要类存在,程序就可以访问该类的类变量。在程序中访问类变量通过如下语法
类.类变量
只要实例存在,程序就可以访问该实例的实例变量。在程序中访问实例变量通过如下语法:
实例.实例变量
当然,类变量 可以让该类的实例来访问 通过实例来访问类变量的语法如下:
实例.类变量
但由于这个实例并不拥有这个类变量,因此它访问的并不是这个实例的变量,依然是访问它对应类的类变量。
也就是说如果通过一个实例修改了类变量的值,由于这个类变并不属于它,而是属于它对应的类,因此,修改的依然是类的类变量,与通过该类来修改类变量的结果完全相同。
下面程序定义了 Person类, 在这个Person类中定义两个成员变量,一个实例变量 name ,以及一个类变量 eyeNum,并分别通过 Person 类和 Person实例来访问实例变量和类变量:
class Person {
//定义一个实例变量
public String name;
//定义一个类变量
public static int eyeNum;
}
public class PersonTest {
public static void main(String[] args) {
//第一次主动使用Person类,该类自动初始化,则eyeNum变量开始起作用,输出
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);
}
}
结果:
Person 的 eyeNum 类变量值:0
p变量的 name变量值是:null;p对象的 eyeNum 变量值是:0
p变量的 name变量值是:孙悟空;P 对象的 eyeNum 变量值是:2
Person类的 eyeNum 类变量值:2
p2对象的 eyeNum类变量值:2
成员变量无须显式初始化:只要为一个类定义了类变量或实例变量,系统就会在这个类的准备阶段或创建该类的实例时进行默认初始化,成员变量默认初始化时的赋值规则与数组动态初始化时数组元素的赋值规则完全相同。
类变量的作用域比实例变量的作用域更大:实例变量随实例的 存在而存在,而类变量则随类的存在而存在。实例也可访问类变量,同一个类的所有实例访问类变量时, 实际上访问的是该类本身的同一个变量,也就是说,访问了同一片内存区。
局部变量
局部变量根据定义形式的不同,又可以被分为如下三种。
- 形参:在定义方法签名时定义的变量,形参的作用域在整个方法内有效。
- 方法局部变量:在方法体内定义的局部变量,它的作用域是从定义该变量的地方生效,到该方 法结束时失效。
- 代码块局部变量:在代码块中定义的局部变量,这个局部变量的作用域从定义该变量的地方生 效,到该代码块结束时失效。
与成员变量不同的是,局部变量除了形参之外,都必须显式初始化。也就是说,必须先给方法局部变量和代码块局部变量指定初始值,否则不可以访问它们。
-
代码块局部变量的作用域是所在代码块,只要离开了代码块局部变量所在的代码块,这个局部变量就立即被销毁, 变为不可见
-
方法局部变量,其作用域从定义该变量开始,直到该方法结束
-
形参的作用域是整个方法体内有效,而且形参也无须显式初始化,形参的初始化在调用该方法时由 系统完成,形参的值由方法的调用者负责指定
当通过类或对象调用某个方法时,系统会在该方法栈区内为所有的形参分配内存空间,并将实参的值赋给对应的形参,这就完成了形参的初始化。
作用范围与命名
在同一个类里,成员变量的作用范围是整个类内有效,
- 一个类里不能定义两个同名的成员变量,即使一个是类变量,一个是实例变量也不行;
- 一个方法里不能定义两个同名的方法局部变量,方法局部变量与形参也不能同名;
- 同一个方法中不同代码块内的代码块局部变量可以同名;
- 如果先定义代码块局部变量,后定义方法局部变量,前面定义的代码块局部变量与后面定义的方法局部变量也可以同名。
Java允许局部变量和成员变量同名,如果方法里的局部变量和成员变量同名,局部变量会覆盖成员 变量,如果需要在这个方法里引用被覆盖的成员变量,则可使用this (对于实例变量)或类名(对于类变量)作为调用者来限定访问成员变量。
成员变量的初始化和内存中的运行机制
当系统加载类或创建该类的实例时,系统自动为成员变量分配内存空间,并在分配内存空间后,自动为成员变量指定初始值。
//创建第一个Person对象
Person pl = new Person();
//创建第二个Person对象
Person p2 = new Person();
//分别对两个Person对象的name实例变量赋值
pl.name = "张三";
p2.name = "孙悟空";
//分别为两个Person对象的eyeNum类变量赋值
pl.eyeNum =2;
p2.eyeNum = 3;
- 当程序执行第一行代码Person p1 = new Person();时,如果这行代码是第一次使用Person类,则系统通常会在第一次使用Person类时加载这个类,并初始化这个类。在类的准备阶段,系统将会为该类的类变量分配内存空间,并指定默认初始值。 当Person类初始化完成后,系统内 存中的存储示意图如图一所示。
从图一中可以看出,当Person 类初始化完成后,系统将在堆内存中为Person类分配一块内存区(当 Person类初始化完成后,系统会为 Person类创建一个类对象),在这块内存区里包含了保存eyeNum类变量的内存,并设 置eyeNum的默认初始值:0。
- 系统接着创建了一个Person 象,并把这个Person对象赋给p1变量,Person对象里包含了名为name 的实例变量,实例变量是在创建实例 时分配内存空间并指定初始值的。当 创建了第一个Person对象后,系统内存中的存储示意图如图二所示。
从图二中可以看出,eyeNum类变量并不属于Person对象,它是属于Person类的,所以创建第 ―个Person对象时并不需要为eyeNum类变量分配内存,系统只是为name实例变量分配了内存空间, 并指定默认初始值:null。
-
接着执行Person p2 = new Person(); 代码创建第二个Person对象,此时因为Person类已经存在于堆内存中了,所以不再需要对Person类进行初始化。创建第二个Person对象与创建第一个Person对象并 没有什么不同。
-
当程序执行 p1.name ="张三"; 代码时,将为p1的name实例变量赋值,也就是让图二中堆内存中的name指向”张三"字符串。执行完成后,两个Person对象在内存中的存储示意图如图三所示。
从图三中可以看出,name实例变量是属于单个Person实例的,因此修改第一个Person对象的 name实例变量时仅仅与该对象有关,与Person类和其他Person对象没有任何关系。同样,修改第二个 Person对象的name实例变量时,也与Person类和其他Person对象无关。
- 直到执行p1.eyeNum = 2;代码时,此时通过Person对象来修改Person的类变量,从图三中看出,Person对象根本没有保存eyeNum这个变量,通过p1访问的eyeNum类变量,其实还是Person 类的eyeNum类变量。因此,此时修改的是Person类的eyeNum类变量。修改成功后,内存中的存储示 意图如图四所示。
从图四中可以看出,当通过p1来访问类变量时,实际上访问的是Person类的eyeNum类变量。 事实上,所有的Person实例访问eyeNum类变量时都将访问到Person类的eyeNum类变量,也就是图四中灰色覆盖的区域,本质其实还是通过Person类来访问eyeNum类变量时,访问的是同一块内存。基于这个理由,当程序需要访问类变量时,尽量使用类作为主调,而不要使用对象作为主调,这样可以避免程序产生歧义,提高程序的可读性。
局部变量的初始化和内存中的运行机制
局部变量定义后,必须经过显式初始化后才能使用,系统不会为局部变量执行初始化。这意味着定 义局部变量后,系统并未为这个变量分配内存空间,直到等到程序为这个变量赋初始值时,系统才会为局部变量分配内存,并将初始值保存到这块内存中。
与成员变量不同,局部变量不属于任何类或实例,因此它总是保存在其所在方法的栈内存中。
- 如果局部变量是基本类型的变量,则直接把这个变量的值保存在该变量对应的内存中;
- 如果局部变量是一个 引用类型的变量,则这个变量里存放的是地址,通过该地址引用到该变量实际引用的对象或数组。
栈内存中的变量无须系统垃圾回收,往往随方法或代码块的运行结束而结束。因此,局部变量的作用域是从初始化该变量开始,直到该方法或该代码块运行完成而结束。因为局部变量只保存基本类型的值或者对象的引用,因此局部变量所占的内存区通常比较小。
**参考:**
【1】:《疯狂Java讲义》
【2】:《Java核心技术 卷一》
【3】:https://www.runoob.com/java/java-variable-types.html