Java参数传递到底是按 值传递 还是 引用传递 ?
前言
首先明确,Java中方法参数传递方式是按值传递。对于基本类型(int a, long b),参数传递时传递的是值,例如int a = 5,传递的就是5。如果是引用类型,传递是指向具体对象内存地址的地址值,例如用System.out.println(new Object())打印出来的 java.lang.Object@7716f4 中 @符号后面的7716f4 就是16进制的内存地址,System.out.println实际上是默认调用了对象的toString方法,
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
复制代码
可以看到7716f4是由hashCode()输出的,如果有对象重写了hashCode方法,那输出的有可能就不是对象的初始内存地址了,所以如果要准确获得对象的初始地址建议调用System.identityHashCode()。
值得一提的是,在Java中获取一个对象的内存地址一般没有什么意义,因为它可能在程序运行过程中随着垃圾回收等动作被JVM更改。不过在下面我们可以根据引用的对象地址是否相同来看看参数传递的各种情况。
举例说明
基本类型作为参数传递
public class ValuePass {
public static void main(String[] args) {
//值传递举例
int num = 10;
System.out.println("改之前的值:" + num);
modify(num);
System.out.println("改之后的值:" + num);
}
private static void modify(int num2) {
num2 = 11;
}
}
复制代码
输出结果为
改之前的值:10
改之后的值:10
复制代码
通过这个例子,说明基本数据类型作为参数传递时,传递的是值的拷贝,无论怎么改变这个拷贝,原值是不会改变的。
对象作为参数传递
对象这里可以再划分一下,分为普通对象,集合类型和数组类型。下面依次来看一下效果
普通对象
public class ReferenceBasicPass {
static class TreeNode {
int val;
TreeNode left;
TreeNode right;
public TreeNode(int x) { val = x; }
public void setVal(int val) {
this.val = val;
}
public int getVal() {
return val;
}
}
public static void main(String[] args) {
//普通对象
TreeNode node = new TreeNode(10);
System.out.println("实参 node 指向的内存地址为:" + node.hashCode());
System.out.println("改之前的值:" + node.getVal());
modify(node);
System.out.println("改之后的值:" + node.getVal());
}
private static void modify(TreeNode node) {
System.out.println("形参 node 指向的内存地址为:" + node.hashCode());
//引用了同一块地址,操作了同一块堆内存
node.setVal(11);
}
}
复制代码
输出结果
实参 node 指向的内存地址为:366712642
改之前的值:10
形参 node 指向的内存地址为:366712642
改之后的值:11
复制代码
这说明,引用对象参数传递时,传递的是指向真实对象的地址,而函数中的形参node拿到同样的地址时,通过node.setVal(11),会通过地址找到真实的对象进行操作。
集合对象
由于ArrayList重写了hashcode()方法,所以这里使用System.identityHashCode拿到地址值。
public class ReferencePass {
public static void main(String[] args) {
//集合对象
List<TreeNode> nodes = new ArrayList<>();
nodes.add(new TreeNode(1));
nodes.add(new TreeNode(2));
System.out.println("修改之前实参 node 指向的内存地址为:" + System.identityHashCode(nodes));
System.out.println("修改之前实参 node 指向地址存放的对象内容为:" + JsonUtils.toJson(nodes));
modify(nodes);
System.out.println("修改之后实参 node 指向的内存地址为:" + System.identityHashCode(nodes));
System.out.println("修改之后实参 node 指向地址存放的对象内容为:" + JsonUtils.toJson(nodes));
System.out.println("\n------------------------------------------------\n");
modify2(nodes);
System.out.println("再次修改之后实参 node 指向的内存地址为:" + System.identityHashCode(nodes));
System.out.println("再次修改之后的实参 nodes 指向地址存放的对象内容为:" + JsonUtils.toJson(nodes));
}
private static void modify(List<TreeNode> nodes) {
//引用了同一块地址,操作了同一块堆内存
nodes.add(new TreeNode(3));
}
private static void modify2(List<TreeNode> nodes) {
System.out.println("形参 nodes 指向的内存地址:" + nodes.hashCode());
//形参nodes 指向了新的内存地址,对其进行操作但是不影响实参指向的内存地址的真实对象
nodes = new ArrayList<>();
nodes.add(new TreeNode(5));
System.out.println("形参 nodes 指向的新内存地址:" + nodes.hashCode());
System.out.println("形参 nodes 指向新地址存放的对象内容为:" + JsonUtils.toJson(nodes));
}
}
复制代码
输出结果为
修改之前实参 node 指向的内存地址为:366712642
修改之前实参 node 指向地址存放的对象内容为:[{"val":1},{"val":2}]
修改之后实参 node 指向的内存地址为:366712642
修改之后实参 node 指向地址存放的对象内容为:[{"val":1},{"val":2},{"val":3}]
------------------------------------------------
形参 nodes 指向的内存地址:1110478811
形参 nodes 指向的新内存地址:1458540949
形参 nodes 指向新地址存放的对象内容为:[{"val":5}]
再次修改之后实参 node 指向的内存地址为:366712642
再次修改之后的实参 nodes 指向地址存放的对象内容为:[{"val":1},{"val":2},{"val":3}]
复制代码
对于集合,传递的也是引用的地址,函数内通过形参得到引用地址的拷贝后再操作真实对象,导致实参访问真实对象时已经被修改过了。如果形参指向了新的内存地址,则修改不会影响到原对象的值。
注:JsonUtils是用Jackson实现的。
数组
普通数组,和集合一样是引用类型
public class ReferenceArrayPass {
public static void main(String[] args) {
//普通数组,和集合一样是引用类型,数组本质上也是
int[] ints = new int[3];
ints[0] = 1;
ints[1] = 2;
System.out.println("实参 ints 指向的内存地址为:" + System.identityHashCode(ints));
System.out.println("修改之前 ints 索引为2的值" + ints[2]);
modify(ints);
System.out.println("修改之后 ints 索引为2的值" + ints[2]);
//普通数组的class为[I , I表示int型
System.out.println(ints.getClass());
}
private static void modify(int[] ints) {
//引用了同一块地址,操作了同一块堆内存
System.out.println("形参 ints 指向的内存地址为:" + System.identityHashCode(ints));
ints[2] = 3;
}
}
复制代码
输出
实参 ints 指向的内存地址为:366712642
修改之前 ints 索引为2的值:0
形参 ints 指向的内存地址为:366712642
修改之后 ints 索引为2的值:3
复制代码
数组与集合的情况也是一样的。
基本类型的包装类型
值得注意的是,对于基本类型的包装类型,其参数传递也是属于地址值传递;
public class ValuePass {
public static void main(String[] args) {
//值传递举例
int num = 10;
System.out.println("before modify result:" + num);
modify(num);
System.out.println("after modify result:" + num);
Integer integer = 20;
System.out.println("before modify result:" + integer);
modify(num);
System.out.println("after modify result:" + integer);
}
private static void modify(int num2) {
num2 = 11;
}
private static void modify(Integer integer) {
integer = 21;
}
}
复制代码
输出结果为
before modify result:10
after modify result:10
before modify result:20
after modify result:20
复制代码
而由于jdk1.5以上的自动装箱特性,Integer i = 20 等价于执行 Integer i = Integer.valueOf(20) ,valueOf()方法参看源码会根据传入的数值 如果在-128-127之间 就从常量池中获取一个Integer对象返回,如果不在范围内 会new Integer(20)返回。
即是说Integer的地址会随着值的改变而改变,这其实就是引用类型的赋值,指向了新的内存地址了,例如上面integer = 21的例子, 即等价于integer = Integer.valueOf(21),不管21之前是否有创建过,integer都指向了新的内存地址,但是并不影响实参,外部依旧是20