关于值传递和引用传递

2018-07-20 00:29:43 +08:00
 lux182

今天看到一面试题,对于输出结果为 0 有很多一知半解的人解释,

对于新手来说看得似懂非懂,然后看完还是一头雾水。

 @Test
    public void test1(){
        Integer i = new Integer(0);
        //Integer@853 -----1
        add(i);
        //Integer@853 -----5
        System.out.println(i);//0
        i +=3;
        //Integer@864 -----6
        System.out.println(i);//3
    }
    private void add(Integer i) {
        //Integer@853 -----2
        i = i + 3;
        //Integer@864 -----3
        i = new Integer(i);//3
        //Integer@865 -----4
    }    

在代码上我都标注了 i 的各步骤的引用地址。

从调试信息上来看,方法传递的就是对象的地址。

而让新手迷惑的关键地方是,add 方法中改变了 i 的值啊,为什么还是返回 0 ?

Integer 的加法运算生成了一个新的 Integer 对象,并申明为变量 i,而局部变量的生命周期只存在自己的方法中,两个方法中的变量名都为 i,但是此时他们已经没有关系了。

不知道解释的是否正确,希望错误的地方各位指正,以免让别人产生误解。

4417 次点击
所在节点    程序员
50 条回复
LINWAYNE
2018-07-20 09:38:21 +08:00
Integer 自动拆箱装箱了解一下
raysonx
2018-07-20 09:39:30 +08:00
我猜困惑来自于所谓的值类型和引用类型,后者类似于 C C++的指针。但这个概念和值传递、引用传递没有关系。
sagaxu
2018-07-20 09:39:48 +08:00
内存是货柜,一个单元就是一个抽屉,变量是写了货柜抽屉编号的卡片,int 这种 primitive 类型,直接记卡片上,不放抽屉里。而 Integer 仍然放抽屉,卡片上只记编号,自动装箱拆箱。

函数调用的时候,假设有个变量 a,在 f(a)的时候,会另外拿一张卡片 b,抄好 a 卡片里的抽屉编号,f 函数体执行的时候,拿到的是 b 卡片,抽屉编号跟 a 一样,它可以去读写这个抽屉,但是改变不了 a 卡片。

变量赋值,只是把卡片擦了重写抽屉号码。
raysonx
2018-07-20 09:44:05 +08:00
好吧楼主问题中的语言是 Java,不是 C#(谁让两个语言那么像呢)。
Java 中根本没有引用传递,所有的函数调用都是值传递。
StephenDev
2018-07-20 09:44:10 +08:00
卧槽,我刚才算半天发现怎么算都不对,然后我仔细再看了一下,发现我看错了。
我以为你那个 Integer@xx---1 后面跟着的这个数字是 Integer 对象当前的值。。。。。
我人晕了。
98jiang
2018-07-20 10:08:38 +08:00
你把 i 传过去了,但是没有返回回来所以还是 0 呀?看上面的人说的,应该就是只是值传过去了,并不会影响那个变量。
lux182
2018-07-20 10:11:31 +08:00
@sagaxu 这个解释比较形象
joshu
2018-07-20 10:38:16 +08:00
Integer 是不可变量,Integer i=0;i+=3;执行到这一步时,i 的地址已经不是原来的地址了,数不是原来的那个数了。
而 java 传引用,在子函数里修改 Integer 实际上是新建了一个 Integer 对象覆盖到这个名字上,而不是修改引用的那个对象。
因此主函数里这个数不变。
另外 Integer 对于常用的、较小的数有 cache,可以看看源码。
jzq526
2018-07-20 10:40:46 +08:00
我觉得楼主的说法是正确的,但也不完全正确。C 语言和 Java 语言的参数传递是一样的。但为什么 C 里面分了个值传递和地址传递(引用传递)呢?看内容。从形式上,都是把实参的值复制给了形参,然后形参带入到函数中运算。从内容上看,如果实参是个基本数据类型,那就变量本身就保存值,所以复制给形参的也就是这个变量的值,这就叫做值传递;如果实参是个复合数据类型,比如数组,结构体,Java 中的对象等,实参只保存了真实对象的地址,复制给形参的也是这个对象的地址,这就是地址传递或者引用传递。
所谓值传递和引用传递,形式上一样的,但内容是不同的。
楼主这个问题在哪里?我认为主要是包装类的机制造成的。add 方法中的参数 i,一开始获取的确实是实参的地址,调用的也的确是实参的对象,但在“ i=i+1 ”这一行上,i+1 这个运算并不是在 i 原来的内存空间中进行的,而是将结果放到了另一个空间中,也就是说,JVM 把结果存放到另一个对象中,地址在保存到形参 i 中,原来的空间就不管了。这也是为什么经过这一行程序后,i 的地址发生改变的原因。这个方法在 String 类对象用+号连接时也用了,好处就是能快一点,缺点就是频繁操作的话比较占内存。
所以,最后的结论就是,楼主以为 i=i+1 和普通的基本数据类型运算一样,运算结果会存放到原来的内存空间中,但 Java 没这么干,而是把结果存放到了另一个内存空间中再修改了对象名保存的地址值。
另外,Integer 类貌似没有提供修改自身值属性的方法,所以楼主只能想别的方法了。
zhujinliang
2018-07-20 10:50:13 +08:00
关于指向相同的地址
理解代码只是表达逻辑,最终实际的地址位置以及操作指令,还有编译、优化等等多层包装最终决定,不能想当然的认为计算机严格按照代码流程来做。

