成员变量和局部变量
在Java语言中,根据定义变量位置的不同,可以将变量分成两大类:
- 成员变量
- 局部变量
成员变量和局部变量的运行机制存在较大差异
成员变量和局部变量差异
###成员变量
成员变量指的是在类范围里定义的变量,也就是前面所介绍的Field;局部变量指的是在方法里定义的变量。变量划分如下图:
类Field的作用域与这个类的生存范围相同;而实例Field则从该类的实例被创建起开始存在,直到系统完全销毁这个实例,实例Field的作用域与对应实例的生存范围相同。
在程序中访问类Field通过如下语法:类.类Field
只要实例存在,程序就可以访问该实例的实例Field。在程序中访问实例Field通过如下语法:实例.实例Field
当然,类Field也可以让该类的实例来访问。通过实例来访问类Field的语法如下:实例.类Field
但由于这个实例并不拥有这个类Field,因此它访问的并不是这个实例的Field,依然是访问它对应类的类Field。如果修改的话修改的是类的Field。
下面程序定义了一个Person类,在这个Person类中定义两个成员变量,一个Field:name,以及一个类Field:eyeNUm。程序还通过PersonTest类来创建Person实例,并分别通过Person类和Person实例来访问实例Field和类Field。
class Person
{
//定义一个实例Field
public String name;
//定义一个类Field
public static int eyeNum;
}
public class PersonTest
{
public static void main(String[] args)
{
//Person类已经初始化了,则eyeNum变量起作用了,输出0
System.out.println("Person的eyeNum类Field值:"+ Person.eyeNum);
//创建Person对象
Person p=new Person();
//通过Person对象的引用p来访问Person对象name实例Field
//并通过实例访问eyeNum类Field
System.out.println("p变量的name Field值是:" + p.name+ " p对象的eyeNum值是:" + p.eyeNum);
//直接为name实例Field赋值
p.name="孙悟空";
// 通过p访问eyeNum类Field,依然是访问Person的eyeNum类Field
p.eyeNum=2;
// 再次通过Person对象来访问name实例Field和eyeNum类Field
System.out.println("p变量的name Field值是:" + p.name+ " p对象的eyeNum Field值是:" + p.eyeNum);
//前面通过p修改了Person的eyeNum,此处的Person.eyeNum将输出2
System.out.println("Person的eyeNum类Field值:" + Person.eyeNum);
Person p2=new Person();
//p2访问的eyeNum类Field依然引用Person类的,因此依然输出2
System.out.println("p2对象的eyeNum类Field值:"+ p2.eyeNum); }
}
从上面程序来看,成员变量无须显式初始化,只要为一个类定义了类Field或实例Field,系统就会在这个类的准备阶段或创建该类的实例时进行默认初始化,成员变量默认初始化时的赋值规则与数组动态初始化时数组元素的赋值规则完全相同。
从上面程序运行结果来看,不难发现类Field的作用域比实例Field的作用域更大:实例Field随实例的存在而存在,而类Field则随类的存在而存在。实例也可访问类Field,同一个类的所有实例访问类Field时,实际上访问的是该类本身的同一个Field,也就是说,访问了同一片内存区。
局部变量
局部变量根据定义形式的不同,又可以被分为如下三种:
- 形参:在定义方法签名时定义的变量,形参的作用域在整个方法内有效。
- 方法局部变量:在方法体内定义的局部变量,它的作用域是从定义该变量的地方生效,到该方法结束时失效。
- 代码块局部变量:在代码块中定义的局部变量,这个局部变量的作用域从定义该变量的地方生效,到该代码块结束时失效。
与成员变量不同的是,局部变量除了形参之外,都必须显式初始化。也就是说,必须先给方法局部变量和代码块局部变量指定初始值,否则不可以访问它们。
下面代码是定义代码块局部变量的实例:
public class BlockTest
{
public static void main(String[] args)
{
{
//定义一个代码块局部变量a
int a;
// 下面代码将出现错误,因为a变量还未初始化
System.out.println("代码块局部变量a的值:" + a);
//为a变量赋初始值,也就是进行初始化
a=5;
System.out.println("代码块局部变量a的值:" + a);
}
//下面试图访问的a变量并不存在
System.out.println(a);
}
}
只要离开了局部变量所在的代码块,这个局部变量立即被销毁,变为不可见。
对于方法局部变量,其作用于从定义该变量开始知道该方法结束,下面示范方法局部变量的作用:
public class MethodLocalVariableTest
{
public static void main(String[] args)
{
//定义一个方法局部变量a
int a;
// 下面代码将出现错误,因为a变量还未初始化
// System.out.println("方法局部变量a的值:" + a);
//为a变量赋初始值,也就是进行初始化
a=5;
System.out.println("方法局部变量a的值:" + a);
}
}
在同一个类里,成员变量的作用范围是整个类内有效,一个类里不能定义两个同名的成员变量,即使一个是类Field,一个是实例Field也不行;一个方法里不能定义两个同名的局部变量,即使一个是方法局部变量,一个是代码块局部变量或者形参也不行。
Java允许局部变量和成员变量同名,如果方法里的局部变量和成员变量同名,局部变量会覆盖成员变量,如果需要在这个方法里引用被覆盖的成员变量,则可使用this(对于实例Field)或类名(对于类Field)作为调用者来限定访问成员变量,如下面程序:
public class VariableOverrideTest
{
//定义一个name实例Field
private String name="ffzs";
//定义一个price类Field
private static double price=78.0;
//主方法,程序的入口
public static void main(String[] args)
{
//方法里的局部变量,局部变量覆盖成员变量
int price=65;
// 直接访问price变量,将输出price局部变量的值:65
System.out.println(price);
//使用类名作为price变量的限定
//将输出price类Field的值:78.0
System.out.println(VariableOverrideTest.price);
// 运行info方法
new VariableOverrideTest().info();
}
public void info()
{
//方法里的局部变量,局部变量覆盖成员变量
String name="孙悟空";
// 直接访问name变量,将输出name局部变量的值:"孙悟空"
System.out.println(name);
//使用this来作为name变量的限定
//将输出price实例Field的值:"ffzs"
System.out.println(this.name);
}
}
成员变量的初始化和内存中的运行机制
当系统加载类或创建该类的实例时,系统自动为成员变量分配内存空间,并在分配内存空间后,自动成为成员变量指定初始值。
//创建第一个Person对象
Person p1=new Person();
//创建第二个Person对象
Person p2=new Person();
//分别为两个Person对象的name Field赋值
p1.name="张三";
p2.name="孙悟空";
//分别为两个Person对象的eyeNum Field赋值
p1.eyeNum=2;
p2.eyeNum=3;
当程序执行第一行代码时:如果是第一次使用Person类,系统会初始化这个类,并为该类的Field分配内存空间,并指定默认初始值,初始化后,系统中内存如下:
内存区里包含了保存eyeNum类Field的内存,并设置eyeNum的默认初始值:0
系统接着创建了一个Person对象,并把这个Person对象赋给p1变量,Person对象里包含了名为name的实例Field,实例Field是在创建实例时分配内存空间并指定初始值的:
eyeNum类Field并不属于Person对象,它是属于Person类的,所以创建第一个Person对象时并不需要为eyeNum类Field分配内存,系统只是为name实例Field分配了内存空间,并指定默认初始值:null。
执行第二行代码,创建第二个Person对象,此时Person类已经存在于堆内存中了,所以不再需要对Person类进行初始化,这与上一步相同。
当执行第三行代码p1.name="张三"时,为p1的name赋值,name实例Field是属于单个Person实例的,因此修改第一个Person对象的name实例Field时仅仅与该对象有关,与Person类和其他Person对象没有任何关系,如图:
直到执行p1.eyeNum=2;代码时,此时通过Person对象来修改Person的类Field,Person对象根本没有保存eyeNum这个Field,通过p1访问的eyeNum类Field,其实还是Person类的eyeNum类Field。因此,此时修改的是Person类的eyeNum类Field,如图:
通过p1来访问类Field时,实际上访问的是Person类的eyeNum类Field。事实上,所有的Person实例访问eyeNum Field时都将访问到Person类的eyeNum Field,不管通过哪个Person实例来访问eyeNum Field,本质其实还是通过Person类来访问eyeNum Field,它们所访问的是同一块内存。基于这个理由,笔者建议读者,当程序需要访问类Field时,尽量使用类作为主调,而不要使用对象作为主调,这样可以避免程序产生歧义,提高程序的可读性。
这时我们再看这段代码的结果就更明朗了:
class Person
{
//定义一个实例Field
public String name;
//定义一个类Field
public static int eyeNum;
}
public class PersonTest
{
public static void main(String[] args)
{
//Person类已经初始化了,则eyeNum变量起作用了,输出0
System.out.println("Person的eyeNum类Field值:"+ Person.eyeNum);
//创建Person对象
Person p=new Person();
//通过Person对象的引用p来访问Person对象name实例Field
//并通过实例访问eyeNum类Field
System.out.println("p变量的name Field值是:" + p.name+ " p对象的eyeNum值是:" + p.eyeNum);
//直接为name实例Field赋值
p.name="孙悟空";
// 通过p访问eyeNum类Field,依然是访问Person的eyeNum类Field
p.eyeNum=2;
// 再次通过Person对象来访问name实例Field和eyeNum类Field
System.out.println("p变量的name Field值是:" + p.name+ " p对象的eyeNum Field值是:" + p.eyeNum);
//前面通过p修改了Person的eyeNum,此处的Person.eyeNum将输出2
System.out.println("Person的eyeNum类Field值:" + Person.eyeNum);
Person p2=new Person();
//p2访问的eyeNum类Field依然引用Person类的,因此依然输出2
System.out.println("p2对象的eyeNum类Field值:"+ p2.eyeNum); }
}
运行程序,结果如下:
Person的eyeNum类Field值:0
p变量的name Field值是:null p对象的eyeNum值是:0
p变量的name Field值是:孙悟空 p对象的eyeNum Field值是:2
Person的eyeNum类Field值:2
p2对象的eyeNum类Field值:2
局部变量的初始化和内存中的运行机制
局部变量定义后,必须经过显式初始化后才能使用,系统不会为局部变量执行初始化。这意味着定义局部变量后,系统并未为这个变量分配内存空间,直到等到程序为这个变量赋初始值时,系统才会为局部变量分配内存,并将初始值保存到这块内存中。
与成员变量不同,局部变量不属于任何类或实例,因此它总是保存在其所在方法的栈内存中。如果局部变量是基本类型的变量,则直接把这个变量的值保存在该变量对应的内存中;如果局部变量是一个引用类型的变量,则这个变量里存放的是地址,通过该地址引用到该变量实际引用的对象或数组。
栈内存中的变量无须系统垃圾回收,往往随方法或代码块的运行结束而结束。因此,局部变量的作用域是从初始化该变量开始,直到该方法或该代码块运行完成而结束。因为局部变量只保存基本类型的值或者对象的引用,因此局部变量所占的内存区通常比较小。
变量的使用规则
只是用全局变量不适用局部变量,这种做法相当错误,因为当我们定义一个成员变量时,成员变量将被放置到堆内存中,成员变量的作用域将扩大到类存在范围或者对象存在范围,这种范围的扩大有两个害处:
- 增大了变量的生存时间,这将导致更大的内存开销;
- 扩大了变量的作用域,这不利于提高程序的内聚性。
比较下面三个程序:
程序一
public class ScopeTest1
{
//定义一个类成员变量作为循环变量
static int i;
public static void main(String[] args)
{
for ( i=0 ; i < 10 ; i++)
{
System.out.println("Hello");
}
}
}
程序二
public class ScopeTest2
{
public static void main(String[] args)
{
//定义一个方法局部变量作为循环变量
int i;
for ( i=0 ; i < 10 ; i++)
{
System.out.println("Hello");
}
}
}
程序三
public class ScopeTest3
{
public static void main(String[] args)
{
//定义一个代码块局部变量作为循环变量
for (int i=0 ; i < 10 ; i++) {
System.out.println("Hello");
}
}
}
这三个程序的运行结果完全相同,但程序的效果则大有差异。第三个程序最符合软件开发规范:对于一个循环变量而言,只需要它在循环体内有效,因此只需要把这个变量放在循环体内(也就是在代码块内定义),从而保证这个变量的作用域仅在该代码块内。
如果有如下几种情形,则应该考虑使用成员变量:
- 如果需要定义的变量是用于描述某个类或某个对象的固有信息的,例如人的身高、体重等信息,它们是人对象的固有信息,每个人对象都具有这些信息。这种变量应该定义为成员变量。如果这种信息对这个类的所有实例完全相同,或者说它是类相关的,例如人类的眼睛数量这个Field,目前所有人的眼睛数量都是2,如果人类进化了,变成了3个眼睛,则所有人的眼睛数量都是3,这种类相关的信息应该定义成类Field;如果这种信息是实例相关的,例如人的身高、体重等,每个人实例的身高、体重可能互不相同,这种信息是实例相关的,因此应该定义成实例Field。
- 如果在某个类中需要以一个变量来保存该类或者实例运行时的状态信息,例如上面五子棋程序中的棋盘数组,它用以保存五子棋实例运行时的状态信息。这种用于保存某个类或某个实例状态信息的变量通常应该使用成员变量。
- 如果某个信息需要在某个类的多个方法之间进行共享,则这个信息应该使用成员变量来保存。例如,在把浮点数转换为人民币读法字符串的程序中,数字的大写字符和单位字符等是多个方法的共享信息,因此应设置为成员变量。
即使在程序中使用局部变量,也应该尽可能地缩小局部变量的作用范围,局部变量的作用范围越小,它在内存里停留的时间就越短,程序运行性能就越好。因此,能用代码块局部变量的地方,就坚决不要使用方法局部变量。