配合JVM+javap解析Java中参数的传递方式与方法返回值方式

1. 参数的传递方式

首先给出答案:Java 中只有值传递,没有引用传递

值传递和引用传递都是一种求值策略,它们的区别并不在于传递的是什么东西,而在于传递方式。

1.1 值传递是什么

不论传递的是什么,值传递会拷贝一份将要被传递的变量,然后将拷贝传递给方法。

例子1:

public static void anInt(int i){
        i = 6;
}

int i = 1;
System.out.println(i);//1
anInt(i);
System.out.println(i);//1

所以方法接收参数后,怎么对参数进行操作,都不会影响原来的值。看到这句话,有人反对了,说:“不对!你看下面的代码就改变了原来的值!”

例子2:

public static void changeField(User user){
    user.setName("thd");
}

User user = new User("123");
System.out.println("before" + user.getName());//123
changeField(user);
System.out.println("after" + user.getName());//thd

这位仁兄没有清晰理解"影响原来的值"这句话。传递给 changeField() 的值实际上是 user 对象的引用,是内存地址。无论 changeField() 方法怎么操作,都不会改变原 user 对象的引用。让我们通过下面两个例子理解这句话。

例子3:

public static void changeObject(User user){
    user = new User("djw");
}

User user = new User("123");
System.out.println("before" + user.getName());//123
System.out.println("before" + user);//User@4554617c
changeObject(user);
System.out.println("after" + user.getName());//123
System.out.println("after" + user);//User@4554617c

changeObject() 方法调用前后,对象引用不变。

例子4:

public static void changeString(String str,Integer integer){
    str = "djw";
    integer = 321;
}

String str = "123";
Integer integer = 123;
System.out.println(str + "," + integer);//123,123
changeString(str,integer);
System.out.println(str + "," + integer);//123,123

String类 ,Integer 等包装类有点特别,因为它们都是不可变类,只能通过构造方法初始化,不向外提供 setter 方法。

str = "djw"; 实际上是将原 str 引用的拷贝指向堆中内容为 “djw” 的字符串对象(已存在)

integer = 321; 实际上是integer = new Integer(321);(Integer 默认缓存范围 -128 ~ 127)

integer = 123 实际上是 integer = Integer.valueOf(123);

深入浅出包装类 : https://blog.csdn.net/qq_44707077/article/details/116030063

1.2 引用传递是什么

不论传递的是什么,引用传递会直接将要被传递的变量传递给方法,不进行拷贝。

例子:C语言指针

1.3 值传递与引用传递区别

求值策略传递方式是否改变原来的值
值传递传递原值的副本不会
引用传递传递原值

1.4 配合局部变量表来分析方法中局部变量是如何读取和修改的

一个方法对应一个虚拟机栈栈帧,栈帧中保存了 局部变量表,操作数栈,动态连接,方法出口。在编译后,一个方法需要使用的内存大小就已经确定了,在方法运行期间不会改变。内存大小的基本单位是局部变量槽,变量槽中可以存储基本类型,对象引用,字节码指令地址。其中除了 long,double 是8字节的需要占用两个变量槽,其他都只占一个。

public static void changeObject(User user){
    int i = 1;
    float f = 1.0f;
    long l = 1L;
    user = new User("djw");
}

例如这个静态方法,局部变量表内容为

在这里插入图片描述

如果是实例方法的话,变量槽0应该为 this 关键字保留,局部变量从变量槽1开始存储。

当方法运行到 user = new User("djw");时,只是将变量槽 1 中的值替换为新的 User 对象的引用,如下图:

在这里插入图片描述

user = new User("djw");对应的字节码指令为:

0: new           #4                  // class keyword/passParam/User
3: dup
4: ldc  将"djw"压入操作数栈顶         #5                  // String djw
6: invokespecial #6   创建User对象,并将对象的引用压入操作数栈顶               // Method keyword/passParam/User."<init>":(Ljava/lang/String;)V
9: astore_1 读取操作数栈顶元素,保存在变量槽1(0x456 -> 0x789)
10: return 方法结束

可见, changeObject() 方法中 user = new User("djw"); 只是将变量槽中的值替换了一下,不会影响原 user 存储的值(内存地址);

2. 方法返回值方式

2.1 配合操作数栈来分析方法返回值

判断以下代码两种情况下的返回值 1.没有异常出现 2.出现 Exception 及其子类的异常(虽然不太可能出现异常)

public static int test1(){
    int i = 0;
    try {
        i = 1;
        return i;
    } catch (Exception e) {
        i = 3;
        return i;
    } finally {
        i = 2;
        System.out.println(i);
    }
}

