java到底是按值传递还是按引用传递?

UnAgain 2006-06-05 01:44:31
最近看了一个帖子,问“java到底是按值传递还是按引用传递?”。本来觉得很简单,为了能说的准确一点,我还专门就这个问题看了看langspec3.0。一看收获还真不小,就写了这篇文章。

我还不敢确定自己的观点对不对,所以贴在这里,希望大家一起讨论。

另外,贴在blog上了,在那里的效果比这里好。
http://blog.csdn.net/UnAgain/archive/2006/06/05/774039.aspx

1 数据类型
1.1 PrimitiveType(简单类型)
1.2 ReferenceType(引用类型)
2. 变量
2.1 简单类型变量
2.2 引用类型变量
3.赋值与传递
3.1 对象的赋值
3.2 传递
3.3 final变量能改变吗?
3.4 包装类的赋值与传递

1 数据类型
java的数据类型有两类:
PrimitiveType(简单类型)
ReferenceType(引用类型)

1.1 PrimitiveType(简单类型)
(参考:langspec-3.0/typesValues.html#4.2)

PrimitiveType的分类如下所示:

PrimitiveType:
NumericType
boolean

NumericType:
IntegralType
FloatingPointType

IntegralType: one of
byte short int long char

FloatingPointType: one of
float double

PrimitiveType是java预定义的类型,并且使用保留字命名。比如int、long、float等。由此看来其包装类不算PrimitiveType。
1.2 ReferenceType(引用类型)
(参考:langspec-3.0/typesValues.html#4.3)
ReferenceType有三种类型:类、接口、和数组。

2. 变量
(参考:langspec-3.0/typesValues.html#4.12)
A variable is a storage location and has an associated type, sometimes called its compile-time type, that is either a primitive type (§4.2) or a reference type (§4.3).
变量是关联于特定类型的存储单元,所关联的类型有时叫做变量的编译时类型,即,既可以是简单类型也可以是引用类型。
2.1 简单类型变量
A variable of a primitive type always holds a value of that exact primitive type.
简单类型的变量总是执持简单类型的值。
2.2 引用类型变量
A variable of a class type T can hold a null reference or a reference to an instance of class T or of any class that is a subclass of T. A variable of an interface type can hold a null reference or a reference to any instance of any class that implements the interface.

类型是T的类的变量可以执持null引用,或者类T及其子类的实例引用。接口类型的变量可以执持null引用,或者任何实现该接口的类的实例引用。

注:与langspec2.0不同的是,3.0引入了泛型的概念,其中有Type Variable的概念,上面的T就是一个Type Variable。
3.赋值与传递
如上所述,可以得出下面结论:
1) 对于简单类型变量的赋值是按值传递。就是说直接把数值存放到变量的存储单元里。
2) 对于引用类型的变量,赋值是把原对象的引用(可以理解为入口地址),存放在变量的存储单元里。
3.1 对象的赋值
简单类型的赋值很容易理解,这里仅讨论对象的赋值。所有引用类型的实例就是我们常说的对象。
可以这样说,除了null以外,任何变量的初始赋值都是分两步:
1) 创建对象实例
2) 把对象实例的引用赋值给变量。

比如:
Object o1 = new Object();
3.2 传递
传递是通过变量之间的赋值实现的。在以前的回贴中我说过这样一句话,单纯从变量的角度看,变量之间的赋值是值传递。现在我解释一下我的观点。

先举一个例子:
// java中所有的类的基类默认为Object,在此不赘述。
class Object1 {}
class Object2 {}

Object o1, o2;

o1 = new Object1();

o2 = o1;
o2 = new Object2();

这时候,o1的类型是什么?是Object1还是Object2?正确答案是Object1。
再举一个例子:
class Word {
String word;
public Word(String word){
this.word = word;
}
public void print(){
System.out.println(word);
}
}

Word o1, o2;

o1 = new Word("Every Day");

o2 = o1;
o2 = new Word("Every Night!");

w1.print();

会出现什么结果?"Every Day" 还是 "Every Night!"?仍然是"Every Day"。

