java值传递与引用传递

最近在学习feign时,看了一些源码,其中有一段涉及到==的代码,于是复习了一下java中值传递和值引用的知识点
代码如下:

IClientConfig getClientConfig(Request.Options options, String clientName) {
        IClientConfig requestConfig;
        if (options == DEFAULT_OPTIONS) {
            requestConfig = this.clientFactory.getClientConfig(clientName);
        }
        else {
            requestConfig = new FeignOptionsClientConfig(options);
        }
        return requestConfig;
    }

注意在if中开发人员使用“==”对两个对象进行比较,这里我比较费解,options是一个参数配置对象,里面记录的是一些参数配置,那么比较的应该是其内容才对。而实际比较其地址值,因此我先模拟传参过程,观察对象在传参过程中的变化
模拟代码如下:

public class MyTest {
    private static final Person person = new Person("李四",20);

    public void testOne(Person p){
        System.out.println("入参p:" + p);

        p = new Person("张三",18);

        System.out.println("重新赋值后p:" + p);
    }

    public static void main(String[] args) {
        MyTest m = new MyTest();

        System.out.println("执行testOne前:" + person);

        m.testOne(person);

        System.out.println("执行testOne后:" + person);
    }
    

    public static class Person{
        private String name;
        private Integer age;

        public Person() {
        }

        public Person(String name, Integer age) {
            this.name = name;
            this.age = age;
        }
        //省略get set

打印结果如下:

执行testOne前:Person{name='李四', age=20}
入参p:Person{name='李四', age=20}
重新赋值后p:Person{name='张三', age=18}
执行testOne后:Person{name='李四', age=20}

根据打印结果可以得出以下结论:
1.person对象在进入testOne()方法前后,对象没有发生变化
2.person对象作为参数传递到testOne()方法内部后,person对象作为参数发生了变化
可能你认为private static final Person person = new Person("李四",20);这里的person对象使用final修饰的所以不会发生变化,那么我们不使用final修饰进行测试:

public class MyTest {
    private Person person = new Person("李四",20);

    public void testOne(Person p){
        System.out.println("入参p:" + p);

        p = new Person("张三",18);

        System.out.println("重新赋值后p:" + p);
    }

    public static void main(String[] args) {
        MyTest m = new MyTest();

        System.out.println("执行testOne前:" + m.getPerson());

        m.testOne(m.getPerson());

        System.out.println("执行testOne后:" + m.getPerson());
    }

结果如下:

执行testOne前:Person{name='李四', age=20}
入参p:Person{name='李四', age=20}
重新赋值后p:Person{name='张三', age=18}
执行testOne后:Person{name='李四', age=20}

可以看到得到的结论是一样的。那么为什么会有这样的结果呢,这就需要读者具备一定的java基础知识,了解java对象是如何在内存中存储的。简单来说,对于引用数据类型,也就是对象来说,初始化一个对象,比如:

Person person = new Person("李四",20);

创建对象时,会在栈中创建变量person,在堆中初始化对象,栈中person变量持有对堆中("李四",20)对象的引用,也就是我们常说的,变量持有地址值。使用变量person,就可以获取到堆中对象的内容name,age。那么当变量person被当做参数传递进一个方法后会发生什么,传递的内容是什么,我们用两张图来表示:
这是new对象时:


image.png

这是对象作为参数传入方法时:


image.png

当对象作为参数传入方法时,实际传入的是person对象的一个副本,传入方法的是对与对象的引用,因此你可以看到当参数进入方法时,打印出来的与传入前是一致的,因为他们持有相同的引用,获取的内容是相同的
执行testOne前:Person{name='李四', age=20}
入参p:Person{name='李四', age=20}

此时对变量进行的set,get操作,实际上是对变量持有引用的对象进行操作,比如这时候去修改对象内部属性值:

    public void testTwo(Person p){
        System.out.println("入参p:" + p);

        p.setName("王二麻子");
        p.setAge(25);

        System.out.println("重新赋值后p:" + p);
    }

