Loading...
墨滴

舒米勒

2021/07/10  阅读:38  主题:橙心

终于弄懂了:Java 中到底有没有引用传递?

大家好,我是 杰哥

还记不记得,刚开始找工作的时候,笔试题中,往往会有那么几道代码题是这样的:给你几行代码,让你判断输出的结果是什么。经常出现的是判断程序启动时,父类子类的构造方法、静态方法、静态属性与普通方法等的执行顺序。还有一种类型则是,一个值,调用了一个 void 方法之后,判断打印出来的是修改以后的值是什么,一般都是考你经过这个方法的调用,这个值会不会发生变化

这些套路题,往往在最初会难倒很多基础较为薄弱的童鞋,但是若搞懂了这些背后的原理,那你还怕什么,这些对你来说,岂不是送分题?推你进大厂呢?

那么,今天我们就来一起看看,java 中的值传递与引用传递,消灭以前的一些错误理解,彻底搞清楚 java 在调用中,究竟是如何进行参数的传递的?

一 理论先行

要搞清楚 java 中的方法调用,到底是值传递,还是引用传递之前,先来看看 值传递引用传递分别是什么

值传递(pass by value)是指在调用函数时将实际参数 复制 一份传递到函数中,而并不是将这个值直接传递给函数

引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,而传递过来的地址还是与之前的地址指向同一个值,那么要是修改了这个参数,就会影响这个值的改变

二 实践寻找

很多人都说 Java 中实际上是只有值传递,其实,我也很认同,并通过以下几个例子,做了验证

看的时候,可以先别看答案,先自己猜一猜会是什么结果哦

1 传递类型为基本类型 int

public static void main(String[] args) {
        int value = 10;
        setValue(value);
        System.out.println("调用后:"+ value);
    }


    public static void setValue(int value) {
        value = 2 * value;
        System.out.println("调用时:" + value);

    }

打印结果:

调用时:20
调用后:10

说明

value 的值并没有因为调用了 setValue()方法,而发生改变

也就是说,我们验证了 java 中的 基本类型 是采用值传递的方式的

2 传递类型为引用类型: String

public static void main(String[] args) {
        String value = "hello";
        setValue(value);
        System.out.println("调用后:"+ value);
    }


    public static void setValue(String value) {
        value = value + "123";
        System.out.println("调用时:" + value);

    }

打印结果:

调用时:hello123
调用后:hello

说明

value 的值并没有因为调用了 setValue()方法,而发生改变

那么,我们也验证了 java 中的 引用类型 是采用值传递的方式的

中场休息

来来来,那么也就是说,我们基本上可以下个结论,就是说:java 的确如大家所说,在方法调用过程中,是采用值传递的

我们来分析一下他们调用过程中的步骤

我们知道,java 在进行方法调用时,会将线程中每个方法分别放入栈中,先进入的方法,最终被放在了最低层,因此会在最后被执行,最后入栈的方法,则会最先被执行

那么,对于 main 方法和 setValue() 方法,在 栈中存放的 方式如下:

image.png
image.png

main 方法 中的 value,被传递给 setValue() 方法,最终它的值并没有改变,也就是说,它是值传递:即传递 给 setValue() 的参数,是 value 的副本,而并不是 value 本身,所以,我们在 setValue() 方法中,即时为 value 重新赋值,由于它是另一个 value,那么,原来的 value 值当然不会被修改了

也许,你会对此提出质疑,因为你在想,即使 基本类型 和这里的 String 类型,虽然被印证了是采用值传递的,但是要是其他引用类型呢?比如说 一个实体类对象的值,往往就因为调用方法,而发生变化

是这样吗?我们来接着看看

3 传递类型为引用类型:User

 public static void main(String[] args) {
        User user = User.builder().age(11).name("杰哥").build();
        setValue(user);
        System.out.println("调用后:"+ user);
    }


    public static void setValue(User user) {
        user = user.builder().age(15).name("李四").build();
        System.out.println("调用时:" + user);
    }

打印结果:

调用时:User(id=0, name=李四, age=15)
调用后:User(id=0, name=杰哥, age=11)

说明

value 的值并没有因为调用了 setValue()方法,而发生改变。杰哥,还是那个杰哥

这下你相信了吧?引用类型,也是采用了值传递的:在方法调用过程中,只会传递一份参数的副本,因此并不会影响原来的值

