文章大纲
值传递 和 引用传递 的区别
Integer.valueOf(num)
画图分析Integer的赋值
Synchronized感受值传递
画图分析值传递-地址值
先来看看下面这段 Java 代码, 单元测试能不能跑通呢?
@Test
public void demo() {
Integer a = 888; // @1
Integer b = change1(a); // @2
Assert.assertTrue(a == 999);
}
private Integer change1(Integer a) {
a = 999; // @3
return a;
}答案是不能, @1 中实参 Integer a 的值还是 888 。
其实对于上面的问题, 很多人感到疑惑的地方是, @2 中方法 change1 的形参 a , 接收到的是 Integer a 的数值, 还是 Integer a 的地址引用呢?
如果你已经知道答案, 那么本文也没必要往下看了。
先直接说一下 结论 吧! 方法调用的时候都是 值传递 :
对于一个局部变量 int a = 888, 这个传递给方法形参的是 数值 888 ;
对于一个局部变量 Integer a = 888, 那么传递给方法形参的则是一个 地址值 ;
这里要说明一下, Java中只有值传递!
关于引用传递和值传递的区别, 我觉得网上一个比喻非常好, 在这里摘抄一下(原文链接在末尾):
“你有一把钥匙,当你的朋友想要去你家的时候,如果你直接把你的钥匙给他了,这就是 引用传递 。这种情况下,如果他对这把钥匙做了什么事情,比如他在钥匙上刻下了自己名字,那么这把钥匙还给你的时候,你自己的钥匙上也会多出他刻的名字。你有一把钥匙,当你的朋友想要去你家的时候,你复刻了一把新钥匙给他,自己的还在自己手里,这就是 值传递 。这种情况下,他对这把钥匙做什么都不会影响你手里的这把钥匙。但是,不管上面哪种情况,你的朋友拿着你给他的钥匙,进到你的家里,把你家的电视砸了。那你说你会不会受到影响?你改变的不是那把钥匙,而是钥匙打开的房子。”
那么值传递中 数值 和 地址值 有什么区别呢?回答这个问题我们要先理解整形的赋值。
1 Integer.valueOf()
整形的赋值操作底层其实是调用了 Integer.valueOf(num) 方法。
所以 , Integer a = 888 这句代码其实等价于:
Integer a = Integer.valueOf(888); 来看看 Integer.valueOf 的代码实现:
/**
*
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
*
* @param i an {@code int} value.
* @return an {@code Integer} instance representing {@code i}.
* @since 1.5
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}也就是说, 如果整数的范围是在[-128, 127]区间内的, 是直接从池中读取; 否则的话是直接 new Integer(i); 。
所以 , Integer a = 888 这句代码其实等价于下面这句代码:
Integer a = new Integer(888);而 int a = 999 这句代码其实等价于下面这句代码(这里涉及到拆箱):
int a = new Integer(888).intValue();理解了上面的源码,自然也就理解了下面两个单元测试代码的差异。
先看看这个超出[-128, 127]区间的赋值案例:
@Test
public void demo1_1() {
Integer a = 888; // 实际上调用的是Integer.valueOf(888);
Integer b = 888; // 每次都是 new Integer()
Assert.assertFalse("a == b ", a == b);// 通过, a != b
}上面的代码画出内存布局图会更好理解, 这里的局部变量指向的其实就是堆中的地址值:

再看看[-128, 127]区间内的赋值案例:
@Test
public void demo1_2() {
Integer a = 88; // [-128, 127]会读缓存
Integer b = 88; // 读缓存
Assert.assertTrue("a == b ", a == b); // 通过, a == b
}内存布局图如下, 这里的局部变量表示的其实就是数字本身的值:

2“感受”值传递
我们可以用关键字 synchronized 来感受下 值传递 中数值和地址值的差异。
对于 int 类型, 因为传递的是数值 888, 而 synchronized 无法对一个数值加锁, 所以编译错误:
int numInt = 888; // 值传递 888
synchronized (numInt) { } // 编译错误那么如果是 Integer 类型, 见下面的例子, 传递的就是 new Integer(888) 的地址值。
@Test
public void demo2() {
Integer lockInteger = 888; // 值传递 new Integer(888)的地址
try {
for (int i = 0; i < 20; i++) {
Runnable run = () -> {
synchronized (lockInteger) {
num += 1;
System.out.println(num);
}
};
executorService.submit(run);
}
} finally {
executorService.shutdown();
}
}
private final int poolSize = Runtime.getRuntime().availableProcessors() * 2;
private ExecutorService executorService = new ThreadPoolExecutor(
poolSize, poolSize, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(100)
);
private static int num = 0;如果我们用多个线程对变量 int num 进行并发累加, 没有锁的情况下输出肯定是乱序的。但是加上这句 synchronized (lockInteger) 后, 就没有了乱序输出, 从而证明了 Integer 传入的是同一个对象的地址值。
3 内存布局图
有了上面的知识储备, 那么相信大家对本文开头的案例不再感到困惑了。
下面我们对着案例画一下内存分配图, 更加简单直观地理解代码。
@Test
public void demo3_1() {
Integer a = 888; // @1 实际上调用的是Integer.valueOf(888);
Integer b = change1(a); // @2 值传递
Assert.assertTrue(a == 888); // 通过
Assert.assertFalse("a == b", a == b); // 通过, a != b
}
private Integer change1(Integer a) { // @3
a = 999; // @4 其实是new Integer(999)
return a;
}首先是 @1 这里, 栈内存中会为实参 a 分配一块空间, 因为赋值是 888,也就是在堆内存中分配了一块空间 0x0001 存放 new Integer(888);
此时的内存布局如下图所示,实参 a 指向的地址值,就是new Integer(888) 的地址。

接下来到了 @3 这里, 首先在栈内存中为 change1 的形参 a 分配一块空间,
因为是值传递, 也就是把实参 a 的值 0x0001 复制了一份给形参 a ,形参 a 最终指向堆内存中的 new Integer(888) 。

到了 @4 这行代码, change1里面的赋值操作 a = 999, 其实就是在堆内存里分配一块新的空间 0x0002 存放 new Integer(999) ,形参 a 最终引用的堆内存地址也从原先的 0x0001 变成了 0x0002。

最后看@2这行代码, 方法change1 返回的值再赋值给 实参b,最终实参 b 指向的地址值就变成了 0x0002。

所以最后实参 a 和 实参 b 指向的已经是不同的对象了。
而方法 change1 执行完之后, 栈中分配的内存会被回收。

下面这段单元测试代码, change2 方法没有改变形参指向的地址:
@Test
public void demo3_2() {
Integer a = 888; // @1
Integer b = change2(a); // @2 值传递
Assert.assertTrue(a == 888); // 通过
Assert.assertTrue("a == b", a == b); // 通过
}
private Integer change2(Integer a) { // @3
return a;
}代码执行到 @3 这行,也就是方法 change2 这里时, 内存分布如下:

接着到了 @2 这行实参 b 赋值完成, 内存分布如下:

所以最后实参 a 和 实参 b 就是同一个对象。
方法 change2 执行完之后, 为其在栈中分配的局部变量内存会被回收。

完结撒花!
X References
为什么说Java中只有值传递
https://blog.csdn.net/bjweimengshu/article/details/79799485
来自公众号 Hollis
往期回顾
码字不易, 记得关注公众号"字节武装"