专栏原创出处:github-源笔记文件 github-源码 ,欢迎 Star,转载请附上原文出处链接和本声明。

Java 核心知识专栏系列笔记,系统性学习可访问个人复盘笔记-技术博客 Java 核心知识

# 一、关于值传递引用传递的结论

公网上流传的绝大多数文章都是在研究「Java 到底是值传递还是引用传递」。并且一致认为是值传递,但是证明的过程并没有充分的理由。

虽然最终大部分人“证明”是值传递,那么为什么是值传递,值是什么?怎么传递的?修改这个值以后为什么不会改变原始值(方法参数)?

先下结论

在此总结一下个人的理解,后续以字节码的分析进行说明。

———— 所谓的传递是 拷贝了 对象的引用指针地址或者基本类型的值到栈帧的局部变量表中

  • 如果方法中没有重新赋值操作,一直操作的是方法参数的数据。
  • 如果进行了重新赋值操作 (参数名称 = 新的数据),相当于把拷贝在局部变量表内的数据替换了。

该部分研究需要虚拟机相关知识,尤其是栈帧,方法的调用过程,字节码指令。

相关知识可以从 Java JVM(JDK13)-专栏文章目录汇总 了解。

局部变量表简介

方法执行时,传入的方法参数与方法内部创建的变量都保存在栈帧的局部变量表中,每个槽位保存一个变量,对应的名字是惟一的。

因为类实例方法隐式的会传入一个 this 引用(方法参数的第 0 个位置),所以包含 N 个参数的方法槽位内容一般是:

第 0 个槽位:隐式传入的 this 对象引用
第 1 个槽位:方法的第 1 个参数
第 2 个槽位:方法的第 2 个参数
第 3 个槽位:方法的第 3 个参数
第 N 个槽位:方法的第 n 个参数

第 N+1 个槽位:方法内部声明的第 1 个变量
第 N+2 个槽位:方法内部声明的第 2 个变量
第 N+3 个槽位:方法内部声明的第 3 个变量
...

举个例子,我们的方法及注释如下

// 槽位 0:this,槽位 1:o 的对象的引用指针,假设地址为 x011
Object refUpdate(Object o) { 
    
   // 生成新对象的引用地址假设为 x012 ,重新赋值名称为 o 的局部变量,此时直接覆盖槽位 1 的对象的引用指针地址(重点!!!) 
   // x011 被替换为 x012
   o = new Object();        
   
   // 后续我们操作的都是新创建的对象引用 x012,方法传入的 o 无法使用了
   int hashCode = o.hashCode(); 
   return o;
}

# 二、引用类型作为方法参数

  1. 我们以下面代码块中 refOriginal 方法进行字节码分析。
    public Object refOriginal(Object o) { // 不改变方法参数的引用
        return o;
    }
    字节码:
         0: aload_1 // 将局部变量表槽位 1 压入栈顶(方法参数 o)
         1: areturn // 将栈顶数据返回
      局部变量表:
        Start  Length  Slot  Name   Signature
            0       2     0  this   LPassByRef; // 槽位 0,当前对象引用 this
            0       2     1     o   Ljava/lang/Object; // 槽位 1,方法的第一个 Object 参数 o
  1. 我们以下面代码块中 refUpdate 方法进行字节码分析。(重点理解: 7: astore_1
    public Object refUpdate(Object o) {  // 改变方法参数的引用
        o = new Object();
        int hashCode = o.hashCode();
        return o;
    }
    字节码:
         0: new           #2 // 创建一个 Object 对象引用并压入栈顶
         3: dup              // 复制 Object 对象引用并压入栈顶
         4: invokespecial #1 // 调用 Object 构造方法,使用一个引用

         7: astore_1         // 因为赋值操作 o = new Object() ,赋值的符号名称和方法参数都是 o ,
                             // 所以将新创建对象的引用放入槽位 1(相当于替换了方法参数的 o)

         8: aload_1          // 将 o 引用压入栈顶,准备调用 hashCode 方法
         9: invokevirtual #4 // 调用 hashCode 方法返回 int 值
        12: istore_2         // 将 int 值放入局部变量表中,槽位为 2,名称为 hashCode 
        13: aload_1          // 将 o 引用压入栈顶,准备返回
        14: areturn          // 返回 o 引用
      局部变量表:
        Start  Length  Slot  Name   Signature
            0      15     0  this   LPassByRef; // 槽位 0,当前对象引用 this
            0      15     1     o   Ljava/lang/Object; // 槽位 1,方法的第一个 Object 参数 o
           13       2     2 hashCode   I        // 槽位 2,hashCode int 值

# 三、基础数据类型作为方法参数

  1. 我们以下面代码块中 intOriginal 方法进行字节码分析。
    public int intOriginal(int o) { // 不改变方法参数基础数据的值
        return o;
    }
    字节码:
         0: iload_1 // 将局部变量表槽位 1 压入栈顶(方法 int 参数 o)
         1: ireturn // 将栈顶数据返回
      局部变量表:
        Start  Length  Slot  Name   Signature
            0       2     0  this   LPassByValue; // this
            0       2     1     o   I   // 槽位 1,方法的第一个 int 参数 o
  1. 我们以下面代码块中 intUpdate 方法进行字节码分析。(重点理解: 4: istore_1
    public int intUpdate(int o) { // 改变方法参数基础数据的值
        o = o + 10;
        return o;
    }
    字节码:
         0: iload_1     // 将局部变量表槽位 1 压入栈顶(方法 int 参数 o)
         1: bipush  10  // 将 10 压入栈顶
         3: iadd        // 将栈顶的 2 个数出栈相加后压入栈顶
         4: istore_1    // 将相加的结果放入槽位 1(int 参数 o)
         5: iload_1     // 将槽位 1(int 参数 o)出栈,准备返回
         6: ireturn     // 将栈顶数据返回
      局部变量表:
        Start  Length  Slot  Name   Signature
            0       7     0  this   LPassByValue;// this
            0       7     1     o   I // 槽位 1,方法的第一个 int 参数 o

# 总结

通过字节码的分析我们明确了方法调用及方法参数在方法内部的作用过程。

方法调用时,拷贝一份方法参数的引用指针地址或基本数据到局部变量的槽位中,重新赋值就会替换对应槽位的数据。

其中对字节码的分析可参考 Java JVM(JDK13)-专栏文章目录汇总 , 比如为什么 new 指令后跟随着一个 dup 指令,如果创建对象后不赋值怎么处理(类似匿名类) ?

# 专栏更多文章笔记

最后修改时间: 2/17/2020, 4:43:04 AM