下一个问题: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)这种情况例外,因为它实际上被晃猄ystem.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规则》)
1、对象是按引用传递的
2、Java 应用程序有且仅有的一种参数传递机制,即按值传递
3、按值传递意味着当将一个参数传递给一个函数时,函数接收的是原始值的一个副本
4、按引用传递意味着当将一个参数传递给一个函数时,函数接收的是原始值的内存地址,而不是值的副本
三句话总结一下:
1.对象就是传引用
2.原始类型就是传值
3.String类型因为没有提供自身修改的函数,每次操作都是新生成一个String对象,所以要特殊对待。可以认为是传值。
==========================================================================
public class Test03 { public static void stringUpd(String str) { str = str.replace("j", "l"); System.out.println(str); } public static void stringBufferUpd(StringBuffer bf) { bf.append("c"); System.out.println(bf); } public static void main(String[] args) { /** * 對於基本類型和字符串(特殊)是傳值 * * 輸出lava,java */ String s1 = new String("java"); stringUpd(s1); System.out.println(s1); /** * 對於對象而言,傳的是引用,而引用指向的是同一個對象 * * 輸出javac,javac */ StringBuffer bb = new StringBuffer("java"); stringBufferUpd(bb); System.out.println(bb); } } |
解析:就像光到底是波还是粒子的问题一样众说纷纭,对于Java参数是传值还是传引用的问题,也有很多错误的理解和认识。我们首先要搞清楚一点就是:不管Java参数的类型是什么,一律传递参数的副本。对此,thinking in Java一书给出的经典解释是When you’re passing primitives into a method, you get a distinct copy of the primitive. When you’re passing a reference into a method, you get a copy of the reference.(如果Java是传值,那么传递的是值的副本;如果Java是传引用,那么传递的是引用的副本。)
在Java中,变量分为以下两类:
① 对于基本类型变量(int、long、double、float、byte、boolean、char),Java是传值的副本。(这里Java和C++相同)
② 对于一切对象型变量,Java都是传引用的副本。其实传引用副本的实质就是复制指向地址的指针,只不过Java不像C++中有显著的*和&符号。(这里Java和C++不同,在C++中,当参数是引用类型时,传递的是真实引用而不是引用副本)
需要注意的是:String类型也是对象型变量,所以它必然是传引用副本。不要因为String在Java里面非常易于使用,而且不需要new,就被蒙蔽而把String当做基本变量类型。只不过String是一个非可变类,使得其传值还是传引用显得没什么区别。
对基本类型而言,传值就是把自己复制一份传递,即使自己的副本变了,自己也不变。而对于对象类型而言,它传的引用副本(类似于C++中的指针)指向自己的地址,而不是自己实际值的副本。为什么要这么做呢?因为对象类型是放在堆里的,一方面,速度相对于基本类型比较慢,另一方面,对象类型本身比较大,如果采用重新复制对象值的办法,浪费内存且速度又慢。就像你要张三(张三相当于函数)打开仓库并检查库里面的货物(仓库相当于地址),有必要新建一座仓库(并放入相同货物)给张三么? 没有必要,你只需要把钥匙(引用)复制一把寄给张三就可以了,张三会拿备用钥匙(引用副本,但是有时效性,函数结束,钥匙销毁)打开仓库。
在这里提一下,很多经典书籍包括thinking in Java都是这样解释的:“不管是基本类型还是对象类型,都是传值。”这种说法也不能算错,因为它们把引用副本也当做是一种“值”。但是笔者认为:传值和传引用本来就是两个不同的内容,没必要把两者弄在一起,弄在一起反而更不易理解。