一、运行程序看结果
有这样一段代码,你是否知道运行结果并作出合理的解释。
一个简单的实体类Person,里面只有一个name属性:
publicclass Person {
public String name;
}
与之对应的一个PersonService,里面有两个方法,做同样一件事——改变Person实例的name值,只是实现方式不同:
publicclass PersonService {
publicvoid changeName(Person p){
p.name="alan";
}
publicvoid changeNameTwo(Person p){
p=new Person();
p.name="alan";
}
}
在客户端中分别调用这两个方式,打印结果是什么?又是否会一致?
测试一:
publicclass Client {
publicstaticvoid main(String[] args) {
PersonService ser=new PersonService();
Person p=new Person();
p.name="chenyan";
ser.changeName(p);
System.out.println(p.name);
}
}
测试二:
publicclass Client {
publicstaticvoid main(String[] args) {
PersonService ser=new PersonService();
Person p=new Person();
p.name="chenyan";
ser.changeNameTwo(p);
System.out.println(p.name);
}
}
运行后输出结果分别为:
alan
chenyan
即第一个方式改变了实例对象p的name值,而方式二却没有改变实例对象p的name值。
对于此结果我们可以从形参和实参为入口来进行分析解释。
二、形参与实参
形参是定义方法的时候,该方法所携带的参数,比如说现在有一个方法
public void printInfo(String info){
System。out。println(info);
}
此处info就是一个形参,它是String类型的。
实参是你在调用方法的时候,给这个方法传递的参数值,比如说有这么一个语句:
**。printInfo("hello");(此处**表示printInfo方法所在类的一个对象),这里的"hello"就是一个实参,实现方法调用的时候,系统会吧实参"hello"的值赋予形参info变量,即info就指向了"hello",调用这个方法后,就会在屏幕上打印输出hello。
搞清楚了什么是形参和实参,现在我们从内存的角度来分析一下调用PersonService两个相同方法不同实现结果不同的原因。
我们先来分析测试一,
Person p=new Person();
可以分为三个步骤,声名Person对象引用p;创建Person对象;返回对象地址并赋值给引用变量p。最后给对象p的name赋值为chenyan.
如下图所示:
当我们调用方法ser.changeName(p);时,在内存中表示为:
由于实参p和形参p指向的是同一个对象,所以在方法publicvoid changeName(Person p){
p.name="alan";
}
中,形参p改将name的值改为alan时,实际修改是的实参和形参共同指向的对象的name值,如图:
所以实参p.name的值输出的值为alan。
现在我们再来分析一下测试二,
当我们调用方法二时ser.changeNameTwo(p);在内存中对象存在的形式和方法一是一样,同样为:
直到方法二中的p=new Person();改变了实参和形参指向同一个对象的关系,如图:
最后变为:
再给形参指向的对象赋值:p.name="alan";
如图:
此时由于实参和形参指向的是各自的对象,所以形参p的name更改为alan,实参p的name并没有改变,扔用chenyan。由上图可知,引用和对象是分开存储在不同的内存中的,引用存在栈内存中,对象存在堆内存中。
三、内存中的堆(stack)与栈(heap)
Java把内存分成两种,一种叫做栈内存,一种叫做堆内存。
在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用。堆内存用于存放由new创建的对象和数组。
java中内存分配策略及堆和栈的比较
1内存分配策略
按照编译原理的观点,程序运行时的内存分配有三种策略,分别是静态的,栈式的,和堆式的。
静态存储分配是指在编译时就能确定每个数据目标在运行时刻的存储空间需求,因而在编译时就可以给他们分配固定的内存空间。这种分配策略要求程序代码中不允许有可变数据结构(比如可变数组)的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间需求。
栈式存储分配也可称为动态存储分配,是由一个类似于堆栈的运行栈来实现的。和静态存储分配相反,在栈式存储方案中,程序对数据区的需求在编译时是完全未知的,只有到运行的时候才能够知道,但是规定在运行中进入一个程序模块时,必须知道该程序模块所需的数据区大小才能够为其分配内存。和我们在数据结构所熟知的栈一样,栈式存储分配按照先进后出的原则进行分配。
静态存储分配要求在编译时能知道所有变量的存储要求,栈式存储分配要求在过程的入口处必须知道所有的存储要求,而堆式存储分配则专门负责在编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配,比如可变长度串和对象实例。堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放。
2 堆和栈的比较
从堆和栈的功能和作用来通俗的比较,堆主要用来存放对象的,栈主要是用来执行程序的。而这种不同又主要是由于堆和栈的特点决定的:
在编程中,例如C/C++中,所有的方法调用都是通过栈来进行的,所有的局部变量,形式参数都是从栈中分配内存空间的。实际上也不是什么分配,只是从栈顶向上用就行,就好像工厂中的传送带(conveyor belt)一样,Stack Pointer会自动指引你到放东西的位置,你所要做的只是把东西放下来就行。退出函数的时候,修改栈指针就可以把栈中的内容销毁。这样的模式速度最快,当然要用来运行程序了。需要注意的是,在分配的时候,比如为一个即将要调用的程序模块分配数据区时,应事先知道这个数据区的大小,也就说是虽然分配是在程序运行时进行的,但是分配的大小多少是确定的,不变的,而这个"大小多少"是在编译时确定的,不是在运行时。
堆是应用程序在运行的时候请求操作系统分配给自己内存,由于从操作系统管理的内存分配,所以在分配和销毁时都要占用时间,因此用堆的效率非常低。但是堆的优点在于,编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间,因此,用堆保存数据时会得到更大的灵活性。事实上,面向对象的多态性,堆内存分配是必不可少的,因为多态变量所需的存储空间只有在运行时创建了对象之后才能确定。在C++中,要求创建一个对象时,只需用 new命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存。当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!这也正是导致效率低的原因。
另外,栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义:
int a = 3;
int b=3;
编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b这个引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。
【上文提到了"引用+数值+内存地址"这三个名词,其中变量名就是引用,给变量赋的值就是数值,而所提到的内存是抽象的内容,让引用指向的不是数值,而是存取数值的那块内存地址】定义完a与b的值后,再令a = 4;那么,b不会等于4,还是等于3。在编译器内部,遇到时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。【定义变量,给变量赋值,然后在编译的过程中就可以将其保存在内存中了】。