这里面有一个很多人特别是初学者忽视了的观点 ―― 变量可以引用对象,但变量不是对象。什么是对象?对象初始化之后,会占用一块内存空间,严格意义上讲,这段内存空间才是对象。对象创建于数据段,而变量存在于代码段;对象的入口地址是不可预知的,所以程序只能通过变量来访问对象。

回到我们的问题上来,第一句
o1 = new Word("Every Day");
首先创建一个Word实例,即对象,然后把“引用”赋值给o1。
第二句
o2 = o1;
o1把对象的引用赋值给o2,注意赋的值是对象的引用而不是o1自身的引用。所以,在的三句
o2 = new Word("Every Night!");
就是又创建一个新对象,再把新对象的引用赋值给o2。

因为o1和 o2之间是值传,所以,对o2的改变丝毫不会影响到o1。

也有一种情况好像是影响到了o1,我们继续上面的例子,给Word增加一个方法
class Word {
String word;
public Word(String word){
this.word = word;
}
public void print(){
System.out.println(word);
}
public void setWord(String word){
this.word = word;
}
}

Word o1, o2;

o1 = new Word("Every Day");
o2 = o1;
o2.set Word("Every Night!");

o1.print();

这时的结果是"Every Night!"。

那么,这是改变了o1吗?从严格意义上讲,不是。因为o1只是保存对象的引用,执行之后,o1还是持有该对象的引用。所以,o1没变,变的是o1所引用的对象。
3.3 final变量能改变吗?
好了,我再出道题目:

final Word o3 = new Word("Every Day!");
o3.setWord("Every Night!");

能通过编译吗?对于final的定义大家都知道,o3是相当于一个常量,既然是常量,怎么能再改变呢?
答案是肯定的,能。道理我想大家也明白,这里不罗嗦了。
3.4 包装类的赋值与传递
以前看过文章说,对于java基本类型及其包装类采用值传递,对于对象采用引用传递。从langspec看,首先包装类不是PrimitiveType,那就只能是ReferenceType,而ReferenceType的变量保存的是引用。既然保存的是引用,也就无从传递数值。那么,这两个观点矛盾吗?

首先,肯定是langspec正确。
其次,虽然前一观点在原理上有错误,但却不影响正常使用。

为什么会出现这种情况?这是因为这些包装类具有一个简单类型的特征,即,不可改变。以String为例,看一下API Specification,不会找到能够改变String对象的方法。任何输出上的改变都是重建新的String对象,而不是在原对象基础上改变。改变的是变量的内容,即,不同对象的引用。
...全文
11513 100 打赏 收藏 转发到动态 举报
写回复
用AI写文章
100 条回复
切换为时间正序
请发表友善的回复…
发表回复
晨星 2006-06-08
  • 打赏
  • 举报
回复
To:UnAgain楼主
我开始明白你所说的“利用数组实现传址效果”的意思了。

但我觉得这个有点牵强,我不赞同把这看成一个特殊情况,也不赞同看成一个技巧。
原因正如你所提到的:“在java中,数组也是对象,如果把数组分量理解为对象的成员变量,就会更容易理解这个问题”。

虽然我也曾被人警告过“不要想当然的认为JAVA中一个数组就等同于一个对象”,但至少在这里,我们这样类比一下没有任何问题,我们能够做的同样仅仅是利用一个变量,这次是一个引用到数组的变量,去修改它所引用着的那些元素们。
所以,这里没有任何值得惊奇的事情发生。正好你所说,完全可以看成对“分量”的修改,只是数组里的“分量”(元素)都是同构的(某种意义上),类里的“分量”(字段)则可以是异构的。
(同构异构可以对比一下Perl语言,Perl语言没有像Java和C/C++语言中的这种class或struct,而只有类似数组的list,我觉得原因就是因为Perl语言类型系统是单一的,变量不分类型,只看使用场合。在这种情况下,用于组织数据的struct(或class)就跟一个array没有任何区别——都是一组不分类型分量而已)。

