网络日志

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 {

输出结果

实参 node 指向的内存地址为:366712642

这说明,引用对象参数传递时,传递的是指向真实对象的地址,而函数中的形参 node 拿到同样的地址时,通过 node.setVal(11),会通过地址找到真实的对象进行操作。这里 TreeNode 没有重写 hashCode 方法,所以

集合对象

由于 ArrayList 重写了 hashcode()方法,所以这里使用 System.identityHashCode 拿到地址值。

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

对于集合,传递的也是引用的地址,函数内通过形参得到引用地址的拷贝后再操作真实对象,导致实参访问真实对象时已经被修改过了。如果形参指向了新的内存地址,则修改不会影响到原对象的值。

注:JsonUtils 是用 Jackson 实现的。

数组

普通数组,和集合一样是引用类型

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}]

数组与集合的情况也是一样的。

基本类型的包装类型

值得注意的是,对于基本类型的包装类型,其参数传递也是属于地址值传递;

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

而由于 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