————————————

————————————

未出现异常,方法返回值为 1;出现能够捕获的异常,方法返回值为 3

2.2 解读反编译结果:

 public static int test1();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=0
         0: iconst_0 将常量 0 压入操作数栈
         1: istore_0 将操作数栈顶元素存入栈帧中的局部变量表变量槽 02: iconst_1 try 块,将常量 1 压入操作数栈顶
         3: istore_0 将操作数栈顶元素存入栈帧中的局部变量表变量槽 04: iload_0 将变量槽 0 中的值(1)赋给局部变量,并将局部变量压入操作数栈(等待 ireturn 指令读取并返回)
         5: istore_1 将返回值存入局部变量表变量槽 16: iconst_2 如果没有出现异常,代码正常运行,进入 finally 块,将常量 2 压入操作数栈
         7: istore_0 将操作数栈顶元素存入局部变量表变量槽 08: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: iload_0 将变量槽 0 中的值(1)赋给局部变量,并将局部变量压入操作数栈顶(等待被输出)
        12: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
        15: iload_1 将变量槽 1 中的值(1)赋给局部变量,并将局部变量压入操作数栈顶(等待被返回)
        16: ireturn 将操作数栈顶的局部变量返回,方法正常结束
            
        17: astore_1 出现 Exception 及其子类异常,被捕捉,进入 catch18: iconst_3 将常量 3 压入操作数栈 
        19: istore_0 将操作数栈顶元素存入栈帧中的局部变量表变量槽 020: iload_0 将变量槽 0 中的值(1)赋给局部变量,并将局部变量压入操作数栈(等待 ireturn 指令读取并返回)
        21: istore_2 将操作数栈顶元素(返回值)存入变量槽 222: iconst_2 处理异常完成,进入 finally23: istore_0
        24: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
        27: iload_0
        28: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
        31: iload_2 
        32: ireturn 通过 catch 块中的 return 语句返回
        33: astore_3 出现无法处理的异常,进入 finally 块,将该异常对象存入局部变量表变量槽 334: iconst_2 
        35: istore_0
        36: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
        39: iload_0
        40: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
        43: aload_3 将变量槽 3 中的值赋给局部变量,并将局部变量压入操作数栈顶(等待抛出)
        44: athrow 抛出异常对象
      Exception table:
         from    to  target type
             2     6    17   Class java/lang/Exception
             2     6    33   any
            17    22    33   any

2.2 解读 Exception table

Exception table:
	from    to  target type
 	   2     6    17   Class java/lang/Exception
 	   2     6    33   any
 	   17    22    33   any

from 2 to 6 target type 17:try 块代码从 2-6。如果 try 块中出现 Exception 及其子类异常,则跳转至 17(catch 块)。

from 2 to 6 target type 33:try 块代码从 2-6。如果 try 块中出现 Exception 及其子类以外的异常,则跳转至 33(finally块)。

from 17 to 22 target type 33:catch 块代码从 17-22。如果 catch 块中出现任何异常,则跳转至 33(finally块)。

2.3 总结反编译结果

test1() 方法共使用到了4个变量槽来存储局部变量 0 1 2 3。其中变量槽 0 用于存储局部变量 i 的值
在这里插入图片描述

解读完反编译结果后我们可以发现,每当运行到方法结束指令时,都会先执行一个 aload 指令。

aload 指令在这里的作用是读取对应变量槽中的值并压入操作数栈顶。例如:

运行到 ireturn 字节码指令前,通过 aload_1 / aload_2 读取变量槽1 / 变量槽2 中的局部变量(返回值)并压入操作数栈顶,等待 ireturn 指令返回。

第 15 行 aload_1 是将变量槽1中的局部变量压入栈顶,然后执行 try 块中的 return 语句。(未出现异常的情况)

第 31 行 aload_2 是将变量槽2中的局部变量压入栈顶,然后执行 catch 块中的 return 语句。(出现可捕获异常的情况)

运行到第44行 athrow 指令前,通过 aload_3,将变量槽3中的异常对象压入操作数栈顶,等待抛出(出现无法捕获异常的情况)

2.4 小细节

第 17 行 astore_1 语句,将异常对象存入变量槽1,但变量槽1已经用于存储返回值的。这样不会乱套吗?答案是不会的。

实际上当运行至 17 行时,代表代码进入 catch 块,也就是出现了异常并被捕获了。变量槽1中的返回值,也就是未出现异常时的方法返回值,也就没用了。jvm 为了节省内存空间,复用了变量槽1,将异常对象存入。
在这里插入图片描述

end…

  • 13
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值