总结java方法(函数)传值和传引用的问题

转自:http://hi.baidu.com/cpuramdisk/item/84a75b5e78007013da163535


【yasi】关于“Java方法中传进去一个对象,为什么不能改变这个对象”的问题,在网上搜了很多中英文的资料,就下面这篇帖子讲的最清楚透彻。果断收录。


java方法中传值和传引用的问题是个基本问题,但是也有很多人一时弄不清。

(一)基本数据类型:传值,方法不会改变实参的值。

public class TestFun {

public static void testInt(int i){
   i=5;   


public static void main(String[] args) {
   int a=0 ;  
   TestFun.testInt(a);
   System.out.println("a="+a);  
}

}

程序执行结果:a=0 。

(二)对象类型参数:传引用,方法体内改变形参引用,不会改变实参的引用,但有可能改变实参对象的属性值。


======【yasi】这里加入对此的个人理解======


这里说“对于对象类型参数,传入的是引用”,不妥。个人觉得这里传入的仍然是值,只不过是“对象引用的副本”,即把对象引用的值,而不是对象引用的引用传进去了。这样说太绕了,下面举个例子。

public void Foo(String s) {
    s = new String("Hello");
}

在函数外面new一个String:String s1 = new String("World"),然后执行 Foo(s1),然后输出s1的值是怎样的,我们都知道,当然还是“World”。但是既然传入的是引用,而不是值,那为什么执行完 Foo(s1) 之后,s1 的值没有变呢?我们再看下面这个C++的例子,从中寻找答案。

#include <iostream>

using namespace std;

class CObj {
public:
        CObj() {x = 0;}
        CObj(int x) {this->x = x;}

public:
        int x;
};

void Foo1(CObj * pObj) {
        pObj = new CObj(200);
}

void Foo2(CObj * &pObj) {
        pObj = new CObj(200);
}

int main(int argc, char * argv[]) {
        CObj * pObj1 = new CObj(100);
        cout << pObj1->x << endl;

        Foo1(pObj1);
        cout << pObj1->x << endl;

        Foo2(pObj1);
        cout << pObj1->x << endl;

        return 0;
}

运行结果如下:

100
100
200

这里的 Foo1() 函数就类似上面 Java 代码中的 Foo() 函数。Java中所谓的引用,可以理解成堆上生成的真正对象的地址,这里用 “C++中的指针”、“地址”、或是“引用”来表示都可以,总之,它本身不是对象的“肉身”,只是一个能找到“肉身”的标识。请注意,这样的一个标识,也有它的值,它的值就是它指向的对象肉身的地址。

C++代码中的 Foo1() 函数,传入的 pObj1 是一个标识的值,即 pObj1 虽然本身是一个指针,但是Foo1(pOjb1) 中,pObj1 是以“值传递”的方式传入的,说白了,就是真正传给了 Foo1() 函数的,是 pObj1 的一个副本,就相当于下面这样:

void Foo1(CObj * pObj) {
	CObj * pTmp = pObj;
	pTmp = new CObj(200);
}
可见,Foo1() 函数外面的 pObj1在Foo() 函数里是不会被改变的。这就说明了为什么下面的代码不能改变传入的String引用了。
public void Foo(String s) {
	s = new String("Hello");
}
其实,打个比方,这段代码就相当于(当然不是很贴切,因为s本身就已经是一个local变量了):
public void Foo(String s) {
	String tmp = s;
	tmp = new String("Hello");
}

相比之下,C++代码中的 Foo2() 函数,就能改变传入的 pObj 对象本身,因为传入的指针本身被重置了!

void Foo2(CObj * &pObj) {
        pObj = new CObj(200);
}


上面各种方式的比较:


C++的传入引用貌似方便,但是常常要看到函数实现的内部,才知道传入的参数究竟在函数中是只读使用的,还是有写操作的。相比之下,Java基本上就是通过函数返回值传出结果,比C++要清晰的多。因此,在用Java时,尽量不要像C++的方式靠,而应尽量利用函数的返回值。比如,如果要同时返回两个String则可以返回一个List<String>;又比如,如果要将字符串“Jack_London_1234”中的字符串"London"和数字1234识别并返回,可以返回一个List<Object>,第一个元素是String "London",第二个元素是Integer 1234,然后分别做类型转换,转换成String类型和Integer类型。


PS


如果String类提供一个函数,比如void assign(String input),该函数将input的内容赋给当前String对象(注意是堆上的对象实体),那么像下面这样的函数也是可以通过s参数返回“hello”的。之所以String类没有提供这样的函数,可能就是编写者不希望使用者像C++那样去用参数吧。

public void Foo3(String s) {
	s.assign("hello");
}

==================


举两个例子:

(1)方法体内改变形参引用,但不会改变实参引用 ,实参值不变。

public class TestFun2 {

public static void testStr(String str){
   str="hello";//型参指向字符串 “hello”   


public static void main(String[] args) {
   String s="1" ;  
   TestFun2.testStr(s);
   System.out.println("s="+s); //实参s引用没变,值也不变 
}
}

执行结果打印:s=1

(2)方法体内,通过引用改变了实际参数对象的内容,注意是“内容”,引用还是不变的。

import java.util.HashMap;
import java.util.Map;

public class TestFun3 {

public static void testMap(Map map){
   map.put("key2","value2");//通过引用,改变了实参的内容  


public static void main(String[] args) {
   Map map = new HashMap(); 
   map.put("key1", "value1");
   new TestFun3().testMap(map);
   System.out.println("map size:"+map.size()); //map内容变化了 
}
}

执行结果,打印:map size:2 。可见在方法testMap()内改变了实参的内容。

(3)第二个例子是拿map举例的,还有经常涉及的是 StringBuffer :

public class TestFun4 {

public static void testStringBuffer(StringBuffer sb){
   sb.append("java");//改变了实参的内容 


public static void main(String[] args) {
   StringBuffer sb= new StringBuffer("my "); 
   new TestFun4().testStringBuffer(sb);
   System.out.println("sb="+sb.toString());//内容变化了 
}
}

执行结果,打印:sb=my java 。

所以比较参数是String和StringBuffer 的两个例子就会理解什么是“改变实参对象内容”了。

总结

第一:java方法基本数据类型是传值,对象类型传引用,这是千真万确的。

第二:当参数是对象时,无论方法体内进行了何种操作,都不会改变实参对象的引用。

第三:当参数是对象时,只有在方法内部改变了对象的内容时,才会改变实参对象内容。

 

java程序的函数调用到底是传值呢还是传参呢?这可是个难缠的问题,如果搞不清楚还是挺容易出错的:P对于这个问题,最经典的解释莫过于“java函数是传值的,java函数传递的参数是对象的引用” 

这两句话好像初听上去有点绕,不过意思倒是表达得蛮精确的。我看到过几个解释这个问题的例子,不过个人感觉看过例子之后还是只知道是什么不知道为什么,停留在照猫画虎的水平上还是挺容易出问题的。所以举例子之前,先从jvm的实现原理上有个了解应当是不无裨益的。jvm的结构图前一阵子贴到blog上了,那可是从“深入java虚拟机”这本巨牛的书上看来的,绝对有权威性。从jvm的结构图上可以看出来,jvm在实现的时候将属于它的内存分为五部分,其中程序代码(严格的说应当是字节码)是放在java栈的栈帧中,而对象是从堆中分配的,堆这个东西我看可以理解成“对象池”。程序和程序中需要用到的对象放在两个相对独立的区域中,那么程序怎么使用对象呢?答案是程序中真正使用对象的地方其实只是声明了一个对象的引用,也就是把堆中分配了的相应对象的地址放到引用中,栈和堆之间就是通过一个一个的引用来联系的。引用嘛,我理解就是一个指针常量,指针常量又是个什么东西呢?说白了,就是一个无符号整数,这个整数所表达的是引用对象的地址。好了,这下清楚了,不管是基本类型变量(int,float,double什么的)还是对象,相应的内存地址中存放的都是一个数(无符号整数,整数,浮点数等)。传递参数的时候传递的就是相应内存地址中的数,所以说“java函数是传值的”。当然,这个数对于基本类型和对象类型来说意义是不一样的,对于基本类型这个数就是其值本身,传递值的结果就是,改变新的变量的值不影响旧的变量的值;而对于对象来说这个数是它的地址,传递这个值就相当于传递了真实对象的引用,传递了引用或者说是地址的结果就是变化会全局可见,所以又可以说“java函数传递的参数是对象的引用”。唔,松口气啦。经过上面这一小堆讨论,不难理解为什么java在传递参数时对于基本类型和对象表现不同:)现在开始举例了,举网上搜来的例子,看看是不是比原来没有上面的解释的时候好理解一点?

public class TestRef 

{
 public static void main(String[] args)
 {
  ValueObject vo1 = new ValueObject("A", 1);
  System.out.println("after vo1: " + vo1.getName()); //=A
  
  changeValue1(vo1);
  System.out.println("after changeValue1: " + vo1.getName());
                //=A1, changed
  
  changeValue2(vo1);
  System.out.println("after changeValue2: " + vo1.getName()); 
                //=A1, changeValue2内部的赋值不会影响这里。
 }
 /**
  * 使用vo1自身的函数对其内部数据进行改变是有效的,函数外可反映出来,  * 因为这是对对象本身的操作
  * 这种object称为可变的(mutable)
  * @param vo1
  */
 private static void changeValue1(ValueObject vo1) 
 {
  vo1.setName("A1");
 }
 /**
  * 在函数内给vo1重新赋值不会改变函数外的原始值,因为这种改变了引用的指向
  * @param vo1
  */
 private static void changeValue2(ValueObject vo1) 
 {
  vo1 = new ValueObject("B", 2);
  System.out.println("inside changeValue2: "+ vo1.getName());
                //=B,赋值操作引起的结果变化仅在changeValue2内部有效
 }
}
class ValueObject 
{
 
