免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
123下一页
最近访问板块 发新帖
查看: 12009 | 回复: 23
打印 上一主题 下一主题

java经典问题:传值还是传引用 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2006-03-17 19:03 |只看该作者 |倒序浏览
经典的问题,但却不容易弄懂,尤其对有c基础的java程序员来说,更容易引起混乱,这里我试图简单点描述。

“java函数是传值的,java函数传递的参数是对象的引用”

这两句话好像初听上去有些矛盾,但却是事实,因而引起很多初学者的混乱。在这里我试图据个简单的例子来说明java的这个特性,可能不全面,希望大家来补全。


  1. public class TestRef {
  2.        
  3.         public static void main(String[] args)
  4.         {
  5.                 ValueObject vo1 = new ValueObject("A", 1);
  6.                 System.out.println("after vo1: " + vo1.getName()); //=A
  7.                
  8.                 changeValue1(vo1);
  9.                 System.out.println("after changeValue1: " + vo1.getName());
  10.                 //=A1, changed
  11.                
  12.                 changeValue2(vo1);
  13.                 System.out.println("after changeValue2: " + vo1.getName());
  14.                 //=A1, changeValue2内部的赋值不会影响这里。
  15.         }

  16.         /**
  17.          * 使用vo1自身的函数对其内部数据进行改变是有效的,函数外可反映出来
  18.          * 这种object称为可变的(mutable)
  19.          * @param vo1
  20.          */
  21.         private static void changeValue1(ValueObject vo1) {
  22.                 vo1.setName("A1");
  23.         }

  24.         /**
  25.          * 在函数内给vo1重新赋值不会改变函数外的原始值
  26.          * @param vo1
  27.          */
  28.         private static void changeValue2(ValueObject vo1) {
  29.                 vo1 = new ValueObject("B", 2);
  30.                 System.out.println("inside changeValue2: "+ vo1.getName());
  31.                 //=B,赋值操作引起的结果变化仅在changeValue2内部有效
  32.         }
  33. }

  34. class ValueObject {
  35.        
  36.         public ValueObject() {}
  37.        
  38.         public ValueObject(String name, int id)
  39.         {
  40.                 this.name = name;
  41.                 this.id = id;
  42.         }
  43.        
  44.         private String name;
  45.         private int id;
  46.         public int getId() {
  47.                 return id;
  48.         }
  49.         public void setId(int id) {
  50.                 this.id = id;
  51.         }
  52.         public String getName() {
  53.                 return name;
  54.         }
  55.         public void setName(String name) {
  56.                 this.name = name;
  57.         }
  58. }
复制代码


解释,vo1作为一个object,当它被用作函数参数的时候传递给函数的是一个引用值,这个名称有点怪,又有引用又有值,到底是引用还是值呢,就看你怎么理解了。如果你是去考试,官方的答案是值。可是看起来又象引用啊,希望从这个例子,你能理解java参数传递和和C/C++程序中的引用传递的不同的地方。另外,这也是java作为OO语言的特性之一:封装的体现。

先讲一下对象赋值的关系,举例来说,下列代码:

ValueObject v2, v3;
v2 = new ValueObject("C", 3); 粗体的部分创建了一个数据结构,假设存放在内存地址A000,赋值给句柄 v2
v3 = new ValueObject("D", 4); 粗体的部分创建了一个数据结构,假设存放在内存地址B000,赋值给句柄 v3
v2 = v3; 这句话的作用是把操作B000的地址的句柄的值付给了v2的句柄,使得v2和v3一样操作B000的地址,这意味着:
1.原来v2指向的地址A000变成无主的内存地址,将自动被jvm回收。
2.既然v2和v3指向同一片地址,对v3的修改v2也能得到,反之亦然。

