Introduction
This page is gradually growing as the topic comes up more and more often on the Java newsgroups. It is now in three sections:
- My own answer (directly below)
- Dale King's formal analysis
- Chris Smith's "workaround" post which lists some of the ways of avoiding needing pass-by-reference semantics in the first place
介绍
本文是由于在Java新闻组中相关话题被越来越多地提出而逐渐积累起来的。现在它分为三个部分:
l 我的回答(如下)
l Dale King的正式的分析
l Chris Smith在其工作区中列出的一些方法,如何在一开始的时候就避免产生需要传递引用的语义出现
----------------------------------------------------------------------------------------------------------------------------------
Parameter passing in Java - by reference or by value
This is another common question on Java newsgroups, made worse by the fact that people who should know better still perpetuate the following myth:
Myth: "Objects are passed by reference, primitives are passed by value"
Some proponents of this then say, "Ah, except for immutable objects which are passed by value [etc]" which introduces loads of rules without really tackling how Java works. Fortunately the truth is much simpler:
Truth #1: Everything in Java is passed by value. Objects, however, are never passed at all.
That needs some explanation - after all, if we can't pass objects, how can we do any work The answer is that we pass references to objects. That sounds like it's getting dangerously close to the myth, until you look at truth #2:
Truth #2: The values of variables are always primitives or references, never objects.
This is probably the single most important point in learning Java properly. It's amazing how far you can actually get without knowing it, in fact - but vast numbers of things suddenly make sense when you grasp it.
Java
参数传递是传值还是传引用?
以下是另一个在Java工作组中经常被提出的问题,而本应该更清楚问题的答案的人们却依然延续着一个谬论:
谬论:“对象传递是传引用,原始数据类型传递是传值”
某些这种观点的支持者会说:“哦,除了一些常量对象,它们是通过值传递的,等等”,这些人在没有真正掌握Java是如何工作前,向人们介绍了大量的“规则”。幸运的是,事实要简单许多:
事实1:Java中所有的参数都是值传递的。并且,对象本身永远都不会被传递。
这里我们需要解释一下,如果我们不能传递对象,那我们怎么能进行各项工作呢?答案是我们向对象传递引用(这句话我个人认为是有问题的,事实上Java中根本没有引用的概念,引用是C++语言等里的概念。Java传递的是指针的值,即地址,传地址不能说是传引用,因为Java和C++对这个地址的后续处理是不同的。如果硬要这么说,我觉得改成we pass references to variables更好)。这听起来和上面的谬论很接近,直到你看到以下第二个事实:
事实2:变量的值始终是原始数据类型或引用,而不是对象。
这一点可能是Java学习中最重要的一点了。如果不了解这一点,你可能会偏差很远,而一旦你掌握了它,很多事情一下子就变得有意义了。
----------------------------------------------------------------------------------------------------------------------------------
Why is all this important
When people hear the words "pass by reference", they may understand different things by the words. There are some pretty specific definitions of what it should mean, however. Dale King sometimes quotes this one: "The lvalue of the formal parameter is set to the lvalue of the actual parameter." (Dale's formal analysis of this whole question can be found at the bottom of this page.) This would mean that if you wrote the following code:
Object x = null;
giveMeAString (x);
System.out.println (x);
[...]
void giveMeAString (Object y){
y = "This is a string";
}
the result (if Java used pass-by-reference semantics) would be
This is a string
instead of the actual result:
null
Explaining the two truths above eliminates all of this confusion.
为什么这一点很重要?
当人们听到“传引用”时,可能会有不同的理解。对这个词的意思有一些特有的定义,但是Dale King不时会引述这句话:“形参的左值(lvalue)被设置到实参的左值中去。”(Dale的正式的分析可以在本文的底部找到)这意味着如果你写如下代码:
Object x = null;
giveMeAString (x);
System.out.println (x);
[...]
void giveMeAString (Object y){
y = "This is a string";
}
如果Java使用传引用的语义,结果将是
This is a string
而真正的结果是:
null
使用上文的两个事实解释就能够排除所有的疑惑。
----------------------------------------------------------------------------------------------------------------------------------
So what does passing a reference by value actually mean
It means you can think of references how you think of primitives, to a large extent. For instance, the equivalent to the above bit of code using primitives would be:
int x = 0;
giveMeATen (x);
System.out.println (x);
[...]
void giveMeATen (int y){
y = 10;
}
Now, the above doesn't print out "10". Why not Because the value "0" was passed into the method giveMeTen, not the variable itself. Exactly the same is true of reference variables - the value of the reference is passed in, not the variable itself. It's the same kind of copying that happens on variable assignment. The first code snippet, if inlined, is equivalent to:
// Before the method call
Object x = null;
// Start of method call - parameter copying
Object y = x;
// Body of method call
y = "This is a piece of string.";
// End of method call
System.out.println (x);
If you want to think pictorially, you might find my "objects are balloons, references are pieces of string" analogy helpful.
因此通过值来传递引用的意思是什么呢?
它的意思是广义上你可以把引用当成原始数据类型考虑。例如,可以使用原始数据类型来等价地描述上面的代码:
int x = 0;
giveMeATen (x);
System.out.println (x);
[...]
void giveMeATen (int y){
y = 10;
}
上面的代码不会打印“10”。为什么?因为是值“0”传到了方法giveMeATen中,而不是变量本身。引用变量也是这样,引用的值被传了进去,而不是引用变量本身。这与变量赋值时是拷贝的道理一样,第一个代码段如果使用内联等价于:
// 方法调用前
Object x = null;
// 开始函数调用——参数拷贝
Object y = x;
// 执行函数体
y = "This is a piece of string.";
// 函数调用结束
System.out.println (x);
如果你想更形象的思考,你可能会觉得以下“对象是气球,引用是线”的类比有用。
----------------------------------------------------------------------------------------------------------------------------------
The balloon analogy
I imagine every object as a helium balloon, every reference as a piece of string, and every variable as something which can hold onto a piece of string. If the reference is a null reference, that's like having a piece of string without anything attached to the end. If it's a reference to a genuine object, it's a piece of string tied onto the balloon representing that object. When a reference is copied (either for variable assignment or as part of a method call) it's as if another piece of string is created attached to whatever the first piece of string is attached to. The actual piece of string the variable (if any) is holding onto doesn't go anywhere - it's only copied.
This analogy also explains garbage collection (apart from the java.lang.ref API, which does "odd" things :) - a balloon floats away unless it is tethered down to something. The balloons can have further holders on them (instance variables), but just because two balloons are holding onto each other doesn't stop them from floating away. (Cyclic references are collected.) Any balloon representing an object which is in the middle of having a method invoked is tethered to the JVM. (Apologies for not being able to phrase that more succinctly - all I mean is that anything in an active thread's stack isn't garbage collected.)
气球类比
这个类比也解释了垃圾回收机制(java.lang.ref API的一部分,它处理“落单”的对象)——气球如果不绑住某些东西就会飞走。气球可以有更多的捆绑物(实例变量),但是两个气球互相绑在一起不能阻止它们飞走(循环引用会被回收)。任何代表一个对象的气球在方法调用中是绑在JVM上的。(很抱歉没有说得更简洁——我想表达的意思是任何在一个激活的线程栈中的事物都不会被垃圾回收)
----------------------------------------------------------------------------------------------------------------------------------
A more formal analysis
This excellent formal analysis of the question is courtesy of Dale King
Question: Does Java pass objects by reference or by value
Answer:
Since it makes no sense to begin any argument without agreed upon defintions let's formally define our terms. I will use abstract pseudocode to keep the issue from being clouded by the idiom of a particular language. The source of my information is the book "Advanced Programming Language Design" by Raphael A. Finkel.
For those unfamiliar with the term below an L-value is an expression that can appear on the left side of an assignment statement. It is basically a way to address where a variable is stored. Variables and other ways to refer to locations in memory are L-values. Most expressions are not L-values, e.g.
We assume the presence of a procedure named f that takes a formal parameter s. We call that function giving it an actual parameter g.
The calling code:
f( g )
The function:
procedure f( s )
begin
-- body of the procedure
end;
There are several parameter passing semantics that have been proposed or used:
value
The value of the actual parameter is copied into the formal parameter when the procedure is invoked. Any modification of the formal parameter affects only the formal parameter and not the actual parameter. This is the most common form of parameter passing and is the only one provided in C and Java.
result
The value of the formal parameter is copied into the actual parameter when the procedure returns. Modifications to the formal parameter do not affect the formal parameter until the function returns. The actual parameter must be an L-value. It is usually invalid to pass the same L-value to more than one result parameter, but the compiler cannot always detect this. The best example of this is out parameters in CORBA.
value result
Combination of value and result semantics. The best example of this are inout parameters in CORBA.
reference
The L-value of the formal parameter is set to the L-value of the actual parameter. In other words, the address of the formal parameter is the same as the address of the actual parameter. Any modifications to the formal parameter also immediately affect the actual parameter. FORTRAN only has reference mode (expressions are evaluated and stored in a temporary location in order to obtain an L-value). C++ has reference parameters by putting a & before the formal parameter name in the function header. Reference mode can be simulated in C using pointers and adding the & to the actual parameter and dereferencing the formal parameter within the function.
readonly
Can use either value or reference mode, but modification of the formal parameter is forbidden by the compiler.
macro
name
These two have been used in the past, but are very much out of favor because they are confusing and difficult to implement. Therefore I won't bother trying to explain them.
Now that we have some definitions of terms we can return to the question. Does Java pass objects by reference or by value
The answer is NO! The fact is that Java has no facility whatsoever to pass an object to any function! The reason is that Java has no variables that contain objects.
The reason there is so much confusion is people tend to blur the distinction between an object reference variable and an object instance. All object instances in Java are allocated on the heap and can only be accessed through object references. So if I have the following:
StringBuffer g = new StringBuffer( "Hello" );
The variable g does not contain the string "Hello", it contains a reference (or pointer) to an object instance that contains the string "Hello".
So if I then call f( g ), f is free to modify its formal parameter s to make it point to another StringBuffer or to set it to null. The function f could also modify the StringBuffer by appending " World" for instance. While this changes the value of that StringBuffer, the value of that StringBuffer is NOT the value of the actual parameter g.
Imagine for instance if I set g to null before passing it to f. There is no StringBuffer now to modify and f can in no way change the value of g to be non-null.
The bottom line is Java only has variables that hold primitives or object references. Both are passed by value.
一个更加正式的分析
这个优秀的正式分析来自于Dale King的慷慨相助。
问题:Java是传递对象是通过值传递还是引用传递?
回答:
既然在没有一致的定义的前提下展开任何争论是没有意义的,让我们正式的定义我们的术语。我将使用抽象的伪码使我们讨论的话题能够摆脱某一特定语言习惯的束缚。我的信息来源于Raphael A. Finkel的书《高级编程语言设计》。
( x * 2 ) 对下面的术语不熟悉的人来说,一个左值(L-Value)是指能够出现在一条赋值语句的左端的表达式。它是标明一个变量所存储的地址的一种基本方式。变量和其它涉及在内存中定位的方式都是左值。大多数的表达式并不是左值,例如(x * 2)
我们假设一个程序的展现f有一个形参s。我们调用这个函数并传入一个实参g。
调用代码为:
f( g )
函数为:
procedure f( s )
begin
-- 程序体
end;
这里有几个参数传递语义被提出或使用:
值
在程序被调用的时候,实参的值被拷贝到形参中去。任何关于形参的修改只会影响形参本身而不会改变实参。这是最为普遍的一种参数传递方式,也是C和Java都提供的一种方式。
结果
在程序返回的时候,形参的值被拷贝到实参中去。对形参的修改直到函数返回时才会对形参产生影响。实参必须是一个左值。通常向同一个左值传递多个结果参数是无效的,但是编译器并不是总能检测出来。最好的例子就是CORBA的输出参数。
值结果
值语义和结果语义的组合。最好的例子就是CORBA的输入输出参数。
引用
形参的左值被设置到了实参的左值中去。换句话说,形参的地址和实参的地址相同。对形参的任何修改也会立即影响到实参。FORTRAN语言只有引用模式(为了取得一个左值,表达式被求值和存储在一个临时位置上)。C++通过在函数头里形参名前加“&”来表示引用参数。在C语言中,引用模式可以通过指针以及在实参前加“&”和在函数中解除形参来模拟。
只读
能使用值或引用模式,但是对形参的修改被编译器禁止。
宏
名称
这两种形式在过去被使用,但是由于它们容易令人迷惑并且难于执行,目前已经失宠。因此,我不会赘述。
现在我们有了一些术语的定义,让我们回到问题上去。Java传递对象是引用传递还是值传递?
答案是都不是!事实上,Java在任何情况下都不会向任何函数传递对象!原因是Java没有用来容纳对象的变量。
人们迷惑的原因是因为他们弄混了对象引用变量和对象实例的区别。所有Java中的对象实例都被分配在堆上,并且只能通过对象引用来访问它。因此,如果我有以下的:
StringBuffer g = new StringBuffer( "Hello" );
变量g并没有容纳整个字符串“Hello”,它装载的是一个指向对象的引用(或指针),是这个对象包含了字符串“Hello”。
所以如果我调用f(g),f可以随意改变它的形参s使其指向另一个StringBuffer或者使其置为空。例如,函数f也可以追加字符串“World”,这个改变了StringBuffer的值,但是StringBuffer的值并不是实参g的值。
例如,设想如果在传入f前我将g的值置为空,则现在就没有可以修改的Stringbuffer,函数f没有任何办法能将实参g修改成非空。
关键之处在于Java只有能装载原始数据类型和对象引用的变量。而两者都是通过值来传递的。
----------------------------------------------------------------------------------------------------------------------------------
Ways to avoid needing pass-by-reference
This section is courtesy of Chris Smith. (Updated by Jon Skeet.)
There are good reasons that Java excluded the idea of pass-by-reference from its language design, and when writing Java applications it's best to do as Java does. There are elegant solutions to all common problems that may be solved with pass-by-reference in other languages. Before I get there, though, let's look at some of the problems of pass by reference.
Pass by reference mixes inputs and outputs of code. This is the fundamental problem with the technique. In Java, a programmer can assume that variables will not change their value when passed as parameters to a method. In languages with pass by reference semantics, this basic assumption cannot be made.
Pass by reference confuses the interface to a method. Methods written using pass-by-reference semantics can have extremely complex interfaces that are difficult for client programmers to learn and understand. That said, you may be left with a situation where you feel the need to use pass-by-reference in an application. There are two major reasons to use pass by reference, and each has its own solution:
First, pass by reference is used in many languages to reduce the costs of a method call, preventing the copying of large amounts of data. This is a non-issue in Java. The problem is solved by simply realizing that in Java, the values passed to a method are either primitive data or object references, which cannot be large enough to make this a real issue. Objects themselves can be very large, but are never passed to methods.
Second, pass by reference allows the variable to be changed, and the changed value can be seen in client code. The solution here is to refactor the application to use the return value for this purpose. If a parameter is an "in-out" parameter, then its original value should be passed into the method and its result moved to the return value. The client code may then look like this:
a = someMethod(a);
This is the real reason why pass by reference is used in many cases - it allows a method to effectively have many return values. Java doesn't allow multiple "real" return values, and it doesn't allow pass by reference semantics which would be used in other single-return-value languages. However, here are some techniques to work around this:
- If any of your return values are status codes that indicate success or failure of the method, eliminate them immediately. Replace them with exception handling that throws an exception if the method does not complete successfully. The exception is a more standard way of handling error conditions, can be more expressive, and eliminates one of your return values.
- Find related groups of return values, and encapsulate them into objects that contain each piece of information as fields. The classes for these objects can be expanded to encapsulate their behavior later, to further improve the design of the code. Each set of related return values that you encapsulate into an object removes return values from the method by increasing the level of abstraction of the method's interface. For instance, instead of passing co-ordinates X and Y by reference to allow them to be returned, create a mutable Point class, pass an object reference by value, and update the object's values within the method.
- If you find yourself passing in an object and then returning a new version of that object, and the object is mutable, then consider moving the method to be a member of the class of the object that you were passing. This can improve the encapsulation of the design and simplify the interface.
- If, after all steps above, you are still left with multiple returns to a method, split the method into several different methods that each return a part of the answer. Using this set of methods from client code will be much clearer than one mega-method.
规避需要传引用的方法
本部分来自于Chris Smith的慷慨相助(由Jon Skeet更新)。
Java在语言设计上之所以要排除传引用的方式是有很好的原因的。并且在写Java应用程序的时候,我们最好遵从Java的这个习惯。对在其它语言中采用引用传递来解决的所有一般性的问题,Java都有一流的解决方案。在我讨论它们以前,让我们来看看引用传递的一些毛病。
引用传递将代码的输入和输出混在了一起。这是这项技术的最基本的问题所在。在Java中,程序员可以假设变量在作为参数传入到方法中时其值不会改变。在拥有引用传递语义的语言中,这个基本的假设是不成立的。
引用传递混淆了方法的接口。使用引用传递语义书写的方法拥有很复杂的接口,这样客户程序员很难学习和理解。有人说,在应用中你可能会遇到你觉得非常需要使用引用传递的情况。我们使用引用传递有两个主要的原因,并且每一个都有解决的办法:
首先,在许多语言中引用传递被用来减少函数调用的成本,防止出现拷贝大量数据的情况。在Java中这不是个问题。在Java中给方法传递的值是原始数据类型或者对象的引用,问题就被轻而易举地解决了,因为这两种值都不是很大。对象本身可以很大,但是它永远都不会被传递给方法。
其次,引用传递允许变量被改变,并且改变的值在客户端代码中可见。解决的办法是重构应用程序,使用返回值来达到这个目的。如果参数是一个“输入-输出”参数,那么它的原值应该传入到方法中,而它的结果应该移动到返回值中去。客户端代码可能会像下面这样:
a = someMethod(a);
这是在很多场合使用引用传递的真正原因——它允许方法高效地拥有多个返回值。Java不允许有多个“实”返回值,并且它也不允许在其它单一返回值语言中用到的引用传递方式。但是,这里有一些办法来解决这个问题:
- 如果你的任一返回值是用于显示方法成功或失败执行的状态代码,立刻将它们删除。使用异常处理机制来替代它们,如果代码没有成功完成抛出一个异常。异常是更标准的处理错误情况的方式,它不仅更富于表现力,而且多可以排除你的一种返回值。
- 找到相关的返回值组,将它们封装到对象中去,而每条信息作为这个对象的一个字段。这些对象的类可以在将来封装更多的行为,大大提升了代码的设计。封装到对象中的每个返回值的集合,可以通过提升方法接口抽象级别的方式来从方法中移除返回值。例如,与其将协调工作的X和Y以引用的方式传递,以达到能够同时返回的目的,不如创建一个可以变化的指向类,通过值来传递对象引用,并且在方法中更新对象的值。
- 如果你发觉你需要传递一个对象,然后再返回这个对象的新版本,并且这个对象是可变的,那么你可以考虑将方法移到对象的类中去,使其成为这个类成员函数。这个可以提高设计的封装性和简化接口。
- 如果在以上步骤后,你依然需要方法有多个返回值,那么把这个方法分成若干个不同的子方法,每个子方法返回原来一部分的返回值。与一个大方法比起来,使用这样的方法集合会使客户代码更加清晰。