假设编译器将 add 函数内联,即将 add 代码片段拷贝到 test1 函数的对应位置,结果可以是这样:
public void test1(){
Integer i = new Integer(0);

// add(i);
Integer add_i = i + 3;
add_i = new Integer(add_i);//3

System.out.println(i);//0
i +=3;
System.out.println(i);//3
}
对照来看,实际进入 add 函数的 i 还是原来的 i,只不过将 i+3 的结果存到了另外的地方,避免修改原来的 i

我们说值传递、引用传递,也只是为了方便理解和讨论。实际 CPU 做了什么,抄了哪些近道,我们在这个层面并不关心。
momocraft
2018-07-20 11:07:28 +08:00
都写 java 了就不要担心地址了,引入地址这个(只存在于 JVM 层的)要素只会让你更混乱
mx1700
2018-07-20 12:14:39 +08:00
如果把 Integer 换成 String 你能理解吗?
Integer 和 String 一样,都是不可变对象
sc13
2018-07-20 12:34:22 +08:00
java 只有值传递的,对象传递的是引用的复制的值
hyyou2010
2018-07-20 13:12:20 +08:00
目前为止,可能只有 @joshu 直接说出了关键

本质上只有两种传递。
值传递:function(int i),复制了一份 i 进去
指针传递:function(Object o),复制了一份 o 的地址进去
还真不知道 function(Integer i)是哪一种传递,按说应该是指针传递,也即,函数内部操作的 Integer 就是外部的 Integer

但根据 @joshu 提示搜了一下,Integer 内部还真是一个 final int value,所以 add 函数的 i=i+3 时会生成新 Integer,因此原先的 i 值,同时也是函数外部的 i 值不会被改变

但是,在外部函数的 i+=3 这一步,由于生成了新的 Integer,且 i 指向新的 Integer,所以打印 i 是打印的新 Integer,所以是新值 3
suixn
2018-07-20 14:22:37 +08:00
String、Integer,Long, Short, Double, Float, Character, Byte, Boolean 都是不可变的。
意思是只要改变了他们的值,地址映射就变了。
suixn
2018-07-20 14:27:02 +08:00
按你的代码:
```java
public void test1(){
Integer i = new Integer(0);
add(i);
System.out.println(i);//0
}
private void add(Integer i) {
i = i + 3;
}
```
而其他类型,比如 map
```java
public static void main(String[] args) {
HashMap<String, String> map = new HashMap<>(2);
System.out.println(map.size());//0
add(map);
System.out.println(map.size());//1

}

private static void add(HashMap<String, String> i) {
i.put("a", "a");
}

```
alamaya
2018-07-20 14:32:13 +08:00
@suixn 这里跟可变不可变没有关系,你在 add 里将 i 重新指向另一个 map,main 里面的 i 也不会受影响
suixn
2018-07-20 14:35:19 +08:00
@alamaya #37 楼主的例子完全说明不了问题,在函数了直接重新 new 了
suixn
2018-07-20 14:37:21 +08:00
@alamaya #37 另外,你说的是
public static void main(String[] args) {
HashMap<String, String> map = new HashMap<>(2);
System.out.println(map.size());//0
add(map);
System.out.println(map.size());//1
}

private static void add(HashMap<String, String> i) {
i.put("a", "a");
i = new HashMap<>();//这样吗?
}
alamaya
2018-07-20 14:43:52 +08:00
@suixn 楼主这个问题的本质就是 java 传参的问题,跟是不是可变类没有关系。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/472515

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX