问题 现象一交换两个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;@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;@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的取值,超过这个区间的话, 那么就不会有这个问题,比如:
这个问题好奇怪啊,为什么呢?一个脑袋两个大了。。。。。
解答这个问题其实很简单,需要从两个角度看这个问题。
integer这个包装类是个神奇的类, 首先会有这个疑问:integer 明明是包装类,为什么可以这样用
为什么 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 3 4 5 6 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 { 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)); }
这样也是没有问题的,这也是比较建议的防范。