画图理解Java Integer的“值传递”

2,456 阅读6分钟


文章大纲
  • 值传递 和 引用传递 的区别

  • 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


往期回顾


码字不易, 记得关注公众号"字节武装"