面试的时候碰到的了一个java基础问题,竟然给问蒙了,回来之后感觉针对这个问题总结一下
这边再将具体的值传递和引用传递,之前先普及一下基本知识
Java虚拟机中,数据类型可以分为两类:基本类型和引用类型。基本类型的变量保存原始值,即:他代表的值就是数值本身;而引用类型的变量保存引用值。“引用值”代表了某个对象的引用,而不是对象本身,对象本身存放在这个引用值所表示的地址的位置。
基本类型 | byte, short, int, long, char, float, double, Boolean |
---|---|
引用类型 | 类,接口和数组 |
有了数据类型,JVM对程序数据的管理就规范化了,不同的数据类型,它的存储形式和位置是不一样的,要想知道JVM是怎么存储各种类型的数据,就得先了解JVM的内存划分以及每部分的职能。
Java语言本身是不能操作内存的,它的一切都是交给JVM来管理和控制的,因此Java内存区域的划分也就是JVM的区域划分,在说JVM的内存划分之前,我们先来看一下Java程序的执行过程,如下图:
JVM结构图.png
上图可以看出:Java代码被编译器编译成字节码之后,JVM开辟一片内存空间(也叫运行时数据区),通过类加载器加到到运行时数据区来存储程序执行期间需要用到的数据和相关信息。
栈是线程私有的,也就是线程之间的栈是隔离的;当程序中某个线程开始执行一个方法时就会相应的创建一个栈帧并且入栈(位于栈顶),在方法结束后,栈帧出栈。
Java栈的模型以及栈帧结构图.png
栈帧
:是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。
每个栈帧中包括:
1、 局部变量表:用来存储方法中的局部变量(非静态变量、函数形参)。当变量为基本数据类型时,直接存储值,当变量为引用类型时,存储的是指向具体对象的引用。
2、 操作数栈:Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是指操作数栈。
3、指向运行时常量池的引用:存储程序执行时可能用到常量的引用。
4、方法返回地址:存储方法执行完成后的返回地址。
相关的异常
:
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。
局部变量在栈表现.png
image.png 图中per的地址指向的是堆内存中的一块区域
public class Person{
private int age;
private String name;
private int grade;
//篇幅较长,省略setter getter方法
static void run(){
System.out.println("run....");
};
}
//调用
Person per=new Person();
在方法中有 Print p1 = new Print(0, 0);
image.png
变量p1里存储着实际对象的地址,一般称这种变量为"引用",引用指向实际对象,我们称实际对象为该引用的值;赋值操作符=实际上做的就是将引用指向值的地址的工作,如果我们有p1 = new Print(3,3); 的话,情形就是这样:
image.png
要注意到,在堆中的对象Print(0,0) 并没有发生改变,改变的只有引用p1 指向的地址。
在方法被调用时,实参通过形参把它的内容副本传入方法内部,此时形参接收到的内容是实参值的一个拷贝,因此在方法内对形参的任何操作,都仅仅是对这个副本的操作,不影响原始值的内容。
public static void valueCrossTest(int age,float weight){
System.out.println("传入的age:"+age);
System.out.println("传入的weight:"+weight);
age=33;
weight=89.5f;
System.out.println("方法内重新赋值后的age:"+age);
System.out.println("方法内重新赋值后的weight:"+weight);
}
public static void main(String[] args) {
int a=25;
float w=77.5f;
valueCrossTest(a,w);
System.out.println("方法执行后的age:"+a);
System.out.println("方法执行后的weight:"+w);
}
输出结果: 传入的age:25 传入的weight:77.5 方法内重新赋值后的age:33 方法内重新赋值后的weight:89.5 方法执行后的age:25 方法执行后的weight:77.5 从上面输出结果可以看出:a和w作为实参传入valueCrossTest之后,无论在方法内做了什么操作,最终a和w都没变化。
在方法调用时,传入方法内部的是实参引用的拷贝,因此对形参的任何操作都不会影响到实参。
public class Print {
private int x;
private int y;
public Print(int x, int y) {
this.x = x;
this.y = y;
}
public void setLocation(int x, int y) {
this.x = x;
this.y = y;
}
private static void modifyPrint(Print p1, Print p2) {
Print tmpPrint = p1;
p1 = p2;
p2 = tmpPrint;
p1.setLocation(5, 5);
p2 = new Print(5, 5);
}
public static void main(String[] args) {
Print p1 = new Print(0, 0);
Print p2 = new Print(0, 0);
modifyPrint(p1, p2);
System.out.println("[" + p1.x + "," + p1.y + "],[" + p2.x + "," + p2.y + "]");
}
}
运行结果: [0,0],[5,5]
这个结果是不是很意外,不是想象中的[5,5],[5,5]
在modifyPrint()方法中下面的代码
Point tmpPoint = p1;
p1 = p2;
p2 = tmpPoint;
可以理解在方法内部有形参p1'=实参p1,形参p2'=实参p2,这样我们在方法里操作的p1'和p2'实际只是个临时变量,它的生命周期仅限于方法里。
image.png
然后调用了p1'.setLocation(5, 5); 如果实例对象本身提供了改变自身的方法,那么在形参调用该方法后也会改变实参的,因为它们都指向了同一个实例,所以这时实参p2 也变为Point[5.5] 。 代码走到下一行p2' = new Point(5, 5); 基于整篇文章的论述,这一行我们可以直接跳过不管了,因为形参的重赋值操作不会影响到实参。最后的堆栈信息如下:
image.png
所以最终答案显而易见:[0,0],[5,5]
public static void main(String[] args) {
List<String> colorList = new ArrayList<>();
colorList.add("BLUE");
colorList.add("RED");
colorList.add("GRAY");
System.out.println(colorList);
removeFirst(colorList);
System.out.println(colorList);
}
private static void removeFirst(List colorList) {
if (!colorList.isEmpty()) colorList.remove(0);
}
// 输出的结果
// [BLUE, RED, GRAY]
// [RED, GRAY]
上面代码中,调用removeFirst方法,传进入的参数List,方法内部调用的是实例对象本身提供了改变自身的方法remove(int index),那么在形参调用该方法后也会改变实参的,因为它们都指向了同一个实例List, 所以colorlist的内容变为[RED, GRAY], 该在栈和堆的表现,如下:
image.png
如果调整上面的代码: 修改removeFirst的方法内容改为下面的代码:
private static void removeFirst(List colorList) {
//if (!colorList.isEmpty()) colorList.remove(0);
if (!colorList.isEmpty())
colorList = colorList.subList(1, colorList.size());
}
//再次运行的结果为:
//[BLUE, RED, GRAY]
//[BLUE, RED, GRAY]
colorList.subList(1, colorList.size()); 虽然也是实例对象本身提供的方法,但是该方法是返回的是一个new的新对象。相当于colorList = new ArrayList(); 相当于在方法内创建一个新的对象,形参colorList' 该对象声明周期,只在removeFirst有效,当方法结束,形参colorList' 被回收,原来的实参还是不变。
image.png
ArrayList类中subList方法的源码:SubList也是实现了List接口
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
因此:值传递(Call by value)和引用传递(Call by reference),描述的是函数调用时参数的求值策略(Evaluation strategy),是对调用函数时,求值和取值方式的描述,而非传递的内容。
https://my.oschina.net/u/1455908/blog/392009 https://www.cnblogs.com/lfxiao/p/10599546.html https://blog.csdn.net/bntx2jsqfehy7/article/details/83508006 https://blog.csdn.net/hongzhen91/article/details/90666066 https://www.cnblogs.com/zwbg/p/6194470.html
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有