Java 对象作为参数传递的相关原理研究

概述

Java编程中,当对象作为参数传递给方法时候,是按引用传递的,但是有的人会说这实质上
是按值传递的。其实两者说的都不错,只是理解的方式不一样罢了,二者的原理其实是一样的。

说明

下面通过一个例子来详细说明Java对象作为方法参数的时候会出现的情况:

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
import java.util.ArrayList;
import java.util.List;

/**
* 测试java的引用传值
*
* Created by Xuyh at 2017/01/05 上午 10:27.
*/
public class SetParamTest {
public static void main(String... args) {
// 集合对象,列表[a, b, c]
List<String> testParam = new ArrayList<String>();
testParam.add("a");
testParam.add("b");
testParam.add("c");
System.out.println("Old: " + testParam.toString() + "\r\n");

// 引用(地址)传值,并直接修改testParam指向对象的值
change(testParam);
System.out.println("Change1: " + testParam.toString() + "\r\n");

testParam.add("a");

// 引用(地址)传值,修改指向对象之后再将地址返回
testParam = change2(testParam);
System.out.println("Change2: " + testParam.toString() + "\r\n");

// 集合对象变为[b, c, d, e]
testParam.add("d");
testParam.add("e");

// 引用(地址)传值,方法内部new新集合对象并将地址赋给方法局部参数,
// 并不改变testParam的值,testParam仍然指向原地址
sort(testParam);
System.out.println("sort1: " + testParam.toString() + "\r\n");
// 引用(地址)传值,方法内部new新集合对象并将地址赋给方法局部参数,
// 并将新地址返回,再赋值给testParam,因此testParam指向的对象发生改变
testParam = sort2(testParam);
System.out.println("sort2: " + testParam.toString() + "\r\n");
}

public static void change(List<String> param) {
if (param.contains("a"))
param.remove("a");
}

public static List<String> change2(List<String> param) {
if (param.contains("a"))
param.remove("a");
return param;
}

public static void sort(List<String> param) {
List<String> newParam = new ArrayList<String>();
for (int i = param.size() - 1; i >= 0; i--) {
newParam.add(param.get(i));
}
param = newParam;
}

public static List<String> sort2(List<String> param) {
List<String> newParam = new ArrayList<String>();
for (int i = param.size() - 1; i >= 0; i--) {
newParam.add(param.get(i));
}
return newParam;
}
}

上面的例子简单的说就是一个列表,[a, b, c],首先通过两个方法把其中的字符”a”去除,
然后通过两个方法对列表[b, c, d, e]进行倒序排序。同时在操作的过程中不断将main方法中的
testParam输出出来。

接着请看程序运行的结果:

1
2
3
4
5
6
7
8
9
Old: [a, b, c]

Change1: [b, c]

Change2: [b, c]

sort1: [b, c, d, e]

sort2: [e, d, c, b]

由结果可以看到,两个change方法都将原来的testParam改变了,
而sort方法只有一个改变了testParam的值,这个有趣的现象接下来要好好说说了。
下面从四个比较典型的方法来介绍不同情况下对象作为值传入方法时候内存中的情况,
以便我们更好理解java的方法传值原理。

change方法

1
2
3
4
public static void change(List<String> param) {
if (param.contains("a"))
param.remove("a");
}

testParam作为参数传递给param时候,内存内是这样的:

testParam的地址值复制给了change方法局部变量param,于是param指向了与testParam相同的一个对象。
注意这时候param的引用(地址)值是testParam的一份拷贝。

Change方法中将列表的”a”元素去除时候的情况是这样的:

此时testParam与param指向的对象(同一个)发生了改变,所以方法执行完毕之后输出testParam是发生改变的。

change2方法

1
2
3
4
5
public static List<String> change2(List<String> param) {
if (param.contains("a"))
param.remove("a");
return param;
}

与change方法类似,当对象在方法中发生改变时候,内存中的情况是这样的:

1
testParam = change2(testParam);

方法执行到最后,param会返回,也就是param的引用(地址)值会返回,
然后又拷贝给testParam(其实是多此一举,二者的地址值完全相同)。
所以方法执行完成之后输出的testParam是发生改变的。

sort方法

1
2
3
4
5
6
7
public static void sort(List<String> param) {
List<String> newParam = new ArrayList<String>();
for (int i = param.size() - 1; i >= 0; i--) {
newParam.add(param.get(i));
}
param = newParam;
}

调用方法时候内存中是这样的

接着,方法中new了一个新的对象,并在处理排序之后将新对象的引用(地址)赋值给param,这时候内存中的情况是这样的:

可以看到,testParam与param指向的已经不是同一个地址了,并且testParam指向的对象仍然没有发生改变。因此方法执行完毕之后输出的结果看来,sort方法是没有用的。

sort2方法

最后我们来看一下sort2方法

1
2
3
4
5
6
7
public static List<String> sort2(List<String> param) {
List<String> newParam = new ArrayList<String>();
for (int i = param.size() - 1; i >= 0; i--) {
newParam.add(param.get(i));
}
return newParam;
}

同sort方法类似,执行sort2方法前内存中是这样的:

新对象排序完成后是这样的:

这时候testParam与param都是指向原来的对象
方法的最后把newParam返回,即将newParam的引用(地址)值返回,在main方法中将其赋值给testParam

1
testParam = sort2(testParam);

然后内存中就是这样的了:

因此经过返回后的sort2方法是有作用的。

小结:

通过以上的分析可以得出,Java中对象作为方法参数传递的是对象引用(地址)值的拷贝,
通俗的讲,就是拷贝了一把钥匙,main方法中的钥匙跟被调用方法中参数的钥匙对应能
打开的房间是相同的,但是是不同的钥匙。如果在被调用方法中,参数钥匙被修改了
(new了一个新对象并赋值给参数钥匙),不会影响main方法钥匙指向的房间。这点是值得注意的。

因此如果想要在被调用方法中不修改参数指向的对象,可以通过new一个新的对象来实现,
但是如果想要new一个新的对象并使main方法该对象变成这个新的对象,就要将其作为
返回值返回并赋值给旧对象变量了。