又叉开了,呵呵。仿照一下你的test方法,只用一个小例子就可以说明仍然不是传址:
void test(String[] buffers) {
if(null != buffers && 0 != buffers.length)
buffers[0] = "ABCDEFG";
}
String s = "Hello";
String[] sa = {s};
test(sa);
通过test,我们改掉了sa[0],但当然改不掉s。那么,借助这个sa,我们何来“传址”可言呢?传谁的址?
luoqt 2006-06-08
  • 打赏
  • 举报
回复
mark
Hongyu6 2006-06-08
  • 打赏
  • 举报
回复
学习!
晨星 2006-06-08
  • 打赏
  • 举报
回复
靠,怎么没写完就发上去了。。。。。。

续:
……但据我子解,基于引用计数的垃圾回收,由于存在性能等诸方面的问题,在JVM和.net CLR的GC中均未被采用。

《Applied Microsoft .Net Framework Programming》一书中比较详细的阐释了.net CLR的GC实现,没有基于“引用计数”技术。
qingyuan18 2006-06-08
  • 打赏
  • 举报
回复
好经典的问题,已经讨论过多次了:)
个人认为java是传的引用的拷贝!
晨星 2006-06-08
  • 打赏
  • 举报
回复
lxpws(老烦) ( ) 信誉:100 2006-6-7 14:08:04 得分: 0
JAVA在参数传递过程中到底作了什么?
-----------------------
考虑到垃圾回收,我想在传递对象(称其为对象的引用可能更贴切些)给函数时,应该有一个类似于AddRef的调用,同时在函数调用完毕后,应该有一个Release的动作。如果对象的引用也是个一对象的话,它的行为应该类似于COM中的智能指针;如果它仅仅是一个地址值的话,那AddRef/Release的调用就要由编译器来代劳了。
-----------------------

“引用计数”的确在COM技术中起了关键作用,但据我子解,基于引用计数的垃圾回收,由于存在性能等诸方面的问题,在JVM和.net CLR



晨星 2006-06-08
  • 打赏
  • 举报
回复
哦,是的,说“天天”不对,呵呵,应该是尽量避免,尽量用返回值才对。
其实我只是想表达“一点也不稀奇”这样一个意思。:P
nighthawk 2006-06-08
  • 打赏
  • 举报
回复
至于为什么,您可以参考的Martin Fowler在Refactorings当中的Remove Assignments to Parameters :)
nighthawk 2006-06-08
  • 打赏
  • 举报
回复
steedhorse(晨星) :总结的不错。
不过对于7条中的最后一条:
“只要用JAVA编程,我们实际上天天都在通过修改分量或调用方法来改变对象(包括一般的对象和数组)”
个人不完全赞同。在遇到需要通过修改分量或调用方法来改变对象,我往往以返回值代替,这样会使得我的头脑更清晰。
sundeveloper 2006-06-08
  • 打赏
  • 举报
回复
mark
晨星 2006-06-08
  • 打赏
  • 举报
回复
// ps: (五级(中级)),复制回复人那一行文字,贴上就有了。

