问题解析--两个Integer数值交换

问题

现象一

交换两个Integer的值,题目代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

package com.example.demo;

import cn.hutool.core.codec.Base64;
import cn.hutool.crypto.SecureUtil;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import sun.awt.SunHints;

import java.lang.reflect.Field;

/**
* @jpsDesc aaaabb
* @jpTag 测试测试
*/
@SpringBootApplication
@MapperScan("com.example.demo")
@RequestMapping(path = "/aaa")
@RestController
@Configuration
public class DemoApplication {

Integer a=127, b=129;
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {

DemoApplication test = new DemoApplication();

System.out.println("a: "+test.a+" ---- "+test.a.toString());
System.out.println("b: "+test.b+" ---- "+test.b.toString());
test.swap(test.a, test.b);
System.out.println("");
System.out.println("");
System.out.println("a: "+test.a+" ---- "+test.a.toString());
System.out.println("b: "+test.b+" ---- "+test.b.toString());

}
public void swap(Integer a, Integer b) throws NoSuchFieldException, IllegalAccessException {
Integer temp=a;
a=b;
b=tmp;
}
}

测试上面的代码,我们会发现两个integer根本没有交换,这是为什么呢?
因为:swap中的integer类型是main方法中integer引用的副本,所以swap中的代码只是交换了副本的引用,不会对main中的integer造成影响

现象二

既然无法通过引用交换,通过查看integer的源码:

1
private final int value;

integer包装类中也会保存基本类型数据,但是这个数据是私有,并且是代码不可变的,那么这种情况下,我们只能通过反射的方式来解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.example.demo;

import cn.hutool.core.codec.Base64;
import cn.hutool.crypto.SecureUtil;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import sun.awt.SunHints;

import java.lang.reflect.Field;

/**
* @jpsDesc aaaabb
* @jpTag 测试测试
*/
@SpringBootApplication
@MapperScan("com.example.demo")
@RequestMapping(path = "/aaa")
@RestController
@Configuration
public class DemoApplication {

Integer a=3, b=5;
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {

DemoApplication test = new DemoApplication();

System.out.println("a: "+test.a+" ---- "+test.a.toString());
System.out.println("b: "+test.b+" ---- "+test.b.toString());
test.swap(test.a, test.b);
System.out.println("");
System.out.println("");
System.out.println("a: "+test.a+" ---- "+test.a.toString());
System.out.println("b: "+test.b+" ---- "+test.b.toString());

}

public void swap(Integer a, Integer b) throws NoSuchFieldException, IllegalAccessException {
Field value = Integer.class.getDeclaredField("value");
value.setAccessible(true);
int temp = a.intValue();
value.set(a, b.intValue());
System.out.println(temp);
value.set(b, temp);
}
}

以上的代码,理论上是行的通的,但是实际运行的结果是这样的

1
2
3
4
5
a: 3    ---- 3
b: 5 ---- 5
3
a: 5 ---- 5
b: 5 ---- 5

啊哈,问题来了,和我们预期的结果是不一样的,为什么呢???? 仔细查看代码,还是觉得没有任何问题。

现象三

在现象二的基础上,经过我的多个测试,默认情况下,发现 a 和 b 的取值区间只要在[-128, 127), 那么这个问题就会一直存在。
但是如果我把,a和b的取值,超过这个区间的话, 那么就不会有这个问题,比如:

1
Integer a=1000, b=2000;

这个问题好奇怪啊,为什么呢?一个脑袋两个大了。。。。。

解答

这个问题其实很简单,需要从两个角度看这个问题。

integer

这个包装类是个神奇的类,
首先会有这个疑问:integer 明明是包装类,为什么可以这样用

1
Integer a=3, b=5;

为什么 integer == int , 包装类可以等于基本类型,这是为什么的,首先我们要搞清楚这个问题。
我们把上面我们的代码,反编译一下,来看看字节码
具体字节码看不懂的,请看我的另一篇文章:jvm指令集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class com.example.demo.DemoApplication {
java.lang.Integer a;

java.lang.Integer b;

public com.example.demo.DemoApplication();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_3
// 重点!!!!!!!!!
6: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
9: putfield #3 // Field a:Ljava/lang/Integer;
12: aload_0
13: iconst_5
// 重点!!!!!!!!!
14: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
17: putfield #4 // Field b:Ljava/lang/Integer;
20: return

public static void main(java.lang.String[]) throws java.lang.NoSuchFieldException, java.lang.IllegalAccessException;
Code:
0: new #5 // class com/example/demo/DemoApplication
3: dup
4: invokespecial #6 // Method "<init>":()V
7: astore_1
8: getstatic #7 /

通过查看标注的两个重点内容地方,这个很重要,
我们会发现,到了底层的字节码,jvm帮我们做的优化,其实

1
2
3
Integer a = 2;
// 等价于
Integer a = Integer.valueOf(2);

这个问题我们已经了解了,那么我们来看看valueOf 这个方法里到底写了什么:

1
2
3
4
5
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

what?原来integer 自己有缓存。。。 继续查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];

static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;

cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);

// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}

private IntegerCache() {}
}

原来默认情况下[-128, 127)这个区间的数据,已经被默认缓存了。
这个解释了为什么上述数据中[-128, 127)这个区间会出现异常结果,那这是为什么,继续分析
既然我们已经知道有缓存了,那么可以发现,其实a和b对象,从一开始拿到的都是缓存数据的引用,并没有重新创建新的对象,也就是说

1
value.set(a, b.intValue());

当执行到这句代码的时候,其实我们把原本缓存中的integer a中的3, 变成了5
其实这个就是最终的原因,这个要记住,一会下面还会用到它。

反射

理论上反射字段数据,应该直接赋值数据的,不会走方法,除非明确调用了某个方法,但是其实包装类是被jvm优化过得,
我们可以在

1
2
// temp == 5 , 上次保存下来的值,这个int 不可能是引用,基本类型只会是值引用
value.set(b, temp);

1
2
3
4
5
6
// Integer.valueOf
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

打个断点观察,很快就是发现,当执行到set方法的时候,直接调用的valueOf方法,我们在来看看字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public void swap(java.lang.Integer, java.lang.Integer) throws java.lang.NoSuchFieldException, java.lang.IllegalAccessException;
Code:
0: ldc #20 // class java/lang/Integer
2: ldc #21 // String value
4: invokevirtual #22 // Method java/lang/Class.getDeclaredField:(Ljava/lang/String;)Ljava/lang/reflect/Field;
7: astore_3
8: aload_3
9: iconst_1
10: invokevirtual #23 // Method java/lang/reflect/Field.setAccessible:(Z)V
13: aload_1
14: invokevirtual #24 // Method java/lang/Integer.intValue:()I
17: istore 4
19: aload_3
20: aload_1
21: aload_2
22: invokevirtual #24 // Method java/lang/Integer.intValue:()I
// 重点!!!!
25: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
28: invokevirtual #25 // Method java/lang/reflect/Field.set:(Ljava/lang/Object;Ljava/lang/Object;)V
31: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
34: iload 4
36: invokevirtual #26 // Method java/io/PrintStream.println:(I)V
39: aload_3
40: aload_2
41: iload 4
// 重点!!!!
43: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
46: invokevirtual #25 // Method java/lang/reflect/Field.set:(Ljava/lang/Object;Ljava/lang/Object;)V
49: return

啊哈,这下答案已经出来了,反射执行了set,但是实际还是调用了Integer.valueOf方法,
然后我们在回归valueOf 方法,首先temp == 3,
但是因为[-128, 127)这个区间的原因,使用的缓存
有因为原本缓存3的integer, 因为

1
value.set(a, b.intValue());

已经变成了5,

所以在执行b的反射的时候,尽管输入是3, 但实际缓存中的值为5
这就是答案了,看似很简单,其实这个问题很不简单,长见识了!!!!

正确答案

启动参数

1
2
3
4
5
6
7
8
9
10
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];

static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");

通过看源码我们发现,只需要关闭缓存(high = -128)就没有问题了

跳过缓存

在swap方法中,跳过缓存

1
2
3
4
5
6
7
8
9
public void swap(Integer a, Integer b) throws NoSuchFieldException, IllegalAccessException {

Field value = Integer.class.getDeclaredField("value");
value.setAccessible(true);
int temp = a.intValue();
value.set(a, new Integer(b.intValue()));
System.out.println(temp);
value.set(b, new Integer(temp));
}

这样也是没有问题的,这也是比较建议的防范。