我们来看一个新手甚至写了多年Java的朋友都可能不是十分确定的问题:
在Java方法传参时,究竟是引用传递还是值传递?
为了说明问题, 我给出一个非常简单的class定义:
public class Foo { String attribute; Foo(String s) { this.attribute = s; } void setAttribute(String s) { this.attribute = s; } String getAttribute() { return this.attribute; }}
下面在阐明观点时,可能会多次用到该类。
关于Java里值传递还是引用传递,至少从表现形式上来看,两种观点都有支撑的论据。下面我来一一分析:
观点1:引用传递
理由如下:
先看一段代码public class Main { public static void modifyReference(Foo c){ c.setAttribute("c"); // line DDD } public static void main(String[] args) { Foo fooRef = new Foo("a"); // line AAA modifyReference(fooRef); // line BBB System.out.println(fooRef.getAttribute()); // 输出 c }}
上述示例,输出结果为"c",而不是"a", 也就是传入的fooRef里的属性被修改了,发生了side-effect。
我们在line AAA
处新创建了一个Object Foo并将其引用fooRef
在line BBB
处传给了方法modifyReference()
的参数cRef
, 该方法内部处理后,fooRef
指向的Object中的值从"a"变成了"c", 而引用fooRef
还是那个引用, 因此,我们是否可以认为,在line BBB
处发生了引用传递?
先留着疑问,我们继续往下看。
观点2:值传递
继续看一段代码
public class Main { public static void changeReference(Foo aRef){ Foo bRef = new Foo("b"); aRef = bRef; // line EEE } public static void main(String[] args) { Foo fooRef = new Foo("a"); // line AAA changeReference(fooRef); // line BBB System.out.println(fooRef.getAttribute()); // 输出 a }}
上述示例,输出结果为"a", 而不是"b", 即对传入的fooRef内部的change并没有影响外部的传入前的值。
我们在line AAA
处新创建了一个Object Foo并将其引用fooRef
在line EEE
处传给了方法changeReference()
的参数aRef
, 该方法内部引用aRef
在line DDD
处被重新赋值。如果是引用传递,那么引用aRef
在line EEE
处已经被指向了新的Object, 输出应该为"b"
才对,事实上是怎样的呢?事实上输出了"a"
,也就是说changeReference()
方法改变了传入引用所指对象的值。
观点1和观点2的输出结果多少会让人有些困惑,别急,我们继续往下看。
深入分析
为了详细分析这个问题,把上述两段代码合起来:
public class Main { public static void modifyReference(Foo cRef){ cRef.setAttribute("c"); // line DDD } public static void changeReference(Foo aRef){ Foo bRef = new Foo("b"); // line FFF aRef = bRef; // line EEE } public static void main(String[] args) { Foo fooRef = new Foo("a"); // line AAA changeReference(fooRef); // line BBB System.out.println(fooRef.getAttribute()); // 输出 a modifyReference(fooRef); // line CCC System.out.println(fooRef.getAttribute()); // 输出 c }}
下面来深入内部来详细分析一下引用和Object内部的变化。
来看下面图示:① Line AAA, 申明一个名叫fooRef
,类型为Foo
的引用,并见其分配给一个新的包含属性值为"f"
的对象,该对象类型为Foo
。
Foo fooRef = new Foo("a"); // line AAA
② Line DDD, 方法内部,申明了一个Foo
类型的名为aRef
的引用,且aRef
被初始化为null
。
void changeReference(Foo a);
③ Line CCC,changeReference()
方法被调用后,引用aRef
被分配给fooRef
指向的对象。
changeReference(fooRef);
④ Line FFF, 申明一个名叫bRef
,类型为Foo
的引用,并见其分配给一个新的包含属性值为"b"
的对象,该对象类型为Foo
。
Foo bRef = new Foo("b");
⑤ Line EEE, 将引用aRef
重新分配给了包含属性"b"
的对象。此处注意,并非将fooRef
重新分配,而是aRef
。
aRef = bRef;
⑥ Line CCC, 调用方法modifyReference(Foo cRef)
后,新建了一个引用cRef
并将之分配到包含该属性"f"
的对象上,该对象同时被两个引用fooRef
和cRef
指向着。
modifyReference(fooRef);
⑦ Line DDD,cRef.setAttribute("c");
将会改变cRef
引用指向的包含属性"f"
的对象,而该对象同时被引用fooRef
指向着。
cRef.setAttribute("c");
此时引用fooRef
指向的对象内部属性值"f"
也被重新设置为"c"
。
总结
Java内部方法传参不是引用传递,而是引用本身的"值"的传递,归根结底还是值传递。将一个对象的引用fooRef
传给方法的形参newRef
,将给该对象新增了一个引用,相当于多了一个alias
。我们可以通过这个原引用fooRef
,或这是方法参数里的新引用newRef
去访问、操作原对象,也可以改变参数里的引用newRef
本身的值,却无法改变原引用fooRef
的值。