整理得下列代码,请感兴趣的朋友运行验证
ValueObject v2 = new ValueObject("C", 3);
ValueObject v3 = new ValueObject("D", 4);
v2 = v3;
System.out.println("after v2=v3");
System.out.println("v2= "+ v2.getName());//=D
System.out.println("v3= "+ v3.getName());//=D
v3.setName("C1");
System.out.println("after v3 setnameTo C1");
System.out.println("vo2= "+ v2.getName());//=C1
System.out.println("vo3= "+ v3.getName());//=C1

因此,可以得出结论,java中对象的每个实例(instance, 比如vo1, v2, v3 都是ValueObject的实例)的内存地址是唯一的,它一旦被创建,能够对这个地址进行操作的就是每个实例自己,如果ValueObject类中没有public void setName之类的方法对这个类的实例中的数据进行修改的话,程序是没有任何别的方法可以修改ValueObject类的实例中的数据,这个就是java的封装特性。对于不提供修改内部数据的方法的类,我们称为不可变(immutable)的类。在函数中对传入的参数变量进行赋值操作,只能在函数范围内改变局部变量指向的引用地址,但是不会改变原始地址的内容。因此,在changeValue2(...)函数内部的vo1和函数外的vo1虽然名字相同,但是实际上是不同的实例变量,只不过指向了和函数外的vo1同样的地址,所以当我们用vo1=... 对其进行赋值的时候,只不过是把函数内的临时变量指向了新的地址,并没有改变原始vo1内存地址中的内容。这就是在运行changeValue2(...)之后,vo1的值在main范围内仍然没有被修改的原因。而changeValue1里面是调用的ValueObject本身的function来更改其内容,因此是原始内存地址中的数据被更改了,所以是全局有效的。

[ 本帖最后由 perryhg 于 2006-3-18 00:50 编辑 ]

论坛徽章:
0
2 [报告]
发表于 2006-03-17 22:01 |只看该作者
原帖由 perryhg 于 2006-3-17 20:03 发表
经典的问题,但却不容易弄懂,尤其对有c基础的java程序员来说,更容易引起混乱,这里我试图简单点描述。

“java函数是传值的,java传递的参数是对象的引用”

这两句话好像初听上去有些矛盾,但却是事实,因 ...


说的好~版猪辛苦:em11:

论坛徽章:
0
3 [报告]
发表于 2006-03-18 15:39 |只看该作者
原帖由 perryhg 于 2006-3-17 19:03 发表
经典的问题,但却不容易弄懂,尤其对有c基础的java程序员来说,更容易引起混乱


不会吧,用了好几年C了,而且也写过C编译器,感觉应该算是有C基础了。
但我觉得更容易理解java中这种参数传递的本质啊。
所有的地方都是传值,无它。而且这也不会引起混乱。

论坛徽章:
0
4 [报告]
发表于 2006-03-18 22:36 |只看该作者
实际上可以这样理解,java的函数还是传值的。
changeValue1(vo1)的调用,实际上是把main里面的vo1变量(句柄)的值(该值实际上又是vo1代表的对象的指针)传递给changeValue函数的局部变量。

传值是针对变量的,不能混淆变量本身和变量的值所代表的是引用还是值。

论坛徽章:
0
5 [报告]
发表于 2006-03-19 10:10 |只看该作者

我也说:传值还是引用

只知道理解是否正确,请大家指教阿!
看了 《practical java》今天把这个问题搞清楚了,在这里备忘一下!

大家都认为java的引参是by reference来传递的,这个看法是错误的。引参是其实是通过by value来传递的。产生这个错误的原因很简单,因为java的对象的引用都是通过by referencr的。下面举例说明一下。

import java.awt.Point;

