为什么说 Java 中只有值传递

Java 的求值策略

前面我们介绍过了传值调用、传引用调用以及传值调用的特例传共享对象调用,那么,Java 中是采用的哪种求值策略呢?

很多人说 Java 中的基本数据类型是值传递的,这个基本没有什么可以讨论的,普遍都是这样认为的。

但是,有很多人却误认为 Java 中的对象传递是引用传递。之所以会有这个误区,主要是因为 Java 中的变量和对象之间是有引用关系的。Java 语言中是通过对象的引用来操纵对象的。所以,很多人会认为对象的传递是引用的传递。

而且很多人还可以举出以下的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
Test pt = new Test();

User hollis = new User();
hollis.setName("Hollis");
hollis.setGender("Male");
pt.pass(hollis);
System.out.println("print in main , user is " + hollis);
}

public void pass(User user) {
user.setName("hollischuang");
System.out.println("print in pass , user is " + user);
}

输出结果:

1
2
print in pass , user is User{name='hollischuang', gender='Male'}
print in main , user is User{name='hollischuang', gender='Male'}

可以看到,对象类型在被传递到 pass 方法后,在方法内改变了其内容,最终调用方 main 方法中的对象也变了。

所以,很多人说,这和引用传递的现象是一样的,就是在方法内改变参数的值,会影响到调用方。

但是,其实这是走进了一个误区。

Java 中的对象传递

很多人通过代码示例的现象说明 Java 对象是引用传递,那么我们就从现象入手,先来反驳下这个观点。

我们前面说过,无论是值传递,还是引用传递,只不过是求值策略的一种,那求值策略还有很多,比如前面提到的共享对象传递的现象和引用传递也是一样的。那凭什么就说 Java 中的参数传递就一定是引用传递而不是共享对象传递呢?

那么,Java 中的对象传递,到底是哪种形式呢?其实,还真的就是共享对象传递。

其实在 《The Java™ Tutorials》中,是有关于这部分内容的说明的。首先是关于基本类型描述如下:

Primitive arguments, such as an int or a double, are passed into methods by value. This means that any changes to the values of the parameters exist only within the scope of the method. When the method returns, the parameters are gone and any changes to them are lost.

即,原始参数通过值传递给方法。这意味着对参数值的任何更改都只存在于方法的范围内。当方法返回时,参数将消失,对它们的任何更改都将丢失。

关于对象传递的描述如下:

Reference data type parameters, such as objects, are also passed into methods by value. This means that when the method returns, the passed-in reference still references the same object as before. However, the values of the object’s fields can be changed in the method, if they have the proper access level.

也就是说,引用数据类型参数 (如对象) 也按值传递给方法。这意味着,当方法返回时,传入的引用仍然引用与以前相同的对象。但是,如果对象字段具有适当的访问级别,则可以在方法中更改这些字段的值。

这一点官方文档已经很明确的指出了,Java 就是值传递,只不过是把对象的引用当做值传递给方法。你细品,这不就是共享对象传递么?

其实 Java 中使用的求值策略就是传共享对象调用,也就是说,Java 会将对象的地址的拷贝传递给被调函数的形式参数。只不过” 传共享对象调用” 这个词并不常用,所以 Java 社区的人通常说”Java 是传值调用”,这么说也没错,因为传共享对象调用其实是传值调用的一个特例。

值传递和共享对象传递的现象冲突吗?

看到这里很多人可能会有一个疑问,既然共享对象传递是值传递的一个特例,那么为什么他们的现象是完全不同的呢?

难道值传递过程中,如果在被调方法中改变了值,也有可能会对调用者有影响吗?那到底什么时候会影响什么时候不会影响呢?

其实是不冲突的,之所以会有这种疑惑,是因为大家对于到底是什么是” 改变值” 有误解。

我们先回到上面的例子中来,看一下调用过程中实际上发生了什么?

pass2

在参数传递的过程中,实际参数的地址 0X1213456 被拷贝给了形参。这个过程其实就是值传递,只不过传递的值得内容是对象的应用。

那为什么我们改了 user 中的属性的值,却对原来的 user 产生了影响呢?