如果没有完全说服你,那么我们再来一个例子,传递类型为集合

4 传递类型为 集合:List

public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("hello");
        list.add("world");
        setValue(list);
        System.out.println("调用后:"+ list);
    }


    public static void setValue(List<String> list) {
        list = new ArrayList<>();
        list.add("world");
        list.add("hello");
        System.out.println("调用时:" + list);

    }

打印结果:

调用时:[world, hello]
调用后:[hello, world]

说明:

并且将 list 换成 List<User> 类型的结果也一模一样,调用方法之后的 list 值并不会发生变化。这下,我猜你应该已经确信了,java 的确是值传递的,嗯,没毛病

但是,估计有一少部分人,还是会举出一个反例,来推倒这个理论,为了加深大家的理解,我们也一起来看看 这个所谓的"反例"

三 反例说明

public static void main(String[] args) {
        User user = User.builder().age(11).name("杰哥").build();
        setValue(user);
        System.out.println("调用后:"+ user);
    }


    public static void setValue(User user) {
        user.setAge(12);
        user.setName("李四");
        System.out.println("调用时:" + user);
    }

先来猜猜打印结果。。。。。。

是的,打印结果如下:

调用时:User(id=0, name=李四, age=12)
调用后:User(id=0, name=李四, age=12)

说明:

首先,user 的值,在调用了 setValue() 方法之后,是被修改了的,怎么样,是不是会有一点点蒙?

哈哈,没有关系,往下看

1 对比

这个可以跟第二章中的第 3 个例子,对比着看,同样是 User 对象的传递,最终前者未发生变化,而后者却发生了变化。不用疑惑,我们先来对比一下两者的 不同之处

前者的 setValue() 方法,如下:

public static void setValue(User user) {
        user = user.builder().age(15).name("李四").build();
        System.out.println("调用时:" + user);
    }

后者的 setValue() 方法,如下:

public static void setValue(User user) {
        user.setAge(12);
        user.setName("李四");
        System.out.println("调用时:" + user);
    }

其实你会发现,两者的区别很明显:

前者是直接修改了整个 user 的值;而后者,是分别对 user 对象的属性进行重新赋值的

前者的调用过程,我们在上面已经分析过了:由于这里的 usermain 方法中 user 对象的拷贝,那么,这里即使重新对这个 user 赋值,并不会更改 main 方法中的 user 的值

我们一起来分析一下后者这段代码的调用过程,如下图所示:

2 分析

image.png
image.png

说明

  1. main 方法 将 user 拷贝 给 setValue() 方法

2)拷贝的内容是 user 本身,但是拷贝之后的 user 对象的值,指向的还是与拷贝前的 user 所指向的同一份值

age = 11,
name = "杰哥"

3)那么,在 setValue() 中,修改了这份值,改为了:

age = 12,
name = "李四"

那么,值就这一份,被修改了,那最终呈现出来的效果,就是我们最终的 user 对象就发生了变化

这其实也印证了,java 是值传递的,只是对于 java 中的对象参数来说,值的内容是对象的引用。我们再来回顾一下值传递到底是什么?

值传递 是指在调用函数时将实际参数 复制 一份传递到函数中,而并不是将这个值直接传递给函数

而,我们这里实际上也是把 user 复制了一份到 setValue()函数中,这里的值实际上是 user 的对象引用,只 copy 了引用,也就是地址,但是他们所指向的内容,却依旧是同一份,java 中并不会为你再创建一份,那么,直接修改了这些内容,那么原来的 user 对象的值肯定也就发生了改变

四 总结

好了,事实证明,java 中的确只是存在值传递的,不知道你理解了没有。我们再来总结一下

1 java 中只存在值传递,对于 基本类型、引用类型以及对象类型均是如此,因此调用了一个对某个传递过来的参数进行赋值操作的时候,均不会影响原来的值

2 对于对象类型,若直接修改了它的具体属性,当出现在调用方法之后,会发生改变的原因是:在 java 中,对象类型的值是对象引用,在调用过程中,传递的是一份对象引用的拷贝进行传递的,但是原引用和拷贝的引用依旧指向的是堆中的同一份值,因此,这份值做了改变,原来的 对象类型本身就发生了变化

舒米勒

2021/07/10  阅读:38  主题:橙心

作者介绍

舒米勒

深信服-架构师