class PassByValue{

public static  void modifyValue(Point pt,int j) {//pt,j为引参

pt.setLocaltion(5,5);       //      1

j=15;

System.out.println("During modifyPoint" + "pt="+pt+"and j = "+j);

}

public static void main(String [] args){

Point p = new Point();     //        2

int i = 10;

System.out.println(Before modifyPoint" + "pt="+pt+"and j = "+j);

modifyValue(p , i );          //         3

System.out.println(After modifyPoint" + "pt="+pt+"and j = "+j);

}

}

上面的这个例子在2处,生成了一个Point的对象,并将其赋值给一个对象的引用 变量 p。生成了一个int i=10。在3这个地方,通过调用modifyValue(),传入引参,p和i。对第一个参数pt调用setLocation(5,5)这个方法,然后把第二个参数 j 赋值为15。modifyValue()返回后,打印最后的结果。

如下:

   Before modifyPoint p=java.awt.Point[x=0,y=0] and i=10;

   During modifyPoint p=java.awt.Point[x=5,y=5] and i=15;

   After   modifyPoint p=java.awt.Point[x=5,y=5] and i=10;

从结果中大家可以看到,modifyPoint()修改了//2 里面建立的对象Point,却没有改变i的值。在main中,i 被赋值为10,用于引参是 通过by value 来传递的,所以modifyPoint()收到的是一个i的副本,然后将这个副本改成15,并返回。main里面的i并没有受到影响。
对比之下,你可能也认为//2中建立的Point对象也没有被modifyPoint()修改。毕竟java是通过by value来传递引参的。于是乎,在调用modifyPoint()并传入//2中建立的Point对象的时候,会像上面一样产生一个point对象的“复制品”来配合modifyPoint()的工作。因此在modifyPoint()里面对Point对象的复制品的修改是不会影响到main后面的,应为Point的对象和Point对象在modifyValue里面不是同一个对象嘛! 对不对呢? 答案是 错!!!!
    事实上modifyPoint是在和Point的对象的reference的复制品(引用复制品)打交道,而不是和Point的对象打交道。注意了p仅仅是一个引用而已,并且java是以by value的方式来传递引参的。更准确的说,java是通过by value来传递对象的引用的。当p从mian()里面传到modifyValue()的时候传递的是一个p(注意:p只是引用而已一个)的复制品pt,所以modifyPoint()是在和同一个对象打交道,因为pt,是p的一个copy,但是pt和p有相同的一个地址,这个地址指向的是同一个Point对象。所以,对pt的操作会改动Point的对象,也就出现了第三个结果

论坛徽章:
0
6 [报告]
发表于 2006-03-19 12:33 |只看该作者

应当这样来思考

//其实你这个问题很简单,你和程序应当这样写。里边好多错误你难道没有看也来吗?
//关于JAVA里的传值还是传引用其实是一个很简单的问题。
//任何一个方法的传进去的参数都分为两种情况,一是传进去一个对象的形式,二是传进去是基本数据
//的形式。如果方法参数传进去的是对象比如public void function(Object one)那这个one 就是把一个one的
//引用传进去了,也就是我们以前讲的reference.他就是一根拉着对象的风筝线,不可断的。如果是第二
//种情况public void function(int one)那传进去的就是一个单单的一个基本数据了,你对这个数据的任何变化不会对原来那个基本数据
//有什么改动。
//就象你在程序里写这样一段
// int i = 0;
// int j = i;
// j = 5;
// i 是不会变成5的。
//而如果换了对象
// Point p = new Point();
// Point pt = p;
// p 和pt 就会指向了同一个Point();他们二者都是那根绳子,一有变化就风筝就会动了,
// ====================================================================
import java.awt.Point;

public class PassByValue{

        public static  void modifyValue(Point pt,int j)
        {//pt,j为引参
                pt.setLocation(5,5);       //      1
                j=15;
                System.out.println("During modifyPoint" + "pt="+pt+"and j = "+j);
        }
        public static void main(String [] args)
        {
                Point p = new Point();     //        2
                int i = 10;
                System.out.println("Before modifyPoint" + "pt="+p+"and j = "+i);
                modifyValue(p , i );          //         3
                System.out.println("After modifyPoint" + "pt="+p+"and j = "+i);
        }
}

论坛徽章:
0
7 [报告]
发表于 2006-03-19 16:33 |只看该作者
//就象你在程序里写这样一段
// int i = 0;
// int j = i;
// j = 5;
// i 是不会变成5的。
//而如果换了对象
// Point p = new Point();
// Point pt = p;
// p 和pt 就会指向了同一个Point();他们二者都是那根绳子,一有变化就风筝就会动了,


好多地方都举这个例子,但是我觉得这个例子不好,我当初还以为是原始数据类型int和对象类型Object的区别呢,后来才了解这是mutable和 immutable的区别,才写了个不同的例子

[ 本帖最后由 perryhg 于 2006-3-20 14:33 编辑 ]

论坛徽章:
0
8 [报告]
发表于 2006-03-20 09:36 |只看该作者

值传递还是引用传递,我就这样理解了

呵呵,看过两本书,写的也不一样,但是对此产生的结果都能认识到,看来大家理解的方式是不一样的。
perryhg说尤其对有c基础的java程序员来说,更容易引起混乱,我猜是漏了一个字,应该是没有C基础的吧。

原帖由 hncdyjh 于 2006-3-18 22:36 发表
传值是针对变量的,不能混淆变量本身和变量的值所代表的是引用还是值。

kakasi (卡卡西せんせい)在以前说的好:
1.java参数传递值的。
2.java所有对像变量都是对像的引用。

这两个说法我认为很是经典。

我是这么理解的:
变量、变量的值和引用的概念在有些情况下有些可以互相替代,但却有完会不同的意义,很难让人搞清楚。
如多C书上都有如下这样的图,不知道这样理解好不好?
  1. 比如有两个变量
  2. byte    a=16;
  3. String str="he";
  4. 第一列是内存地址,第二列是内存的内容,第三列是对应的变量。
  5. 0000:16 - a
  6. 0001:3  - str
  7. 0002:
  8. 0003:h
  9. 0004:e
  10. 0005:k
  11. 0006:k
  12. 不知道地址可不可以这样编,字符串对像也不可能是这样存储,应该能说明意思吧,
  13. 也不知道java是不是这样的的意思,我是这样理解的。
复制代码

可以看出,a是一般变量,它的值就是16,
str是对像变量,它的值是“he"对像的地址3,所是说它是对像的引用。
当做参数传递时,都是传的它们的拷贝,也就是传值,改变它们的值不影响原来值。
如:
(1)str2=str ;str2="kkk" ;//str2值为3;然后赋值为kk的地址。strt2的值改变了不影响str
(2)str2=str ;str2.replace('h','k');str2值为3;改变了引用的值。但只是改变了str2和str共同的引用的对像的值。


=(等号)操作的是变量的值。
.(句点)操作的是引用的值。

我想这样认为.操作符就应该清楚对像改变了而不是因为参数是引用传递的吧

[ 本帖最后由 只爱一点点 于 2006-3-20 13:21 编辑 ]

论坛徽章:
0
9 [报告]
发表于 2006-03-20 12:00 |只看该作者
师傅领进门,修行在个人;
静下心来冥思一下,相信很快就会豁然开朗,大彻大悟

支持总结,支持引用:em11:~~~

论坛徽章:
0
10 [报告]
发表于 2006-03-20 12:55 |只看该作者

关于传参的问题只要把栈和堆搞明白了就应该很容易理解了

java传参都是传值,也就是把栈里面的值复制一份传给参数
当然如果是原始类型就是栈内原始类型值复制了一份传递
如果是对象或者数组这样的引用类型,那么栈里面的值就是该对象的引用的值具体说就是地址了,把这个值复制一份传给方法,也就是复制了一个引用给方法去使用。
例如:
public void func1(int a){}
那么int i = 10;
func1(i);的时候,实际上是作了a=i的操作
而对象
public void fun2(Object o){}
那么Object m = new Object();
func2(m);的时候,也其实是作了o=m的操作

这样就比较容易理解了吧
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP