本文根据toBeTopJavaer和《码出高效》补充学习Java

Java是通过值传递来传递参数的,还是通过引用传递来传递的呢?这个争议由来已久。接下来看一看在Stack Overflow上Is Java “pass-by-reference” or “pass-by-value”?这个问题下,大家是怎么看的吧。

观点一

我怕我翻译错,因此本来写了好多,最后还是用原文吧。

Stack Overflow上最高票(5558票)的观点是:

Java is always pass-by-value. Unfortunately, when we pass the value of an object, we are passing the reference to it. This is confusing to beginners.

It goes like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
Dog aDog = new Dog("Max");
Dog oldDog = aDog;

// we pass the object to foo
foo(aDog);
// aDog variable is still pointing to the "Max" dog when foo(...) returns.
aDog.getName().equals("Max"); // true
aDog.getName().equals("Fifi"); //false
aDog == oldDog; // true
}

public static void foo(Dog d) {
d.getName().equals("Max"); // true
// change d inside of foo() to point to a new Dog instance of "Fifi";
d = new Dog("Fifi");
d.getName().equals("Fifi"); // true
}

In the example above aDog.getName() will still return "Max". The value aDog within main is not changed in the function foo with the Dog "Fifi" as the object reference is passed by value. If it were passed by reference, then the aDog.getName() in main would return "Fifi" after the call to foo.

Likewise:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
Dog aDog = new Dog("Max");
Dog oldDog = aDog;

foo(aDog);
// when foo(...) returns, the name of the dog has been changed to "Fifi"
aDog.getName().equals("Fifi"); // true
// but it is still the same dog:
aDog == oldDog; // true
}

public static void foo(Dog d) {
d.getName().equals("Max"); // true
// this changes the name of d to be "Fifi"
d.setName("Fifi");
}

In the above example, Fifi is the dog’s name after call to foo(aDog) because the object’s name was set inside of foo(...). Any operations that foo performs on d are such that, for all practical purposes, they are performed on aDog, but it is not possible to change the value of the variable aDog itself.

观点二

I just noticed you referenced my article.

Java规范说在Java中任何东西都是值传递,在Java中没有“引用传递”。

理解这个的关键是

1
Dog myDog;

不是一个Dog,而是一个指向Dog的指针

什么意思呢?就是说当

1
2
Dog myDog = new Dog("Rover");
foo(myDog);

的时候,你实际上给foo传递的是Dog对象的地址

(我说“实际上”是因为Java指针并不是直接的地址,但是我们可以简单地把它理解为地址)

假设Dog对象的地址是在42,我们就把42传给foo方法。

如果我们是这样定义foo方法:

1
2
3
4
5
public void foo(Dog someDog) {
someDog.setName("Max"); // AAA
someDog = new Dog("Fifi"); // BBB
someDog.setName("Rowlf"); // CCC
}

让我们看看发生了什么:

  • someDog的值被设置为42
  • 在AAA行,
    • someDog is followed to the Dog it points to(the Dog object at address 42)(意会,但是不好翻译😂)
    • that Dog (the one at address 42) is asked to change his name to Max
  • 在BBB行
    • 新建了一个Dog对象,假设他的地址是74
    • 我们把someDog的值赋值为74
  • 在CCC行
    • someDog is followed to the Dog it points to (the Dog object at address 74)
    • that Dog (the one at address 74) is asked to change his name to Rowlf
  • 然后,return。

接下来我们想一下,执行foo后发生了什么,myDog改变了吗?

关键来了。

记得myDog是一个指针,而不是一个实际的Dog,那么答案是否定的,myDog地址仍然是42,它仍然指向了原来的Dog(但是因为AAA行,所以它的名字改成了Max,仍但是同一个Dog,myDog的值-42从来没有改变)

It’s perfectly valid to follow an address and change what’s at the end of it; that does not change the variable, however.(不知道如何翻译)

Java的工作原理很像C,你可以指定一个指针,把这个指针传递给一个方法,铜鼓这个指针,方法可以改变这个指针指向的对象的数据,但是却不能改变这个指针的指向。

在C++、Ada、Pascal和其他一些支持引用传递的语言中,你可以真的改变传进来的参数的指向。

If Java had pass-by-reference semantics, the foo method we defined above would have changed where myDog was pointing when it assigned someDog on line BBB.

Think of reference parameters as being aliases for the variable passed in. When that alias is assigned, so is the variable that was passed in.

Hollis的观点

大神在他的toBeTopJavaer中是这样写的:

值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

那么,我来给大家总结一下,值传递和引用传递之前的区别的重点是什么。

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

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

但是,不管上面那种情况,你的朋友拿着你给他的钥匙,进到你的家里,把你家的电视砸了。那你说你会不会受到影响?而我们在pass方法中,改变user对象的name属性的值的时候,不就是在“砸电视”么。

我们来画一张图,看一下整个过程中发生了什么,然后我再告诉你,为啥Java中只有值传递。

上面这种传递是什么传递?肯定不是引用传递,如果是引用传递的话,在user=new User()的时候,实际参数的引用也应该改为指向0X456789,但是实际上并没有。

通过概念我们也能知道,这里是把实际参数的引用的地址复制了一份,传递给了形式参数。所以,上面的参数其实是值传递,把实参对象引用的地址当做值传递给了形式参数

我们再来回顾下之前的那个“砸电视”的例子,看那个例子中的传递过程发生了什么。

同样的,在参数传递的过程中,实际参数的地址0X1213456被拷贝给了形参,只是,在这个方法中,并没有对形参本身进行修改,而是修改的形参持有的地址中存储的内容。

所以,值传递和引用传递的区别并不是传递的内容。而是实参到底有没有被复制一份给形参。在判断实参内容有没有受影响的时候,要看传的的是什么,如果你传递的是个地址,那么就看这个地址的变化会不会有影响,而不是看地址指向的对象的变化。就像钥匙和房子的关系。

那么,既然这样,为啥上面同样是传递对象,传递的String对象和User对象的表现结果不一样呢?我们在pass方法中使用name = “hollischuang”;试着去更改name的值,阴差阳错的直接改变了name的引用的地址。因为这段代码,会new一个String,在把引用交给name,即等价于name = new String(“hollischuang”);。而原来的那个”Hollis”字符串还是由实参持有着的,所以,并没有修改到实际参数的值。

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

总结

无论是值传递还是引用传递,其实都是一种求值策略(Evaluation strategy)。在求值策略中,还有一种叫做按共享传递(call by sharing)。其实Java中的参数传递严格意义上说应该是按共享传递。

按共享传递,是指在调用函数时,传递给函数的是实参的地址的拷贝(如果实参在栈中,则直接拷贝该值)。在函数内部对参数进行操作时,需要先拷贝的地址寻找到具体的值,再进行操作。如果该值在栈中,那么因为是直接拷贝的值,所以函数内部对参数进行操作不会对外部变量产生影响。如果原来拷贝的是原值在堆中的地址,那么需要先根据该地址找到堆中对应的位置,再进行操作。因为传递的是地址的拷贝所以函数内对值的操作对外部变量是可见的。

简单点说,Java中的传递,是值传递,而这个值,实际上是对象的引用。

而按共享传递其实只是按值传递的一个特例罢了。所以我们可以说Java的传递是按共享传递,或者说Java中的传递是值传递。

原文及参考

  1. toBeTopJavaer
  2. Is Java “pass-by-reference” or “pass-by-value”?