    public static void main(String[] args) {
        MyTest m = new MyTest();

        System.out.println("执行testOne前:" + m.getPerson());

//        m.testOne(m.getPerson());
        m.testTwo(m.getPerson());

        System.out.println("执行testOne后:" + m.getPerson());
    }

结果如下:

执行testOne前:Person{name='李四', age=20}
入参p:Person{name='李四', age=20}
重新赋值后p:Person{name='王二麻子', age=25}
执行testOne后:Person{name='王二麻子', age=25}

但是为什么之前对p重新赋值并没有效果呢?因为传入person后,执行了

p = new Person("张三",18);

这段代码在内存中的效果如图所示


image.png

于是我们看到了测试的结果,在方法内部打印person,对象属性已经发生了变化,在方法外部打印person,没有改变原来的person对象,因为一旦内部new了新的对象,person副本持有的引用发生了变化,与原对象的引用不再有任何关系,而方法结束时,person副本消亡,并没有对原对象进行任何操作。

执行testOne前:Person{name='李四', age=20}
入参p:Person{name='李四', age=20}
重新赋值后p:Person{name='张三', age=18}
执行testOne后:Person{name='李四', age=20}

需要注意的时,这里person对象作为参数传入了方法,实际传入的是一个person对象的副本,他持有堆中对于person对象的引用,注意在没有改变这个引用前,对于person的操作,比如get,set仍旧是对原对象的内容进行操作。一旦这个引用发生了变化,对原对象就不会有影响了,这也正是上面两个测试方法产生不同结果的原因,我们可以得出结论当一个对象作为参数传入方法时,实际传递的是对于对象的引用,可以理解为引用传递,那么java中的值传递又是什么呢?我们使用基本数据类型int测试
测试代码:

    public void testThree(int a){
        System.out.println("入参a:" + a);

        a = 20;

        System.out.println("重新赋值后a:" + a);
    }
    public static void main(String[] args) {
        MyTest m = new MyTest();

        int  b = 100;
        System.out.println("执行testThree前:" + b);
        m.testThree(b);
        System.out.println("执行testThree后:" + b);

    }

结果如下:

执行testThree前:100
入参a:100
重新赋值后a:20
执行testThree后:100

其实结论与上面相似,传入的是副本,方法内部改变的是副本值,方法结束,副本消亡,原来的变量的值不受影响,我一位同事用一句话来总结,方法只能改变方法内部的东西,外部的是没有办法改变的

《java编程思想》中有一句话描述引用传递很有意思。解释一下:文中的句柄说的就是引用
“12.2 制作本地副本
稍微总结一下:Java中的所有自变量或参数传递都是通过传递句柄进行的。也就是说,当我们传递“一个对象”时,实际传递的只是指向位于方法外部的那个对象的“一个句柄”。所以一旦要对那个句柄进行任何修改,便相当于修改外部对象。”

同时《java核心技术卷I中》中也指出,java中任何传递都是按值传递的,只不过基本数据类型传递的是数值,而引用数据类型传递的是地址值。

在java核心技术卷I中也对此有描述,内容如下:
“按值调用表示方法接受到的是调用者提供的值。按引用调用表示方法接受到的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值”

事实上java核心技术卷I对值传递的描述就非常准确了,他直接指出了,值传递与引用传递的本质区别,即在方法内部,对变量的影响

总结一下

  1. java中对于基本数据类型都是值传递,传递的是一个副本值,副本在方法执行时生效,方法结束,副本消亡,原来的变量不会发生变化
  2. java中对于引用数据类型,传递的是其变量的副本,该副本持有与原变量对对象相同的引用,简单来说传递的是引用值
  3. 在引用值不发生变化时,可以访问对象内部,对对象进行操作,但需要注意,这个传入的参数仍旧是副本,并不是原对象,只是因为引用值相同,他可以对原对象进行访问
  4. 当副本持有的引用值发生变化时,与原对象已经没有关系,方法结束,副本消亡,不会对对象有影响
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。