引语

在使用链表时,笔者发现有时候在方法中通过头节点对链表内容做修改,此修改有时能反映到原链表中,有时候做出的修改作用域只在函数中,或者函数返回值中,并不会影响到原链表。为探究此现象的原因,笔者查询了相关资料并用代码做了一些测试,在这里将分析过程和结果分享给大家。
不想看分析测试过程可直接跳转结论:结论直达车

先给出结论,出现引语中的现象的原因是:没有正确理解Java中值传递和引用传递的概念。

概念

这里说的是在Java语言中参数传递的方式,对别的编程语言中的传递方式感兴趣的可以在网上自行查找。

  • 值传递:当我们将基本数据类型作为方法参数时,参数传递过程是值传递。
  • 引用传递:当我们将数组、对象等数据类型作为方法参数时,参数传递过程是引用传递。

特点

  • 值传递:值传递的参数是实参的一份拷贝或副本,因此在方法内对此参数做的修改与方法外的实参是隔离的。
  • 引用传递:引用传递的参数是实参对象的引用副本,通过引用对实参做的修改会在方法结束后仍然保留,也就是说与实参不隔离。

浅显一点地解释:值传递的形参是实参的一份拷贝,是个数,与实参相等,那么在拷贝上的修改就对实参不会有影响;而引用传递的形参是实参对象的一份引用拷贝,内容是实参对象的地址,因此在形参地址访问的对象上做修改就是在修改实参,那么在方法结束后此修改会保留,实参也被改变了。

拓展

通过上面特点部分的分析,可以看到引用传递实际上保存的是实参对象的地址拷贝,而这刚好也符合值传递的定义,因此也有 “Java中不存在引用传递,都是值传递” 这种说法,也是有道理的,当然在这种说法中就要区分这两种值传递了。但是无论是哪种说法,两种传递方式的特点是一致的,只要掌握各自的特点就能灵活使用了。
上面特点部分中提到引用传递不隔离,但要注意这个结论是有前提的,其前提是“通过引用修改实参内容”,如果只是通过引用访问实参对象,并不做修改,那么这些访问操作并不会对实参对象有影响。

理解

通过概念性的解释理解起来并不直观,我这里放几个测试的代码,结合代码更好理解。
基本数据类型作参数的值传递:

值传递
1
2
3
4
5
6
7
8
9
10
11
// 值传递测试
public static void changeInt(int value) { // 值传递,形参value是实参a的副本,与形参名字无关,即使形参起名为a也一样
value++;
System.out.println("Inside method: a = " + value); // 2,与外界实参隔离
}
public static void main(String[] args) {
int a = 1;
System.out.println("Outside method: a = " + a); // 1
changeInt(a); // 值传递
System.out.println("Outside method: a = " + a); // 1,值传递方法内修改后回到主函数实参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
// 定义一个Node类
private static class Node{
// 每个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; // 修改参数node的数据为0
return node.data;
}
public static void main(String[] args) {
// 引用类型测试
Node node = new Node(1);
System.out.println(node.data); // 1,直接访问实参node的数据
System.out.println(getNodeData(node)); // 1,对象作为参数是引用传递
System.out.println(node.data); // 1,在方法中只访问不修改引用指向的对象时,实参不变
System.out.println(changeNodeData(node)); // 0
System.out.println(node.data); // 0,在方法中修改了形参引用指向的对象内容,实参会变
}

从上面的例子中可以看到值传递会隔离,引用传递的隔离分情况,只访问则隔离,修改则不隔离。
再细分的话,这里说的修改也要分为两种情况,修改形参引用的指向隔离,修改形参引用指向的对象内容则不隔离。
具体一点,在链表这种数据结构中,遍历链表时存在改变形参的情况:

修改引用指向
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];
// 将原链表逆序赋值给nodeArray
int i = size-1;
for( ;head != null; head = head.next, i--) {
nodeArray[i] = head; // 此方式就是数组的每个元素都是一个引用,每个引用指向实参的每个Node节点
}
for(i = 1; i < size; i++) {
nodeArray[i-1].next = nodeArray[i]; // 将节点连接,修改节点的next值,这就是修改了引用指向的对象内容,因此不隔离
}
nodeArray[i-1].next = null; // nodeArray: 1->2->3
}

即使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; // 1
}

public void changeNode(Node head) {
if(head == null) {
return ;
}
Node[] nodeArray = new Node[size];
// 将原链表逆序赋值给nodeArray
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]; // 将节点连接,注意此处是修改节点的next值,因此不隔离
}
nodeArray[i-1].next = null; // nodeArray: 1->2->3
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; // 修改参数node的数据为0
return node.data;
}
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);

// // 引用类型测试
// Node node = new Node(1);
// System.out.println(node.data); // 1,直接访问实参node的数据
// System.out.println(getNodeData(node)); // 1,对象作为参数是引用传递
// System.out.println(node.data); // 1,在方法中只访问不修改引用指向的对象时,实参不变
// System.out.println(changeNodeData(node)); // 0
// System.out.println(node.data); // 0,在方法中修改了形参引用指向的对象内容,实参会变

// // 只访问不修改的引用传递
// ParameterPassing linkedList = new ParameterPassing();
// linkedList.add(1);
// linkedList.add(2);
// linkedList.add(3);
// System.out.println("--- before getTail ---");
// linkedList.display(linkedList.head);
// linkedList.getTail(linkedList.head);
// System.out.println("--- after getTail ---");
// linkedList.display(linkedList.head);
// // 修改引用所指对象的引用传递
// System.out.println("--- before changeNode ---");
// linkedList.display(linkedList.head);
// linkedList.changeNode(linkedList.head);
// System.out.println("outside linkedList:");
// linkedList.display(linkedList.head);
}
}