Java 参数传递到底是按 值传递 还是 引用传递 ?

简介: 首先明确,**Java中方法参数传递方式是按值传递**。对于基本类型(int a, long b),参数传递时传递的是值,例如int a = 5,传递的就是5。如果是引用类型,传递是指向具体对象内存地址的地址值...

前言

首先明确,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),会通过地址找到真实的对象进行操作。这里TreeNode没有重写hashCode方法,所以

集合对象

由于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)返回。

image-20211124225444095.png

即是说Integer的地址会随着值的改变而改变,这其实就是引用类型的赋值,指向了新的内存地址了,例如上面integer = 21的例子, 即等价于integer = Integer.valueOf(21),不管21之前是否有创建过,integer都指向了新的内存地址,但是并不影响实参,外部依旧是20


参考

相关文章
|
2月前
|
Java
java是值传递还是引用传递
本文澄清了Java中参数传递的常见误解,总结出Java采用“值传递”的方式。对于基本类型,传递其值的拷贝,方法内修改不影响原值;而对于对象类型,则传递其引用地址的拷贝,尽管是拷贝,但因指向同一对象,故方法内的修改会影响原对象状态。形参仅在方法内部有效,而实参则是调用方法时传递的具体值。通过示例和比喻(如复刻仓库钥匙),形象地解释了值传递、引用传递及Java特有的“共享对象传递”概念,帮助理解不同情况下参数传递的行为差异。
|
2月前
|
存储 安全 Java
|
2月前
|
Java
java中的值传递和引用传递
【8月更文挑战第3天】在Java中,值传递用于基本数据类型,传递的是值的副本,因此方法内的修改不影响原值;而引用传递用于对象和数组,虽传递的是引用的副本,但对对象内容的修改会影响原始对象。理解这两者对于正确处理方法调用及参数至关重要。
|
2月前
|
Java
java中的值传递和引用传递
【8月更文挑战第2天】在Java中,基本数据类型如`int`、`double`等采用值传递,传递的是变量值的副本,因此方法内的修改不影响原变量。对象类型则通过引用传递,传递的是对象引用的副本,允许方法内修改原对象。例如,对`StringBuilder`对象的修改会影响原始对象。
|
4月前
|
Java 数据安全/隐私保护
Java基础手册二(类和对象 对象创建和使用 面向对象封装性 构造方法与参数传递 this关键字 static关键字 继承 多态 方法覆盖 final关键字 访问控制权限修饰符)
Java基础手册二(类和对象 对象创建和使用 面向对象封装性 构造方法与参数传递 this关键字 static关键字 继承 多态 方法覆盖 final关键字 访问控制权限修饰符)
32 0
|
4月前
|
Java
Java的值传递与“引用传递”辨析
Java的值传递与“引用传递”辨析
22 0
|
5月前
|
JavaScript 前端开发 Java
【JAVA面试题】什么是引用传递?什么是值传递?
【JAVA面试题】什么是引用传递?什么是值传递?
|
Java
Java里到底是引用传递还是值传递
Java里 只要传参。传的就是变量存的值。而不是 变量本身的地址  (传引用是指传变量本身的地址,注意是变量本身的地址!!!) 请百度  “引用传递” 看看百科是怎么解释这个概念的。
939 0
下一篇
无影云桌面