我对《Java 应用程序中的按值传递语义》的理解(转)

hxz 03-01-09

我对《Java 应用程序中的按值传递语义》的理解
cherami 原创 (参与分:43175,专家分:730) 发表:2002-11-14 下午9:38 版本:1.0 阅读:687次


看到大家在看完我转载的《Java 应用程序中的按值传递语义》反倒引起很多的争论和导致一些人越来越糊涂了,因此有必要再详细的谈谈我对这个的理解了。
以下摘自原文:

节选理解参数是按值而不是按引用传递的说明 Java 应用程序有且仅有的一种参数传递机制,即按值传递。写它是为了揭穿普遍存在的一种神话,即认为 Java 应用程序按引用传递参数,以避免因依赖“按引用传递”这一行为而导致的常见编程错误。

对此节选的某些反馈意见认为,我把这一问题搞糊涂了,或者将它完全搞错了。许多不同意我的读者用 C++ 语言作为例子。因此,在此栏目中我将使用 C++ 和 Java 应用程序进一步阐明一些事实。

要点
读完所有的评论以后,问题终于明白了,至少在一个主要问题上产生了混淆。某些评论认为我的节选是错的,因为对象是按引用传递的。对象确实是按引用传递的;节选与这没有冲突。节选中说所有参数都是按值 -- 另一个参数 -- 传递的。下面的说法是正确的:在 Java 应用程序中永远不会传递对象,而只传递对象引用。因此是按引用传递对象。但重要的是要区分参数是如何传递的,这才是该节选的意图。Java 应用程序按引用传递对象这一事实并不意味着 Java 应用程序按引用传递参数。参数可以是对象引用,而 Java 应用程序是按值传递对象引用的。

C++ 和 Java 应用程序中的参数传递
Java 应用程序中的变量可以为以下两种类型之一:引用类型或基本类型。当作为参数传递给一个方法时,处理这两种类型的方式是相同的。两种类型都是按值传递的;没有一种按引用传递。这是一个重要特性,正如随后的代码示例所示的那样。

在继续讨论之前,定义按值传递和按引用传递这两个术语是重要的。按值传递意味着当将一个参数传递给一个函数时,函数接收的是原始值的一个副本。因此,如果函数修改了该参数,仅改变副本,而原始值保持不变。按引用传递意味着当将一个参数传递给一个函数时,函数接收的是原始值的内存地址,而不是值的副本。因此,如果函数修改了该参数,调用代码中的原始值也随之改变。


上面的这些是很重要的,请大家注意以下几点结论,这些都是我认为的上面的文章中的精华和最终的结论:

1、对象是按引用传递的
2、Java 应用程序有且仅有的一种参数传递机制,即按值传递
3、按值传递意味着当将一个参数传递给一个函数时,函数接收的是原始值的一个副本
4、按引用传递意味着当将一个参数传递给一个函数时,函数接收的是原始值的内存地址,而不是值的副本

首先我们来看看第一点:对象是按引用传递的
确实,这一点我想大家没有任何疑问,例如:

class Test01 { public static void main(String[] args) { StringBuffer s= new StringBuffer("good"); StringBuffer s2=s; s2.append(" afternoon."); System.out.println(s); }}

对象s和s2指向的是内存中的同一个地址因此指向的也是同一个对象。
如何解释“对象是按引用传递的”的呢?

这里的意思是进行对象赋值操作是传递的是对象的引用,因此对象是按引用传递的,有问题吗?
程序运行的输出是:
good afternoon.

这说明s2和s是同一个对象。

这里有一点要澄清的是,这里的传对象其实也是传值,因为对象就是一个指针,这个赋值是指针之间的赋值,因此在java中就将它说成了传引用。(引用是什么?不就是地址吗?地址是什么,不过就是一个整数值)

再看看下面的例子:

class Test02 { public static void main(String[] args) { int i=5; int i2=i; i2=6; System.out.println(i); }}

程序的结果是什么?5!!!
这说明什么,原始数据类型是按值传递的,这个按值传递也是指的是进行赋值时的行为。


下一个问题:Java 应用程序有且仅有的一种参数传递机制,即按值传递

class Test03 { public static void main(String[] args) { StringBuffer s= new StringBuffer("good"); StringBuffer s2=new StringBuffer("bad"); test(s,s2); System.out.println(s);//9 System.out.println(s2);//10 } static void test(StringBuffer s,StringBuffer s2) { System.out.println(s);//1 System.out.println(s2);//2 s2=s;//3 s=new StringBuffer("new");//4 System.out.println(s);//5 System.out.println(s2);//6 s.append("hah");//7 s2.append("hah");//8 }}

程序的输出是:
good
bad
new
good
goodhah
bad

为什么输出是这样的?
这里需要强调的是“参数传递机制”,它是与赋值语句时的传递机制的不同。
我们看到1,2处的输出与我们的预计是完全匹配的
3将s2指向s,4将s指向一个新的对象
因此5的输出打印的是新创建的对象的内容,而6打印的原来的s的内容
7和8两个地方修改对象内容,但是9和10的输出为什么是那样的呢?
Java 应用程序有且仅有的一种参数传递机制,即按值传递。

至此,我想总结一下我对这个问题的最后的看法和我认为可以帮助大家理解的一种方法:
我们可以将java中的对象理解为c/c++中的指针
例如在c/c++中:
int *p;

print(p);//1
*p=5;
print(*p);//2

1打印的结果是什么,一个16进制的地址,2打印的结果是什么?5,也就是指针指向的内容。
即使在c/c++中,这个指针其实也是一个32位的整数,我们可以理解我一个long型的值。

而在java中一个对象s是什么,同样也是一个指针,也是一个int型的整数(对于JVM而言),我们在直接使用(即s2=s这样的情况,但是对于System.out.print(s)这种情况例外,因为它实际上被转换为System.out.print(s.toString()))对象时它是一个int的整数,这个可以同时解释赋值的传引用和传参数时的传值(在这两种情况下都是直接使用),而我们在s.XXX这样的情况下时s其实就是c/c++中的*s这样的使用了。这种在不同的使用情况下出现不同的结果是java为我们做的一种简化,但是对于c/c++程序员可能是一种误导。java中有很多中这种根据上下文进行自动识别和处理的情况,下面是一个有点极端的情况:

class t { public static String t="t"; public static void main(String[] args) { t t =new t(); t.t(); } static void t() { System.out.println(t); }}

(关于根据上下文自动识别的内容,有兴趣的人以后可以看看我们翻译的《java规则》)
呵呵,这篇说明可能会让一些人更混乱了,但是既然写出来了就贴出来吧,不要骂我啊。
^_^


还是新手
2003-01-10 12:58

这种老学究似的辩论真的没有必要,何必在概念上纠缠不清,其实大家都明白是怎么回事,只不过概念的界定有分歧罢了,其实在许多专著上都有所论述。