引语
在使用链表时,笔者发现有时候在方法中通过头节点对链表内容做修改,此修改有时能反映到原链表中,有时候做出的修改作用域只在函数中,或者函数返回值中,并不会影响到原链表。为探究此现象的原因,笔者查询了相关资料并用代码做了一些测试,在这里将分析过程和结果分享给大家。
不想看分析测试过程可直接跳转结论:结论直达车
先给出结论,出现引语中的现象的原因是:没有正确理解Java中值传递和引用传递的概念。
概念
这里说的是在Java语言中参数传递的方式,对别的编程语言中的传递方式感兴趣的可以在网上自行查找。
- 值传递:当我们将基本数据类型作为方法参数时,参数传递过程是值传递。
- 引用传递:当我们将数组、对象等数据类型作为方法参数时,参数传递过程是引用传递。
特点
- 值传递:值传递的参数是实参的一份拷贝或副本,因此在方法内对此参数做的修改与方法外的实参是隔离的。
- 引用传递:引用传递的参数是实参对象的引用副本,通过引用对实参做的修改会在方法结束后仍然保留,也就是说与实参不隔离。
浅显一点地解释:值传递的形参是实参的一份拷贝,是个数,与实参相等,那么在拷贝上的修改就对实参不会有影响;而引用传递的形参是实参对象的一份引用拷贝,内容是实参对象的地址,因此在形参地址访问的对象上做修改就是在修改实参,那么在方法结束后此修改会保留,实参也被改变了。
拓展
通过上面特点部分的分析,可以看到引用传递实际上保存的是实参对象的地址拷贝,而这刚好也符合值传递的定义,因此也有 “Java中不存在引用传递,都是值传递” 这种说法,也是有道理的,当然在这种说法中就要区分这两种值传递了。但是无论是哪种说法,两种传递方式的特点是一致的,只要掌握各自的特点就能灵活使用了。
上面特点部分中提到引用传递不隔离,但要注意这个结论是有前提的,其前提是“通过引用修改实参内容”,如果只是通过引用访问实参对象,并不做修改,那么这些访问操作并不会对实参对象有影响。
理解
通过概念性的解释理解起来并不直观,我这里放几个测试的代码,结合代码更好理解。
基本数据类型作参数的值传递:
值传递1 2 3 4 5 6 7 8 9 10 11
| public static void changeInt(int value) { value++; System.out.println("Inside method: a = " + value); } public static void main(String[] args) { int a = 1; System.out.println("Outside method: a = " + a); changeInt(a); System.out.println("Outside method: a = " + a); }
|
对象作为参数的引用传递:
引用传递1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| private static class Node{ private Integer data; public Node(Integer value) { data = value; } }
private static Integer getNodeData(Node node) { return node == null? null : node.data; }
private static Integer changeNodeData(Node node) { node.data = node == null ? null : 0; return node.data; } public static void main(String[] args) { Node node = new Node(1); System.out.println(node.data); System.out.println(getNodeData(node)); System.out.println(node.data); System.out.println(changeNodeData(node)); System.out.println(node.data); }
|
从上面的例子中可以看到值传递会隔离,引用传递的隔离分情况,只访问则隔离,修改则不隔离。
再细分的话,这里说的修改也要分为两种情况,修改形参引用的指向隔离,修改形参引用指向的对象内容则不隔离。
具体一点,在链表这种数据结构中,遍历链表时存在改变形参的情况:
修改引用指向1 2 3 4 5 6 7
| public Node getTail(Node head) { if(head == null) {return head;} while(head.next != null) { head = head.next; } return head; }
|
形参head
被改变,但是改变的仅是形参head
的指向为head.next
的地址,并没有修改原来head
指向的对象,因此此修改作用域只在方法内部,不会影响原来的实参对象。
理解这一点后再看下面的代码:
修改引用指向对象内容1 2 3 4 5 6 7 8 9 10 11 12 13
| public void changeNode(Node head) { if(head == null) {return;} Node[] nodeArray = new Node[size]; int i = size-1; for( ;head != null; head = head.next, i--) { nodeArray[i] = head; } for(i = 1; i < size; i++) { nodeArray[i-1].next = nodeArray[i]; } nodeArray[i-1].next = null; }
|
即使nodeArray[]
是在方法中新创建的变量,因为给予了其指向实参对象的引用,相当于给了他访问修改实参内容的钥匙,那么通过nodeArray
引用修改实参内容的操作也就会被保留下来,对于上述代码来说,最终以nodeArray[0]
为头节点的链表成功实现了原实参链表的逆序,但是在方法结束后实参的顺序已经被逆序操作给打乱了。如果想要让实参对象不受影响,那就应该在方法前对实参对象使用深拷贝,在拷贝出来的副本上做修改。关于浅拷贝和深拷贝可以看 “浅拷贝和深拷贝” 这篇文章。
在理解了值传递和引用传递后就可以对编程语言中的=
符号有另一种更方便的理解方式,例如int a = b;
中=
表达了将基本数据类型b
的拷贝给a
,那么a
的修改就不会影响到b
;相应的Class1 newNode = node;
表达了将Class1
类型的newNode
对象的地址给node
,因此修改node
引用地址指向的对象会对newNode
造成影响。用值传递和引用传递来理解在我看来是一种不错的方式。
总结
- 值传递:基本数据类型作为形参,保存实参的副本,对形参的修改与实参隔离,作用域在方法内。
- 引用传递:数组、对象数据类型作为形参,保存实参的引用(地址)副本,通过此引用修改指向的对象内容会影响实参;通过此形参引用只访问指向的对象内容或者改变形参引用的指向时,与实参隔离,作用域在方法内。
这种隔离性的设计使得方法无法直接修改调用者的变量,它只能修改传递进来的副本,灵活使用值传递和引用传递,有助于确保数据的安全性和一致性。
附录
贴上完整测试代码:
测试代码1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
| public class ParameterPassing { private Node head; private int size; public ParameterPassing() { size = 0; }
public static void changeInt(int value) { value++; System.out.println("Inside method: a = " + value); } private static class Node { private Integer data; private Node next; public Node(Integer value) { data = value; } } public void add(Integer value) { Node newNode = new Node(value); newNode.next = size == 0 ? null : head; head = newNode; size++; } public Node getTail(Node head) { if(head == null) { return head; } while(head.next != null) { head = head.next; } return head; } public void changeNode(Node head) { if(head == null) { return ; } Node[] nodeArray = new Node[size]; int i = size-1; for( ;head != null; head = head.next, i--) { nodeArray[i] = head; } for(i = 1; i < size; i++) { nodeArray[i-1].next = nodeArray[i]; } nodeArray[i-1].next = null; System.out.println("--- after changeNode ---"); System.out.println("nodeArray: "); display(nodeArray[0]); } public void display(Node head) { if(head == null) { return ; } System.out.print("["); for( ; head.next != null; head = head.next) { System.out.print(head.data + ","); } System.out.println(head.data + "]"); } private static Integer getNodeData(Node node) { return node == null? null : node.data; } private static Integer changeNodeData(Node node) { node.data = node == null ? null : 0; return node.data; } public static void main(String[] args) {
} }
|