其实,这个过程就好像是:你复制了一把你家里的钥匙给到你的朋友,他拿到钥匙以后,并没有在这把钥匙上做任何改动,而是通过钥匙打开了你家里的房门,进到屋里,把你家的电视给砸了。

这个过程,对你手里的钥匙来说,是没有影响的,但是你的钥匙对应的房子里面的内容却是被人改动了。

也就是说,Java 对象的传递,是通过复制的方式把引用关系传递了,如果我们没有改引用关系,而是找到引用的地址,把里面的内容改了,是会对调用方有影响的,因为大家指向的是同一个共享对象。

那么,如果我们改动一下 pass 方法的内容:

1
2
3
4
5
public void pass(User user) {
user = new User();
user.setName("hollischuang");
System.out.println("print in pass , user is " + user);
}

上面的代码中,我们在 pass 方法中,重新 new 了一个 user 对象,并改变了他的值,输出结果如下:

1
2
print in pass , user is User{name='hollischuang', gender='Male'}
print in main , user is User{name='Hollis', gender='Male'}

再看一下整个过程中发生了什么:

pass1

这个过程,就好像你复制了一把钥匙给到你的朋友,你的朋友拿到你给他的钥匙之后,找个锁匠把他修改了一下,他手里的那把钥匙变成了开他家锁的钥匙。这时候,他打开自己家,就算是把房子点了,对你手里的钥匙,和你家的房子来说都是没有任何影响的。

所以,Java 中的对象传递,如果是修改引用,是不会对原来的对象有任何影响的,但是如果直接修改共享对象的属性的值,是会对原来的对象有影响的。

总结

我们知道,编程语言中需要进行方法间的参数传递,这个传递的策略叫做求值策略。

在程序设计中,求值策略有很多种,比较常见的就是值传递和引用传递。还有一种值传递的特例 —— 共享对象传递。

值传递和引用传递最大的区别是传递的过程中有没有复制出一个副本来,如果是传递副本,那就是值传递,否则就是引用传递。

在 Java 中,其实是通过值传递实现的参数传递,只不过对于 Java 对象的传递,传递的内容是对象的引用。

我们可以总结说,Java 中的求值策略是共享对象传递,这是完全正确的。

但是,为了让大家都能理解你说的,我们说 Java 中只有值传递,只不过传递的内容是对象的引用。这也是没毛病的。

但是,绝对不能认为 Java 中有引用传递。

OK,以上就是本文的全部内容,不知道本文是否帮助你解开了你心中一直以来的疑惑。欢迎留言说一下你的想法。

参考资料

The Java™ Tutorials

Evaluation strategy

Is Java “pass-by-reference” or “pass-by-value”?

Passing by Value vs. by Reference Visual Explanation

应该怎么做?

那就要使用 set()get() 方法了。

先看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ParamPass1 {
public static void main(String[] args) {
ParamPass1 p = new ParamPass1();
int i = 10;
System.out.println("pass方法调用前,i的值为=" + i);
p.pass(i);
System.out.println("pass方法调用后,i的值为=" + i);
}

public void pass(int i) {
i *= 3;
System.out.println("pass方法中,i的值为=" + i);
}
}

上面代码中,我们在类中定义了一个 pass 方法,方法内部将传入的参数 i 的值增加至 3 倍,然后分别在 pass 方法和 main 方法中打印参数的值,输出结果如下:

1
2
3
pass方法执行前,i的值为=10
pass方法中,i的值为=30
pass方法执行后,i的值为=10

从上面运行结果来看,pass 方法中,i 的值是 30,pass 方法执行结束后,变量 i 的值依然是 10。

可以看出,main 方法里的变量 i,并不是 pass 方法里的 i,pass 方法内部对 i 的值的修改并没有改变实际参数 i 的值,改变的只是 pass 方法中 i 的值(pass 方法中,i=30),因为 pass 方法中的 i 只是 main 方法中变量 i 的复制品

因此同学们很容易得出结论:_Java 中,一个方法不可能修改一个基本数据类型的参数 ,所以是值传递_。

然而,结论下的还太早,因为方法参数共有两种类型:

  1. 基本数据类型
  2. 引用数据类型