您用的是什么浏览器啊?怎么偶只能得到:
guozhang() ( ) 信誉:100 2006-06-08 10:35:00 得分: 0
:(
guozhang 2006-06-08
  • 打赏
  • 举报
回复
格式不好。 我用语言描述一下:
sa指向一个String数组对象。这个数组中的每个元素都是指向某个String的引用(或说是指针)。
对于给定的例子: sa[0]和s指向了同一个对象"Hello"。
guozhang 2006-06-08
  • 打赏
  • 举报
回复
说说我对下面代码行为的理解:
String s = "Hello";
String[] sa = {s}; // sa[0]获得了s中保存的值,即"Hello"对象的引用。
// 因为这一语是引用,所以地址链不包括s
// 所以,s是否改变不能证明我的对错。


s是一个引用,sa是一个引用,sa中的每个元素是引用。引用关系如下:
-------------
sa--->| sa[0] | ------>"Hello"
------------- /
/
s

ps: (五级(中级)),复制回复人那一行文字,贴上就有了。
UnAgain 2006-06-08
  • 打赏
  • 举报
回复
To:steedhorse(晨星)
你有短消息。
晨星 2006-06-08
  • 打赏
  • 举报
回复
再来了“后记”吧。:P

关于这些问题,如果学习一下C#语言,我觉得会有更清晰的认识,C#中不仅除string之外的大多数内建类型都不是“引用”(C#中的string类型是语言词法内建的),就连用户自定义的struct也不是引用,只有class才是引用类型。C#中更明确地提出了“引用类型”与“值类型”的概念,区分它们是当然学习中必须搞清楚的问题之一。
另外在标准C语言(不是C#)的specification中,包括int,float,结构体等所有变量实体都常常被称为“object”(这显然是个术语上的差别)。从这种术语上,一个类型是值类型还是引用类型就看是否“直接”代表那个“object”,从底层实现上具体来看,可能就是“是否整个对象都排布在栈上”。
cuiweibing 2006-06-08
  • 打赏
  • 举报
回复
mark
晨星 2006-06-08
  • 打赏
  • 举报
回复
我也来做一个总结吧,我觉得我们主要讨论了如下问题:
(1) 在JAVA中,一个内建类型的变量直接代表那个值本身,而一个类类型(或数组类型)的变量则是一个引用,间接地代表着它所引用的那个对像;
(2) 不管是引用类型还是内建内型的变量,就变量本身而言,做参数传递时,都是值传递的语义——你永远无法改变传递的变量本身。
(3) 因为(2),在方法内部对形参值的直接修改不影响实参;
(4) 但引用类型由于本身引用着一个实际的对象,因此可以通过它来修改对象的分量而且使得这种改变对方法外部的实参可见;
(5) 第(4)条所说的“分量”既包括数组的元素,也包括类的字段;
(6) 由于(4),(5),通过数组或类的包装,可以间接实现让方法内对象的改变影响到方法外部。
(7) 从(6),我发现我们绕到了最简单的问题上去了——只要用JAVA编程,我们实际上天天都在通过修改分量或调用方法来改变对象(包括一般的对象和数组)。
晨星 2006-06-08
  • 打赏
  • 举报
回复
是啊,我也一直很奇怪,似乎有些人用传引用的方式就能得到“(五级(中级))”的字样,而我要想得到,只能用传值的方式。^0^
晨星 2006-06-08
  • 打赏
  • 举报
回复
// 因为这一语是引用,所以地址链不包括s
// 所以,s是否改变不能证明我的对错。

我明白你的意思。但我的意思是:在我的例子中,从参数传递的意义上,唯一一个似乎还可以讨论“其地址”的东西就是s了,所以既然s都不是(正如你所说),那么我就不知道我们所说的“传值”到底是在指“谁的址”?难道是sa[0]的地址?sa[0]我们不也是讨论过了么?它也是相当于一个分量。
没错,通过这一方式达到了“不需要额外创建类”的效果,但我们前面不也讨论过了吗?一个数组相当于一个对象,那么对于类型T而言,“T[]”其实也就相当于一个“类”。当然,如你所说,这个“类”不是“额外”创建的,但就我们讨论的问题而言,这能构成实质性我区别么?

我承认这段话有些较真的味道,因为我觉得我们都明白彼此的意思的。:P

总结一下也好,其实我觉得大部分能说也都说了。
至于我今天凌晨发的那几个回复,是有点把问题说复杂了,而且还把问题绕回了原地。
我想表达的意思完全可以这样概括:“数组能包装对象,类同样也能,所以您后面所讨论的用数组模拟传值的问题实质上完全等价于我们前面一直在讨论的传值和传引用的问题,因此我们没有必要同时讨论两个,讨论清楚了一个,另一个也等于解决了。”(它们的区别可能仅仅是您所说的,利用数组来包装,可以避免写一个“额外”的类,但我还是不认为这会影响我们所讨论问题的实质)。
UnAgain 2006-06-08
  • 打赏
  • 举报
回复
To: guozhang()

如上所述。

另,“(五级(中级))”是怎么来的?直接拷贝的还是你手工写的?如果直接拷贝得来的,你用的是什么工具?
加载更多回复(80)

62,614

社区成员

发帖
与我相关
我的任务
社区描述
Java 2 Standard Edition
社区管理员
  • Java SE
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