值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
有关java的参数传递,是一个简单而重要的知识点,但是从表现形式上有一定的迷惑性,如果没弄明白很有可能导致出现bug。
简单一搜就可以知道java只有值传递,我们尝试编写代码发现确实有很经典的值传递的现象,
//代码1
public class DemoTest {
int a, b;
public static void main(String[] args) {
DemoTest d = new DemoTest();
d.a = 3;
d.b = 4;
d.operation(d.a, d.b);
System.out.println("a:" + d.a + " b:" + d.b);
}
public void operation(int num1,int num2) {
num1 = num1 + num2;
num2 = num1 - num2;
}
}
可以看到形参改变对原有实参毫无影响,结合jvm运行区域知识可知,堆中变量拷贝到栈帧中,形参改变知识栈帧中副本的改变,函数结束栈帧释放,而堆中无影响,这是经典的值传递。
不过在java代码编写的时候,我们经常会发现如下现象,
//代码2
public class DemoTest {
int a, b;
public static void main(String[] args) {
DemoTest d = new DemoTest();
d.a = 3;
d.b = 4;
d.operation(d);
System.out.println("a:" + d.a + " b:" + d.b);
}
public void operation(DemoTest d) {
d.a = d.a + d.b;
d.b = d.a - d.b;
}
}
这里你会发现,传入对象变量d,没有return,对象d已经随着函数的运行发生了一定的变化,似乎很像引用传递。
因为这种现象,很多人会说、或者说会把规律记成java的基础类型传递是值传递,对象作为参数传递是引用传递。
我之前也是这么认为的,直到我做了一道力扣题203. 移除链表元素
我开始实现的代码是这样,
//代码3
class Solution {
public ListNode removeElements(ListNode head, int val) {
ListNode cur = new ListNode(-1); //指向当前待判断结点
cur.next = head;
while (cur.next != null) {
if (cur.next.val == val) deleteNode(cur, head);//cur是检测结点的上一个结点
else cur = cur.next;
}
return head;
}
public void deleteNode(ListNode cur, ListNode head) {
if (cur.next == head) {
cur.next = head.next;
head = head.next;
} else {
cur.next = cur.next.next;
}
}
}
然而遇到case:[7,7,7,7],7 的时候,却输出了 [7,7,7,7] ,梳理逻辑可以发现通过deleteNode函数,原本head最终应该等于null,这个结果意味着deleteNode函数没有完成对head的修改。我将函数中的代码复制到函数调用处对函数进行替换,发现成功通过所有case,这说明逻辑没有问题,问题出在参数传递上。
如果java真的在传递对象变量的时候是引用传递,这段代码应该是可行的,然而函数中对形参的改变并没有直接影响到实参,这是值传递是典型特征。
那么我们便进行分析,为什么值传递,代码3中却影响到了实参的值。
这里我们要提到一种非常类似的情景,数组的传递,
public class DemoTest {
public static void main(String[] args) {
DemoTest d = new DemoTest();
int[] nums = {1,2,3};
d.operation(nums);
System.out.println("a:" + nums[0] + " b:" + nums[1]);
}
public void operation(int[] nums) {
nums[0] = nums[0] + nums[1];
nums[1] = nums[0] - nums[1];
}
}
可以发现,这里形式上与对象传递十分相似,而且同样成功的影响到了实参。
作为对比,我附上C++的代码和结果,
void operation(int nums[]) {
nums[0] = nums[0] + nums[1];
nums[1] = nums[0] - nums[1];
}
int main() {
int nums[3] = {1,2,3};
operation(nums);
cout<<"a:"<<nums[0]<<" b:"<<nums[1];
}
一样的逻辑和类型,得到了和java一样的结果。我们知道,C++数组之所以可以影响值,是因为数组的传参其实是传的头指针,或者说是首地址,函数中对数组的修改其实是根据首地址进行偏移在内存中找到对应的值进行修改,而不是对首地址的修改,因此可以修改实参。
java中没有指针概念,但是无处不蕴含着“指针”,java中也是类似的地址操作原理。
这时有人可能会问,传地址那不就是引用传递吗?并不是这样的,如果传递的是复杂数据结构都会传首地址,这里的传地址,是把地址作为数值传值,拷贝到栈帧副本,如果对传进来的形参(实参的地址值)进行直接修改,那么只是对栈帧副本进行修改,无法影响到内存中原本的实参变量。而如果是引用传递,修改这个形参时,会从传进的地址找到内存中对应实参的位置进行修改。
虽然对形参这个副本修改无法影响实参,但是形参作为副本,地址是“真实的”,修改形参指向的内容是可以直接影响到原本的变量内容的(类似指针),这也解释代码2中为什么可以对传进来的d.a和d.b进行改变,因为没有改变传进来的形参d(对象d的地址),而是改变它指向的内容。一句话概括就是:改变我不行,改变我的指向可以。
基于以上理解对代码进行修改如下,
//代码5
class Solution {
public ListNode removeElements(ListNode head, int val) {
ListNode cur = new ListNode(-1); //指向当前待判断结点
ListNode pre = new ListNode(-1); //指向当前头结点
cur.next = head;
pre.next = head;
while (cur.next != null) {
if (cur.next.val == val) deleteNode(cur, pre);//cur是检测结点的上一个结点
else cur = cur.next;
}
return pre.next;
}
public void deleteNode(ListNode cur, ListNode pre) {
if (cur.next == pre.next) {
cur.next = pre.next.next;
pre.next = pre.next.next;
} else {
cur.next = cur.next.next;
}
}
}
既然直接修改head不行,那么我便找一个结点pre来指向head,传入pre虽然无法修改pre,但是我可以修改pre指向的内容,从而实现无返回值得修改头结点。
总结:java的参数传递只有值传递,仅修改形参无法影响实参,例如对于传入参数a使得a=b是改变不了实参a的,但是可以修改形参指向的内容,例如修改a.val、a.next,来完成对其指向的修改。