 public ValueObject() {}
 
 public ValueObject(String name, int id)
 {
  this.name = name;
  this.id = id;
 }
 
 private String name;
 private int id;
 public int getId() {
  return id;
 }
 public void setId(int id) {
  this.id = id;
 }
 public String getName() {
  return name;
 }
 public void setName(String name) {
  this.name = name;
 }
}
java中对象的每个实例(就是对象)内存地址是唯一的,它一旦被创建,能够对这个地址进行操作的就是其本身,如果ValueObject类中没有public void setName之类的方法对这个类的实例中的数据进行修改的话,程序是没有任何别的方法可以修改ValueObject类的实例中的数据,这个就是java的封装特性。对于不提供修改内部数据的方法的类,我们称为不可变(immutable)的类。在函数中对传入的参数变量进行赋值操作,只能在函数范围内改变局部变量指向的引用地址,但是不会改变原始地址的内容。因此,在changeValue2(...)函数内部的vo1和函数外的vo1虽然名字相同,但是实际上是不同的实例变量,只不过指向了和函数外的vo1同样的地址,所以当我们用vo1=... 对其进行赋值的时候,只不过是把函数内的临时变量指向了新的地址,并没有改变原始vo1内存地址中的内容。这就是在运行changeValue2(...)之后,vo1的值在main范围内仍然没有被修改的原因。而changeValue1里面是调用的ValueObject本身的function来更改其内容,因此是原始内存地址中的数据被更改了,所以是全局有效的.总结:
对于引用类型的传参也是传值的,传的是引用类型的值,其实就是对象的地址。 
1. java参数传递值的。 
2. java所有对像变量都是对像的引用。
不知道我解释清楚没,要是还是不能理解java参数传递的原理,强烈推荐看看“深入java虚拟机”一书的第五章,中文翻译的不是特别出色,可以看看英文原版,应该比中文版好懂:)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值