V2EX = way to explore
V2EX 是一个关于分享和探索的地方
Sign Up Now
For Existing Member  Sign In
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
lux182
V2EX  ›  程序员

关于值传递和引用传递

  •  
  •   lux182 · Jul 20, 2018 · 5188 views
    This topic created in 2839 days ago, the information mentioned may be changed or developed.

    今天看到一面试题,对于输出结果为 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,但是此时他们已经没有关系了。

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

    Supplement 1  ·  Jul 20, 2018

    add方法的形参i是局部变量,从这个角度看,确实是test方法的i地址的copy。

    思考了一下值传递和引用传递的本质:

    class A:
    Integer i ;
    i= new Integer(0);
    

    一个对象A的初始化,会在内存堆中申请一些内存空间,并会明确一些地址信息

    i在哪里:指向class A实例化对象的meta存着的地址;i是什么类型:指向Integer类型地址;i是什么值:指向new Integer(0)申请的内存地址。

    值传递,也就是说把i的值引用地址作为值,赋给add方法的形参i,那么此时test和add方法的关联就是,都有一个局部变量i,值引用同一个内存地址。

    Supplement 2  ·  Jul 20, 2018

    更正

    生命周期的说法是不正确的,局部变量的生命周期在压栈至出栈时期内。并且局部变量开辟的内存地址在栈内存中。

    50 replies    2018-08-07 11:19:34 +08:00
    sagaxu
        1
    sagaxu  
       Jul 20, 2018 via Android
    这是 Java 吗? Java 方法调用是没有传引用的,都是传值。
    zjp
        2
    zjp  
       Jul 20, 2018 via Android   ❤️ 1
    都打印出 Integer@853 只是说明 test1()中的 i 和 add()中的 i 指向同一个地址,后者是前者的副本,依然为传值。我觉得不要引入"传引用"这个概念更清晰
    lhx2008
        3
    lhx2008  
       Jul 20, 2018 via Android   ❤️ 1
    很简单,传进函数里面的会复制一份引用,就是 int b = a,你再改 b=1,因为 a 本身不可变,所以没用,a 不会变,但是如果 a 是可变的对象,再引用是可以改里面的内容的
    lhx2008
        4
    lhx2008  
       Jul 20, 2018 via Android   ❤️ 1
    a 不可变的话,改 b 等于是重新赋值,a 可变,那可以改 a 里面的内容不用重新赋值
    lhx2008
        5
    lhx2008  
       Jul 20, 2018 via Android   ❤️ 1
    关键在于你有没对函数的参数变量重新赋值,没有的话是可以改对象内容的
    46Gnj0E0OBmad377
        6
    46Gnj0E0OBmad377  
       Jul 20, 2018 via iPhone
    test1 里边的 i 在 add 调用前后不会改变,add 里边那个 i 的 copy 变了。
    starcraft
        7
    starcraft  
       Jul 20, 2018 via iPhone
    这是书看少了啊。有点名气的外国佬的书,都提到了 java 值传递特性吧。
    araraloren
        8
    araraloren  
       Jul 20, 2018
    C 系列语言的参数传递不都是传值的么。。
    unforgiven
        9
    unforgiven  
       Jul 20, 2018
    @araraloren c++有引用传递
    unforgiven
        10
    unforgiven  
       Jul 20, 2018
    你的 add 方法,实际传入的 i 其实是参数的内存地址的值得拷贝
    araraloren
        11
    araraloren  
       Jul 20, 2018
    @unforgiven :) 莫名就忽略了它。。。
    lux182
        12
    lux182  
    OP
       Jul 20, 2018
    @unforgiven 怎么解释指向相同的地址呢
    hugedata
        13
    hugedata  
       Jul 20, 2018
    @sagaxu 这是 c#。
    saberpowermo
        14
    saberpowermo  
       Jul 20, 2018 via Android
    java 的‘’引用‘’传递 传递的是这个引用的 内存地址值
    hugedata
        15
    hugedata  
       Jul 20, 2018
    @sagaxu 收回我的话。sorry
    raysonx
        16
    raysonx  
       Jul 20, 2018 via Android
    @hugedata C#我了解一些,参数前加 ref 或者 out 关键字才是传引用,否则都是传值。传引用比如:
    void swap(ref int a, ref int b) {
    int t = a;
    a = b;
    b = t;
    }

    调用时,两个变量的赋值会被调换:
    int x = 3;
    int y = 5;
    swap(ref x, ref y);
    Jarvix
        17
    Jarvix  
       Jul 20, 2018
    这是 java 吗?
    public static void main(String[] args) {

    Integer i = new Integer(0);
    i = add(i);
    System.out.println(i);//0
    }
    private static Integer add(Integer i) {
    i = i + 3;
    i = new Integer(i);//3
    return i;
    }
    }
    好像这样才行。(我也是上周才开始学的 java )逃……
    WhyAreYouSoSad
        18
    WhyAreYouSoSad  
       Jul 20, 2018   ❤️ 1
    赞成楼上 2,3 楼说的,其实不要用引用传递这种说法,因为还是值传递的操作影响。我读书时有想过这个问题。
    其实在面向对象里面,所谓的引用传递他是传递一个一级指针(不能改变该对象指向的空间,只能改变该对象成员的值),而所谓的值传递,就是传值。
    如果用 c 语言表达的话就是,如果类要改变指向,那么就必须传一个二级指针过去。而基础类型要改变指向就传一个一级指针,要把一个类当成一个不定长的一维数组。
    raysonx
        19
    raysonx  
       Jul 20, 2018 via Android
    楼主的例子中,add 函数的参数前没加 ref,当然是传值。后续对 i 重新赋值不会影响调用方的变量的值。
    另外,这里 i 的类型用 int 还是 Integer 效果上没有什么区别,不知道楼主为何要用 Integer。
    alamaya
        20
    alamaya  
       Jul 20, 2018
    可以这么理解 java 值传递传的是指针地址的 copy
    LINWAYNE
        21
    LINWAYNE  
       Jul 20, 2018
    Integer 自动拆箱装箱了解一下
    raysonx
        22
    raysonx  
       Jul 20, 2018 via Android
    我猜困惑来自于所谓的值类型和引用类型,后者类似于 C C++的指针。但这个概念和值传递、引用传递没有关系。
    sagaxu
        23
    sagaxu  
       Jul 20, 2018 via Android   ❤️ 1
    内存是货柜,一个单元就是一个抽屉,变量是写了货柜抽屉编号的卡片,int 这种 primitive 类型,直接记卡片上,不放抽屉里。而 Integer 仍然放抽屉,卡片上只记编号,自动装箱拆箱。

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

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

    假设编译器将 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
        31
    momocraft  
       Jul 20, 2018
    都写 java 了就不要担心地址了,引入地址这个(只存在于 JVM 层的)要素只会让你更混乱
    mx1700
        32
    mx1700  
       Jul 20, 2018
    如果把 Integer 换成 String 你能理解吗?
    Integer 和 String 一样,都是不可变对象
    sc13
        33
    sc13  
       Jul 20, 2018
    java 只有值传递的,对象传递的是引用的复制的值
    hyyou2010
        34
    hyyou2010  
       Jul 20, 2018   ❤️ 1
    目前为止,可能只有 @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
        35
    suixn  
       Jul 20, 2018
    String、Integer,Long, Short, Double, Float, Character, Byte, Boolean 都是不可变的。
    意思是只要改变了他们的值,地址映射就变了。
    suixn
        36
    suixn  
       Jul 20, 2018
    按你的代码:
    ```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
        37
    alamaya  
       Jul 20, 2018
    @suixn 这里跟可变不可变没有关系,你在 add 里将 i 重新指向另一个 map,main 里面的 i 也不会受影响
    suixn
        38
    suixn  
       Jul 20, 2018
    @alamaya #37 楼主的例子完全说明不了问题,在函数了直接重新 new 了
    suixn
        39
    suixn  
       Jul 20, 2018
    @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
        40
    alamaya  
       Jul 20, 2018
    @suixn 楼主这个问题的本质就是 java 传参的问题,跟是不是可变类没有关系。
    joshryo
        41
    joshryo  
       Jul 20, 2018
    @alamaya 楼主虽然问的是参数传递问题,但是这里值没有发生变化却是因为 Integer 是不可变类导致的。
    ShineSmile
        42
    ShineSmile  
       Jul 20, 2018
    @sagaxu #1 证据?
    luopengfei14
        43
    luopengfei14  
       Jul 20, 2018
    看来楼主 C 语言没学好
    虽然我是对着答案。。。看懂代码的
    luopengfei14
        44
    luopengfei14  
       Jul 20, 2018   ❤️ 1
    Integer i = new Integer(0);
    ...
    private void add(Integer i)
    ...

    虽然这两个引用(指针)指向同一个实例,但是只是两个指针,后来 add()中 i 的指向变了,也就不会改变原来指向的实例。

    我的想法是这样。。。
    johnj
        45
    johnj  
       Jul 20, 2018   ❤️ 1
    首先 Java 里只有按值传递。所谓按值传递,遇到值类型,复制一份传进去;遇到引用类型,复制一份引用传进去。会出现多个引用指向同一个对象的情况:可以通过每个引用,来改变指向的对象的属性值;其中一个引用改变了指向的对象,对其他的引用没有影响。
    johnj
        46
    johnj  
       Jul 20, 2018
    @jzq526 Java 里只有按值传递(复制),没有按引用传递
    unforgiven
        47
    unforgiven  
       Jul 20, 2018
    @lux182 因为他是参数的内存地址的值得拷贝,所以在访问这个参数的时候指向的是同一个内存地址,当你修改这个参数 i 的时候,你修改的只是这个拷贝的值并不会影响到原来的值
    unforgiven
        48
    unforgiven  
       Jul 20, 2018
    @suixn 和可变不可变没有关系
    @Test
    public void patchModuleRecord() {
    User user = new User("aaa","bbb");
    testan(user);
    System.out.print(user.toString());
    test(user);
    System.out.print(user.toString());
    }

    public void test(User user){
    user = new User("123","456");
    }

    public void testan(User user){
    user.userName = "ccc";
    }

    class User{
    String userName;
    String password;
    public User(String userName, String password){
    this.userName = userName;
    this.password = password;
    }

    public User(){

    }

    @Override
    public String toString() {
    return "User{" +
    "userName='" + userName + '\'' +
    ", password='" + password + '\'' +
    '}';
    }
    }
    User{userName='ccc', password='bbb'}User{userName='ccc', password='bbb'}
    我的 User 对象到是可变的,一样你在函数里修改不了 user 的引用,说到底还是 Java 的所谓引用
    还是传递的内存地址的值,真不了解的话拿着 c 语言的指针去玩玩你就懂了
    jzq526
        49
    jzq526  
       Jul 21, 2018
    @johnj 我想你没有仔细看。我说的是 C 和 Java 的参数传递其实是一样的,形式上都是将实参的值复制给形参。所谓值传递或者地址传递或者引用传递,只是复制的内容不同罢了。Java 中的引用,实际上就是一个受限制的指针。当你用数组做参数或者对象做参数时,复制过去的就是这个数组、对象的地址,也就是指针,或者 Java 中叫做引用。你可以写个方法,参数是一个对象,改变参数中一个属性,再调用这个方法,你看看对象中的属性会不会改变。
    johnj
        50
    johnj  
       Aug 7, 2018
    @jzq526 嗯 如果不看叫法( pass by value / pass by reference )咱俩说的应该是一个意思
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   3817 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 114ms · UTC 00:44 · PVG 08:44 · LAX 17:44 · JFK 20:44
    ♥ Do have faith in what you're doing.