浅析Integer类型传参值不变来理解Java值传参
以前对java值的引用传递有一些疑惑,将Integer和String传入方法中进行修改,但最后值却没有修改,现在经过不断的学习以后,对这里有了一些新的体会,现在总结一下。
代码过程
(1)先上代码
private void add(Integer i) {
i = i - 1;
}
private void reverse(String s) {
s = "sey";
}
public static void main(String[] args) {
Integer i = 1;
String s = "yes";
Test test = new Test();
test.add(i);
test.reverse(s);
// 打印值
System.out.println(String.format("i的值:%d", i));
System.out.println(String.format("s的值:%s", s));
}
(2)打印结果如下:
可以看到值没有改变,接下来我来浅析一下这是为什么。
(3)反编译如下:
public class Test
{
public Test()
{
}
private void add(Integer i)
{
i = Integer.valueOf(i.intValue() - 1);
}
private void reverse(String s)
{
s = "sey";
}
public static void main(String args[])
{
Integer i = Integer.valueOf(1);
String s = "yes";
Test test = new Test();
test.add(i);
test.reverse(s);
System.out.println(String.format("i的值:%d", new Object[] {
i
}));
System.out.println(String.format("s的值:%s", new Object[] {
s
}));
}
}
我们可以明显的看出由于java语法糖的缘故,Integer i = 1;实质上是 Integer.valueOf(1)。接着深入Integer源码看一下:
熟悉的朋友都会知道,Integer有一个-128-127的一个缓存,在这个区间内会直接从IntegerCache中获取缓存返回,超出这个区间则返回一个新的对象。
原理分析
本次我从虚拟机栈和堆的来进行探讨:
首先JVM模型如下:
(1)显示具体字节码
执行 javap -c Test.class 命令分解方法代码,显示每个方法具体的字节码
public class Test {
public Test();
Code:
0: aload_0 // 装载局部变量表[0]位置的变量(一般是this对象)
1: invokespecial #1 // 初始化方法
4: return // 方法结束
public static void main(java.lang.String[]);
Code:
0: iconst_1 // 将常量1压入操作数栈
1: invokestatic #3 // 装箱操作(Integer.valueOf()),返回一个对象并且压入栈顶
4: astore_1 // 栈顶元素出栈,并将引用存入局部变量表[1]的位置
5: ldc #5 // String yes 把常量池中的项压入栈
7: astore_2 // 栈顶元素出栈,并将引用存入局部变量表[2]的位置
8: new #6 // 创建Test对象(堆上分配内存,返回引用),并将引用压入栈顶
11: dup // 栈顶元素出栈,并将栈顶元素复制
12: invokespecial #7 // 栈顶元素出栈,并且调用实例化init()方法
15: astore_3 // 栈顶元素出栈,并将引用存在局部变量表[3]的位置
16: aload_3 // 装载局部变量表[3]的引用 -----> 对应test
17: aload_1 // 装载局部变量表[1]的引用 -----> 对应i
18: invokespecial #8 // 调用实例化方法test,add()方法
21: aload_3 // 装载局部变量表[3]的引用
22: aload_2 // 装载局部变量表[2]的引用
23: invokespecial #9 // 调用实例化方法test,reverse()方法
26: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
29: ldc #11 // String i的值:%d
31: iconst_1
32: anewarray #12 // class java/lang/Object
35: dup
36: iconst_0
37: aload_1
38: aastore
39: invokestatic #13 // Method java/lang/String.format:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
42: invokevirtual #14 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
45: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
48: ldc #15 // String s的值:%s
50: iconst_1
51: anewarray #12 // class java/lang/Object
54: dup
55: iconst_0
56: aload_2
57: aastore
58: invokestatic #13 // Method java/lang/String.format:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
61: invokevirtual #14 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
64: return
}
(2)我们知道Java虚拟机是线程私有的,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息,每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程,这次我们讨论虚拟机栈的这两个方法的栈帧:main(),add()。
(3)由图所示,main对应一个栈帧,栈帧中的局部变量表元素i 分别指向堆中的i,s指向堆中的s。
当在main()方法中执行到字节码的第 18行: invokespecial #8 // 调用实例化方法test,add()方法
时候因为Java是引用传递,所以会把局部变量表中的 i 的引用地址,传递给实例化对象test的add()方法中的i参数,同时add()方法被调用,压入add()方法的栈帧进入java虚拟机栈中,同时该栈帧拥有自己的局部变量表 i 指向堆中内存 i 。
因为 i 此时是局部变量,仅仅存在add的栈帧中,当执行代码
i = i - 1;(等同于执行了 i = Integer.valueOf(i - 1) );
如图所示,在add()方法的栈帧中的局部变量表中 i 内存指向 i1。
综上所诉,我们可以看出,原先main()方法的栈帧中的局部变量表 i 的内存地址指向并没有发生任何的改变,所以自然在打印的时候也不会发生任何的改变。仅仅只是add()栈帧中的局部变量表里面的 i 的指向堆中的内存地址发生了改变,并不会影响到main()方法局部变量表中的 i 。
总结
Java中的传递方式是引用传递,在方法中如果要修改变量的值,只能修改原本变量指向堆中内存的值,而不能通过在方法中改变对象的引用地址来进行修改。
public class Test {
Integer x = 1;
Integer y = 2;
/**
* 交换变量
*/
private void swap(Integer i1, Integer i2) {
i1 = i1 ^ i2;
i2 = i1 ^ i2;
i1 = i1 ^ i2;
}
private void swap(Test test) {
Integer x = test.x;
Integer y = test.y;
x = x ^ y;
y = x ^ y;
x = x ^ y;
test.x = x;
test.y = y;
}
public static void main(String[] args) {
Integer i = 1;
Integer j = 2;
Test test = new Test();
test.swap(i, j);
System.out.println(i);
System.out.println(j);
test.swap(test);
System.out.println(test.x);
System.out.println(test.y);
}
}
这样就能成功交换变量的值了:结果如下