前面看到的只是基本数据类型的参数,那对于引用类型的参数,又是怎么样的呢?看如下代码:

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
public class ParamPass2 {
public static void main(String[] args) {
ParamPass2 p = new ParamPass2();

User user = new User();
user.setName("张三");
user.setAge(18);

System.out.println("pass方法调用前,user=" + user.toString());
p.pass(user);
System.out.println("pass方法调用后,user=" + user.toString());
}

public void pass(User user) {
user.setName("李四");
System.out.println("pass方法中,user = " + user.toString());
}
}

class User {
/**
* 姓名
*/
private String name;

/**
* 年龄
*/
private int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

上面代码中,定义了一个 User 类,在 main 方法中,new 了一个新的 User 对象 user,然后给 user 对象的成员变量赋值,pass 方法中,修改了传入的 user 对象的属性。

运行 main 方法,结果如下:

1
2
3
pass方法调用前,user= User{name='张三', age=18}
pass方法中,user = User{name='李四', age=18}
pass方法调用后,user= User{name='李四', age=18}

经过 pass 方法执行后,实参的值竟然被改变了!!!那按照上面的引用传递的定义,实际参数的值被改变了,这不就是引用传递了么?

有同学可能会说:难道在 Java 的方法中,在传递基本数据类型的时候是值传递,在传递引用数据类型的时候是引用传递?

其实不然,Java 中传递引用数据类型的时候也是值传递

为什么呢?

先给大家说一下概念中的重点:

值传递,是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

引用传递,是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

总结下两者的区别:

值传递

引用传递

根本区别

会创建副本

不会创建副本

所以

函数中无法改变原始对象

函数中可以改变原始对象

敲黑板:复制的是参数的引用(地址值),并不是引用指向的存在于堆内存中的实际对象。

main 方法中的 user 是一个引用(也就是一个指针),它保存了 User 对象的地址值,当把 user 的值赋给 pass 方法的 user 形参后,即让 pass 方法的 user 形参也保存了这个地址值,即也会引用到堆内存中的 User 对象。

上面代码中,之所以产生引用传递的错觉,是因为参数保存的是实际对象的地址值,你改变的只是地址值指向的堆内存中的实际对象,并没有真正改变参数,参数的地址值没有变。

下面结合生活中的场景,再来深入理解一下值传递和引用传递。

你有一把钥匙,当你的朋友想要去你家的时候,如果你直接把你的钥匙给他了,这就是引用传递。这种情况下,如果他对这把钥匙做了什么事情,比如他在钥匙上刻下了自己名字,那么这把钥匙还给你的时候,你自己的钥匙上也会多出他刻的名字。

你有一把钥匙,当你的朋友想要去你家的时候,你复刻了一把新钥匙给他,自己的还在自己手里,这就是值传递。这种情况下,他对这把钥匙做什么都不会影响你手里的这把钥匙。

但是,不管上面哪种情况,你的朋友拿着你给他的钥匙,进到你的家里,把你家的电视砸了。那你说你会不会受到影响?

我们在 pass 方法中,改变 user 对象的 name 属性的值的时候,不就是在 “砸电视” 么。你改变的不是那把钥匙(地址值),而是钥匙打开的房子(地址值对应的实际对象)。

那我们如何真正的改变参数呢,看如下代码:

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
public class ParamPass3 {
public static void main(String[] args) {
ParamPass3 p = new ParamPass3();

User user = new User();
user.setName("张三");
user.setAge(18);

System.out.println("pass方法调用前,user= " + user.toString());
p.pass(user);
System.out.println("pass方法调用后,user= " + user.toString());
}

public void pass(User user) {
user = new User();
user.setName("李四");
user.setAge(20);
System.out.println("pass方法中,user = " + user.toString());
}
}

class User {
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

在这段代码中,pass 方法中,我们真正的改变了 user 参数,因为它指向了一个新的地址 user = new User(),即参数的地址值改变了。运行结果如下:

1
2
3
pass方法调用前,user= User{name='张三', age=18}
pass方法中,user = User{name='李四', age=20}
pass方法调用后,user= User{name='张三', age=18}

从结果看出,对参数进行了修改,没有影响到实际参数。

所以说,Java 中其实还是值传递的,只不过对于引用类型参数,值的内容是对象的引用。