对象引用和指针
当调用代码
Person p=new Person();
这行代码实际上产生了两个东西:一个是p
变量(引用变量);一个是Person
对象.这行代码的含义是:创建Person
实例,并把这个Person
实例赋值给一个引用变量p
.
当我们创建了一个Person
对象时,系统会自动为这个Person
对象开辟一块堆内存用来存储这个Person
对象,
Person
对象赋值给一个引用变量p
时,系统不会把这个Person
对象在内存中重新复制一份,Java
让引用变量指向这个对象即可.也就是说:这个引用变量p
里存放的仅仅是一个引用(地址,指针),它指向实际的对象
程序中定义的Person
类型的变量Person p
实际上是一个引用,它被存放在栈内存里,指向实际的Person
对象;而真正的Person
对象则存放在堆内存中.
下图显示了将Person
对象赋给一个引用变量的示意图
当一个对象被创建出来之后,这个对象被放在堆内存中,
Java
程序不允许直接访问堆内存中的对象,只能通过该对象的引用来操纵该对象,不管是数组还是对象都只能通过引用来访问它们.
堆内存中的对象可以有多个引用,即多个引用变量指向同一个对象.
方法的参数传递机制
java
中方法的参数传递的方式只有一种:值传递.所谓值传递:就是将实际参数的副本(复制品)传入方法内,而参数的本身不会受到任何的影响.
基本类型的参数传递
public class PrimitiveTransferTest
{
public static void swap(int a , int b)
{
// 下面三行代码实现a、b变量的值交换。
// 定义一个临时变量来保存a变量的值
int tmp = a;
// 把b的值赋给a
a = b;
// 把临时变量tmp的值赋给a
b = tmp;
System.out.println("swap方法里,a的值是"
+ a + ";b的值是" + b);
}
public static void main(String[] args)
{
int a = 6;
int b = 9;
swap(a , b);
System.out.println("交换结束后,变量a的值是"
+ a + ";变量b的值是" + b);
}
}
结果如下:
swap方法里,a的值是9;b的值是6
交换结束后,变量a的值是6;变量b的值是9
swap()
方法里的a
和b
只是main()
方法里变量a
和b
的复制品.java
程序总是先从main()
方法开始执行,main()
方法里开始定义了a,b
两个局部变量,两个变量在内存中的存储示意图如下所示
当程序开始执行
swap()
方法时,系统开始进入swap()
方法,并将main()
方法中的a,b
变量作为参数值传入swap()
方法,传入swap()
方法的只是a,b
的副本,而不是a,b
本身,进入swap()
方法后系统产生了4个变量,这4个变量在内存中的存储示意图如下所示main()方法中变量作为参数传入swap()方法存储示意图
程序在
swap()
方法中交换a
,b
两个变量的值,实际上是对下图中灰色区域的a
,b
变量进行交换,交换结束后swap()
方法中输出a
,b
变量的值,看到a
的值为9,b
的值为6image.png
引用类型的参数传递
Java
对引用类型的参数传递一样采取值传递的方式
class DataWrap
{
int a;
int b;
}
public class ReferenceTransferTest
{
public static void swap(DataWrap dw)
{
// 下面三行代码实现dw的a、b两个成员变量的值交换。
// 定义一个临时变量来保存dw对象的a成员变量的值
int tmp = dw.a;
// 把dw对象的b成员变量值赋给a成员变量
dw.a = dw.b;
// 把临时变量tmp的值赋给dw对象的b成员变量
dw.b = tmp;
System.out.println("swap方法里,a成员变量的值是"
+ dw.a + ";b成员变量的值是" + dw.b);
// 把dw直接赋为null,让它不再指向任何有效地址。
dw = null;
}
public static void main(String[] args)
{
DataWrap dw = new DataWrap();
dw.a = 6;
dw.b = 9;
swap(dw);
System.out.println("交换结束后,a成员变量的值是"
+ dw.a + ";b成员变量的值是" + dw.b);
}
}
结果如下:
swap方法里,a成员变量的值是9;b成员变量的值是6
交换结束后,a成员变量的值是9;b成员变量的值是6
很容易引起一个幻觉:调用swap()
方法时,传入的是dw
对象的本身而不是它的复制品,但这只是一种幻觉.
首先程序从main()
方法开始执行,main()
方法中开始创建了一个DataWrap
对象,并定义了一个dw
引用变量指向DataWrap
对象.创建一个对象时,系统内存中有两个东西:一个是堆内存中存储了对象本身,栈内存中保存了引用该对象的引用变量.接下来程序通过引用来操作DataWrap
对象,把该对象的a,b
两个成员变量分别赋值为6和9.此时的存储示意图如下:
接着开始在
main()
方法中调用swap()
方法,main()
方法并没有结束,系统会为main()
和swap()
开辟出两个栈区,用来存放main()
和swap()
方法的局部变量.调用swap()
方法时,dw
会作为实参传入swap()
方法,同样采取值传递的方式:值得指出的是.main()
方法中的dw
是一个引用(也就是一个指针),它保存了DataWrap
对象的地址,当把dw
的值赋给swap()
方法的dw
形参后,即让swap()
方法的形参也保存了这个地址值,即也会引用到DataWrap
对象.image.png
当程序在
swap()
方法中操作dw
形参时,由于dw
是一个引用变量,故实际操作的还是堆内存中的DataWrap
对象.不管操作main()
方法中的dw
变量,还是操作swap()
方法中的dw
参数,其实都是操作它们所引用的DataWrap
对象,它们引用的是同一个对象.因此当swap()
方法中交换dw
参数所引用DataWrap
对象的a,b
两个成员变量的值后,可以看到main()
方法中dw
变量所引用DataWrap
对象的a,b
两个成员变量的值也被交换了.总结:不管是基本类型的参数传递还是引用类型的参数传递都是先复制然后值传递,只不过后者传递的是一个引用变量(地址)
成员变量的初始化和内存中的运行机制
class Person
{
// 定义一个实例变量
public String name;
// 定义一个类变量
public static int eyeNum;
}
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);
}
}
//创建第一个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
类时加载这个类,并初始化这个类.在该类的准备阶段,系统会为该类的类变量分配内存空间,并指定默认初始值.当Person
类初始化完成后,系统内存中的存储示意图如下所示:
当
Person
类初始化完成后,系统将在堆内存中为Person
类分配一块内存区(当Person
类初始化完成后,系统会为Person
类创建一个类对象),在这块内存区里包含了保存eyeNum
类变量的内存,并设置eyeNum
的默认初始值:0.系统接着创建了一个
Person
对象,并把这个Person
对象赋给p1
变量,Person
对象里包含了名为name
的实例变量,实例变量是在创建实例时分配内存空间并指定初始值.当创建了第一个Person
对象后,系统内存中存储状态示意图如下所示:image.png
从图中可以看出
eyeNum
变量并不属于Person
对象,而是属于Person
类的,所以创建一个Person
对象时并不需要为eyeNum
类变量分配内存,系统只是为了name
实例变量分配了内存空间,并指定默认初始值:null接着执行
Person p2=new Person();
代码创建第二个Person
对象,此时Person
类已经存在于堆内存中了,所以不需要再对Person
类进行初始化了.创建第二个Person
对象与创建第一个Person
对象并没有什么不同.当程序执行
p1.name="张三";
代码时,将为p1
的name
实例变量赋值,也就是让堆内存中的name
指向"张三"字符串image.png
从上图可以看出,
name
实例变量是属于单个Person
实例的,因此修改任何一个Person
对象的name
实例仅仅与该对象有关,与Person
类和其他的Person
对象没有任何关系.直到执行
p1.eyeNum=2;
代码时,此时通过Person
对象来修改Person
类的类变量,从上图中不难看出Person
对象根本就没有eyeNum
这个变量,通过p1
访问eyeNum
类变量,其实是Person
类的eyeNum
类变量.因此此时修改的是Person
类的eyeNum
类变量.修改之后的内存图image.png
事实上,所有的
Person
实例访问eyeNum
类变量都是访问的是Person
类的eyeNum
类变量,换句话来说:不管通过哪个Person
实例来访问eyeNum
类变量,本质上其实还是通过Person
类来访问eyeNum
类变量,它们访问的是同一块内存.
注意:当程序需要访问类变量时,尽量使用类作为主调,而不要使用对象作为主调,这样可以避免程序产生歧义,提高程序的可读性.
网友评论