开篇:
Java的函数参数传递实际上不存在所谓的按引用传递和按值传递,说白了都是按值传递。
Java基本类型可以分为三类:
字符类型char,布尔类型boolean以及数值类型byte、short、int、long、float、double。数值类型又可以分为整数类型byte、short、int、long和浮点数类型float、double。
理论解释:
Java中函数参数传递时,作为基本类型,是直接产生一个拷贝值作为内部参数使用的。这个拷贝的值,只有在值的层面上与原来的输入参数有相同,在内存地址上已经不再指向原来的参数地址了,而是和拷贝出来的值相关联了,这点可以从普通变量的定义来理解。因此在函数内部修改这个拷贝的值,并不会影响原来输入参数的值,即不会动到那块内存。
作为对象引用类型,也是产生一个拷贝值,但是这个拷贝值是输入参数引用的拷贝值,是一个内存地址,这个拷贝值与输入参数的引用指向的内存地址是一个数值,也就是同一块内存地址。既然是同一块内存地址,名义上是产生了一个引用的拷贝值,事实上指向的是同一块内存地址,那么修改这个拷贝的引用指向的内存地址的数据,当然也就是直接修改了原来的引用指向的内存的数据,从而原来的数据也被修改,但是其引用本身没有被修改,所以一般如果不想改变输入参数用final修饰保证编译器能检查出意外修改输入参数的问题。
结果是引用没有被修改,但是引用的数据被修改。
根据编译器的优化原理,一般情况下,对于输入参数,编译器只需要一个整数就可以满足传递参数的目的,这个整数是一个内存地址。当是基本类型时,由于代价很小,就是直接复制一份参数作为内部参数使用。而对于对象引用,则只需要复制一份对象引用,既然是复制了对象引用,那么此时引用指向的内存地址(对象)是一样的,存储这个引用本身的内存地址则是不同的(变量自身需要内存分配才能产生变量,以定义该变量)。
因此,java中参数传递都是按所谓的“按值”传递,只不过,一个是基本类型的值(拷贝),一个是引用的值(拷贝),而非引用的对象。
public class HelloWorld { String str = new String("good");//创建一个 内容为 "good"的字符串对象,str 引用指向该对象 ,运行期产生"good"字符串对象。 String str2 = "good"; //str2指向常量池中"good"的字符串对象。编译时产生"good"字符串 //此时str/str2分别是两个不同的引用,(str==str2)为false //碰巧指向内容相同的字符串。 StringBuffer str3 = new StringBuffer("good");//创建"good"的StringBuffer对象,str3为指向该字符串的引用。 char[] ch = { 'a', 'b', 'c' };//初始化char数组,ch为指向该数组的引用 Integer i = 2;//初始化数值为2的Integer对象,i为指向该对象的引用 public void change(Integer i) { //i为传入参数的一个引用的拷贝,也就是此时i变量的指向的内存地址和传入参数的指向的内存地址是相同的,但是保存这个临时参数的内存是另外一个地方,与原始参数不是同一个内存(因为拷贝了,肯定是另外一个内存了) //此时也指向了原来的内存地址 i = 10;//试图改变原始i指向的对象的值,这里是将i重新引用到一个值为10的Integer对象 //函数结束时,i指向的对象(内存)被销毁,i自身也被销毁。 //原始的i并未被改变 } public void change(String str, char ch[]) { //str,ch传入时,均会自动产生一个对应引用的拷贝,下面的操作是针对这个拷贝的 //在未做任何修改前,都是指向了原始引用参数指向的对象 str = "I'm changed";//试图改变原始引用指向的对象内容,但是这里实际上是将该引用重新指向了一个新的String对象 str = str + "changed?";//由于String是immutable类,【+】 操作不会修改str的拷贝之前指向的对象的内容,这里是 //重新产生了一个新的String对象,然后将str的拷贝指向它。 ch[0] = 'g';//改变原始引用的指向的对象内容,由于ch[0]为基本类型,即是将该ch[0]指向的内存地址的值修改。 //函数退出时,临时拷贝将被回收,临时创建的对象由于不再使用也被回收。 //但是ch[0]已经改变了原始对象所在的内存的值。 } public void change(StringBuffer str, char ch[]) { //str,ch传入时,均会自动产生一个对应引用的拷贝,下面的操作是针对这个拷贝的 //在未做任何修改前,都是指向了原始引用参数指向的对象 str.append("changed");//由于StringBuffer.append操作会修改自身的内容(mutable) //也即会修改str指向的对象的内容,因此会直接修改原始引用参数对应的对象的内容 ch[0] = 'g';//改变原始引用的指向的对象内容,由于ch[0]为基本类型,即是将该ch[0]指向的内存地址的值修改。 //函数退出时,临时拷贝将被回收,临时创建的对象由于不再使用也被回收。 //但是ch[0]已经改变了原始对象所在的内存的值。 } public static void main(String[] args) { // TODO Auto-generated method stub System.out.println("===================华丽的分割线========================"); String s1 = "Hello"; String s2 = s1; s1.replace('e', 'a'); System.out.println(s1); System.out.println(s2); HelloWorld h = new HelloWorld(); h.change(h.str, h.ch); System.out.println("===================华丽的分割线========================"); System.out.println("1:" + h.str + " and " + String.valueOf(h.ch)); h.change(h.str2, h.ch); System.out.println("===================华丽的分割线========================"); System.out.println("2:" +h.str2 + " and " + String.valueOf(h.ch)); System.out.println("===================华丽的分割线========================"); h.change(h.str3, h.ch); System.out.println("3:" +h.str3 + " and " + String.valueOf(h.ch)); System.out.println("===================华丽的分割线========================"); h.change(h.i); System.out.println("4:" + h.i); } }
//结果
===================华丽的分割线======================== Hello Hello ===================华丽的分割线======================== 1:good and gbc ===================华丽的分割线======================== 2:good and gbc ===================华丽的分割线======================== 3:goodchanged and gbc ===================华丽的分割线======================== 4:2
注意上面示例代码中,作为Integer/String等不可变类和作为StringBuffer的可变类的结果不同。
因此,对象的引用作为参数传入以后会不会修改原始的对象内容,完全取决于该对象的类的实现,实现为immutable的非可变类,比如Integer,String等封装了基本类型的类,是不会修改原始对象的内容的(经常是新产生一个对象返回给调用者);而作为可变类的StringBuffer,由于相应的操作实现是会修改到引用对象的内容,从而会修改到原始对象的内容(因为引用的拷贝和引用都指向了该对象)。
总而言之,
Java的函数参数传递实际上不存在所谓的按引用传递和按值传递,说白了都是按值传递,只不过这个值所代表的意义要搞清楚。与C++/C里面的原理大致相同,因为编译器原理也基本相同嘛。
结合对象和引用管理的内存概念和编译器原理来理解,比较好理解这里的各种容易混淆的地方。
参考:
http://www.yoda.arachsys.com/java/passing.html