前言
明白Java中的传递机制,有助于帮助我们更好的理解Java在内存中的地址指向。这篇文章我会通过尽可能简单的讲解,让大家更好的理解。
1. 传递类型
首先,我们需要知道,传递类型指的是调用函数时传入的参数传递的方式。传递类型包括两种:值传递和引用传递
值传递:我们传入的实参拷贝一份传给函数的形参,在函数中操作形参不改变实参的值
引用传递:将实参的引用地址传递给方法
对于C++这类的语言来说,既存在值传递又存在引用传递
值传递实现方式:int func(int a, int b);
而引用传递的实现通过指针或引用来实现,例如:int func(int *a, int &b);
而对于Java来说,只存在值传递,不存在引用传递
2. Java中的值传递机制
为了让大家更好的理解,接下来会通过大量的画图来进行展示。
这里我从网上找了一张JVM内存结构图,可以看到Java程序在内存中是分为各个区域的。这里要讲的Java值传递机制为了帮助大家理解,主要针对栈区和堆区这两块最常见的区域进行讲解,而忽略里面的细节部分。
首先,我们需要知道,Java中的变量类型分为两种:基本类型和引用类型。接下来我通过几个例子来帮助大家更好的理解Java值传递机制。
第一个例子
public class Main01 {
public static void main(String[] args) {
int a = 10, b = 20;
exchange(a, b);
System.out.println("a = " + a + ", b = " + b);
}
public static void exchange(int a, int b) {
int tmp = a;
a = b;
b = tmp;
}
}
首先看一个基本类型的例子,看看这段代码,大家猜一下结果是什么?
a = 10, b = 20
应该不难理解, 因为Java采用值传递机制,传入的是实参的一份拷贝,改变函数中形参的值不会影响原来的值,接下来通过内存来进行理解。
Java中的栈区由一个个的栈帧构成,一个栈帧对应一个方法,而方法的开始和结束对应入栈和出栈过程。程序执行时首先将由主方法main()构成的栈帧压栈,执行主方法过程中遇到方法的执行则对应压栈,遇到方法的结束对应出栈,当主方法执行结束时则程序结束。
上面这段代码通过画图就是这个样子,先开辟主方法main()对应的栈帧。然后压入exchange()方法对应的栈帧,exchange()方法结束后出栈,最后主方法main()结束意味着程序的结束。可以看到,在exchange()方法中改变形参a’和b’并没有影响主方法main()中实参a、b的值。
我们接着往下看,再分析一段关于引用类型的代码
第二个例子
public class Main01 {
public static void main(String[] args) {
Student s1 = new Student("张三");
Student s2 = new Student("李四");
exchange(s1, s2);
System.out.println(s1);
System.out.println(s2);
}
public static void exchange(Student s1, Student s2) {
Student tmp = s1;
s1 = s2;
s2 = tmp;
}
}
class Student {
public Student(String name) {
this.name = name;
}
public String name;
@Override
public String toString() {
return "Student [name=" + name + "]";
}
}
这次的结果又是什么?
Student [name=张三]
Student [name=李四]
可以看到,s1和s2的name值并没有发生改变
这是为什么呢?引用类型传入的是地址,在exchange()方法中交换其指向,为什么主方法main()中s1和s2的指向为什么没有相互交换呢?
看完这个例子,如果你针对这个疑问已经有了答案的话,那太好了,你对Java值传递一定有了一定的理解?如果还存在疑惑,那就跟着我继续往下分析吧!
分析时,我们需要带着一个问题,值传递对于基本类型和引用类型有什么区别吗?
我们同样通过内存图来进行分析
你只要记住一点,Java值传递传递的实参的一份拷贝,在这个例子中,exchange()函数中携带的同样是s1和s2的一份拷贝,只不过这一次,两者指向的是堆区的同一块内存空间。
在执行exchange()函数时,交换的是形参s1’和s2’的内存地址指向,对于实参s1和s2来说,其指向还是没有发生改变。
看完前面两个例子,你似乎会觉得无论是基本类型还是引用类型都是值传递,传递是一份拷贝,都无法真正改变原来的值?
但是,真的是这样吗?
接下来我们看最后一个关于引用类型的例子,或许它能帮助你真正理解Java的值传递
第三个例子
public class Main01 {
public static void main(String[] args) {
Student s1 = new Student();
Student s2 = new Student();
s1.name = "张三";
s2.name = "李四";
exchange(s1, s2);
System.out.println(s1);
System.out.println(s2);
}
public static void exchange(Student s1, Student s2) {
Student tmp = new Student();
tmp.name = "张三";
s1.name = s2.name;
s2.name = tmp.name;
}
}
class Student {
public String name;
@Override
public String toString() {
return "Student [name=" + name + "]";
}
}
仔细分析一下这段代码,你的结果是什么?
是否认为s1.name还是"张三",s2.name还是"李四"?
我们看一下结果
Student [name=李四]
Student [name=张三]
诶?怎么跟你想的不一样?不是说引用类型传递,即使传的是地址的拷贝,改变形参地址的指向无法改变实参的指向吗?
对!的确是这样的,值传递,改变形参的地址指向确实影响不了实参的地址指向,但是第三个例子中改变的是其指向地址空间的值
哈哈,是不是被我说懵了
让我们接着画内存图进行分析
执行exchange()前:
此时只有主方法main()对应的栈帧
紧接着执行exchange()方法,exchange()对应的栈帧入栈
首先,创建了一个新的Student对象tmp,令其name = “张三”
然后,交换了形参s1’和s2’指向堆区内存空间的对象的name值
于是就变成了下面这个样子
看到这里,你理解了吗?注意第三个例子和第二个例子的区别。
第二个例子是改变了形参s1’和s2’的指向,并没有改变实参s1和s2的指向,所以实参的指向没变
第三个例子确实也没有改变实参s1和s2的指向,其在堆区指向的内存空间仍没有发生改变,但是我们是通过改变其对应堆区对象的name值,所以输出就发生了改变。本质上改变形参的值还是没有改变形参的指向,这也就是值传递。
通过以上三个例子,你是否深刻了解Java值传递机制?
ps. 本人大二狗一条,能力有限,大佬轻喷,如果有哪块没看懂欢迎评论区留言交流!