原来你是这样的 IntegerCache

原来你是这样的 IntegerCache

本段内容与题目无关,是我自己对这段时间的反思。实习以来的确学到了不少技术,Spring Boot、Gradle 等,然而我却越是迷茫,一会儿这个框架没听过要不要去搞搞,一会儿这个是什么协议,机器学习和大数据挺火的整整吧,学会儿前端吧毕设用得上,什么是SSL……本想着这会儿学习 A ,却在学习 A 的过程中看到了上面那些不会的 B、C、D 等。
我反思一下还是自己太过于浮躁,什么事都想速成!既羡慕别人 Github 上一个项目 2000+ 的 Star,又羡慕别人能用英语写出流利的技术文档。我高估了自己的实力,明明自己 Java 基础都没搞懂,连个渣渣儿都算不上呢,成天想那些目前与自己毫不沾边的东西有毛用?还有一个重要的点是,我只看到了人家表面上的东西,而看不到背地里付出的努力。
想不劳而获,天上掉馅饼可能嘛?
规划:白天没活儿时,继续看《 Spring 实战 》,看 Java 基础。

一个关于 Integer 源码的面试题
这是前些天这个公众号推送的一篇文章,说实话我读起来比较吃力,尤其是后边那一节,真是摸不着头脑。我又是个爱钻牛角尖的人儿,也不想放弃这次学习源码的机会,来吧自己搞起来。

基本数据类型与封装类的转化

这里只针对 Integer 来说

不知道你怎么看下边的这段代码,你可能会说,不就是自动装箱吗,基本数据库类型 int 转化成了其包装类 Integer 。确实是,但是只知道这些还远远不够

Integer x=12;
Integer y=1;

一个基本数据类型直接赋值给一个对象类型,这其中肯定调用了某个方法或 JVM 对其进行了转化。单针对上面的代码进行分析,反编译 .class 文件看看。

 Integer x = Integer.valueOf(12);
 Integer y = Integer.valueOf(1);

原来是调用了 Integer.valueOf(int i) 方法,这个方法就把我们即将要讨论的 IntegerCache 带出来了。

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

这个之前也知道,当 i 在 [-128,127] 的范围时,直接把静态内部类 IntegerCache 中已经存在这些 Integer 对象返回,如果 i 不在此范围内就 new Integer(i) 返回。也就是说以这种方式创建两个相同且在 [-128,127] 范围内的 Integer 对象,这两个引用指向堆中相同的一个 Integer 对象。

这个知道了就会懂下面这个面试题:

Integer a1 = 100;
Integer a2 = 100;

Integer b1 = 300;
Integer b2 = 300;

System.out.println("a1==a2:"+(a1==a2));//a1==a2:true
System.out.println("b1==b2:"+(b1==b2));//b1==b2:false

Integer x=0;
Integer y=129;
Integer x1=new Integer(0);
Integer y1=new Integer(129);
System.out.println(x==x1);// false
System.out.println(y==y1);// false

这个也知道了,那对于这道题还差点火候。

反射

Java 中只存在『值传递』,所谓的对象传递,传递的是对象所在的堆内存的首地址,本质也是『值传递』,因此我们不可能通过调用『普通方法』来实现修改一个 对象/基本数据类型 的值。

说到底,我们只是需要一个方法(令方法名为 public static void change(Integer i1,Integer i2) ),把两个 Integer 引用传递过来,之后再做修改:

  1. 首先获取 Integer 中的 private final int value;
  2. 然后再调用 setAccessible(true) 跳过安全检查(操作私有方法或私有属性)
  3. 然后再调用 set(a,b) 方法,设置值。

因为 value 是 private 的,我们只能选用 getDeclaredField(String name)

//只能获取本类中的全部属性,不包括继承
public Field getDeclaredField(String name)
        throws NoSuchFieldException, SecurityException {
        //Identifies the set of declared members of a class or interface. Inherited members are not included.
        checkMemberAccess(Member.DECLARED, Reflection.getCallerClass(), true);
        Field field = searchFields(privateGetDeclaredFields(false), name);
        if (field == null) {
            throw new NoSuchFieldException(name);
        }
        return field;
    }
    
    
//获取包括继承在内的 public 属性
 public Field getField(String name)
        throws NoSuchFieldException, SecurityException {
        // Identifies the set of all public members of a class or interface,including inherited members.
        checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true);
        Field field = getField0(name);
        if (field == null) {
            throw new NoSuchFieldException(name);
        }
        return field;
    }

第一、二步都解决了,就剩第三步了。可能你觉得没啥,不就是整一个中间变量保存住其中一个的值就行了吗,于是你得心应手的写出了一下代码:

public static void main(String[] args)throws Exception {
        Integer x = 100;
        Integer y= 80;
        System.out.println("交换前:x="+x+";y="+y);//交换前:x=100;y=80
        changeWrong1(x,y);
        System.out.println("交换后:x="+x+";y="+y);//交换后:x=80;y=80
}

public static void changeWrong1(Integer i1,Integer i2) throws Exception{
        Field fun = Integer.class.getDeclaredField("value");
        fun.setAccessible(true);
        Integer swap = i1;
        fun.set(i1,i2);
        fun.set(i2,swap);
}

然后运行一看,才发现自己如此粗心,栈中的引用 swap 与 i1 指向堆区同一个 Integer 对象,在调用 fun.set(i1,i2); 时已经把此对象中的 value 值修改成 80 了。所以当调用 fun.set(i2,swap); 时,并不能正确的修改 i2 指向的 Integer 对象。

那这样好了,我设置个 int 变量保存 i2 对象的 value 值,他一定不会变!于是你把 change 函数改成了下面这样:

private static void changeWrong2(Integer x, Integer y) throws Exception{
        Field fun = Integer.class.getDeclaredField("value");
        fun.setAccessible(true);
        //自动拆箱时就是调用的  x.intValue()
        int swap = x;
        fun.set(x,y);
        fun.set(y,swap);
}

你满怀自信地敲了 Enter 键,心里美滋滋~ 然而,程序运行结果却仍然和上一次一样。心中有些许疑问,这次 swap 可是基本数据类型,他一定不会改变的,然而 y 却仍旧没有改变。你有种预感,突破点应该是这个 set(a,b) 方法。你于是仔细观察发现,Field 类的 set(Object a,Object b) 两个参数都是 Object 类型的,但是不对啊,我明明给他传递的是 int 类型,对他肯定掉用了 Integer.valueOf(int x) 方法。想找寻真相的你,迫切熟练地进行断点调试。

set(a,b) 方法跟进去最终调用的是下面这个方法

public void set(Object var1, Object var2) throws IllegalArgumentException, IllegalAccessException {
        this.ensureObj(var1);
        if (this.isReadOnly) {
            this.throwFinalFieldIllegalAccessException(var2);
        }

        if (var2 == null) {
            this.throwSetIllegalArgumentException(var2);
        }

        if (var2 instanceof Byte) {
            unsafe.putIntVolatile(var1, this.fieldOffset, ((Byte)var2).byteValue());
        } else if (var2 instanceof Short) {
            unsafe.putIntVolatile(var1, this.fieldOffset, ((Short)var2).shortValue());
        } else if (var2 instanceof Character) {
            unsafe.putIntVolatile(var1, this.fieldOffset, ((Character)var2).charValue());
        } else if (var2 instanceof Integer) {
        //这些方法都是 native 方法
            unsafe.putIntVolatile(var1, this.fieldOffset, ((Integer)var2).intValue());
        } else {
            this.throwSetIllegalArgumentException(var2);
        }
}

unsafe.putIntVolatile(var1, this.fieldOffset, ((Integer)var2).intValue());突破点就在这个函数,然而却是一个 native 函数,由 C++ 代码实现了具体细节。仔细看第三个参数,你发现

我们在调用 Field 类的 set(a,b) 方法时,第二个参数传递的是 int 类型,会先调用 Integer.valueOf(int a) 方法将其转化为包装类 Integer。然而 int 类型的 swap 变量一经包装又变成了已经被改变了的对象!敲黑板,划重点!

还是前面的代码,再仔细分析一下。『x 对象』即『x引用指向的 Integer 对象』

private static void changeWrong2(Integer x, Integer y) throws Exception{
        Field fun = Integer.class.getDeclaredField("value");
        fun.setAccessible(true);
        //自动拆箱
        int swap = x;
        //现在 x 对象 value 值已经被修改成了 y 对象的 value,也就是 x 已经换为了 y
        fun.set(x,y);
        //怪就在这里,当 int 类型的变量(范围在[-128,127])在自动装箱时,又指向了已经被修改成 y 的 x 对象!
        fun.set(y,swap);
}

再仔细看看,其实如果不是因为 IntegerCache 的存在,我们这样做完全可以,不就是自动装箱嘛,反正它又没有指向之前的对象。然而 IntegerCache 却又的的确确存在,在 [-128,127] 范围内进行自动装箱操作时,就是返回之前的对象。不信你把我上面举的例子大小修改一下,随便改成两个不再此范围内的整数看看,结果肯定会被正确交换。

因此,我们要避免因为自动拆/装箱而可能产生的错误,直接不管它在不在这个范围,我们都统统不让系统进行这个操蛋的自动装箱操作。

正确代码如下:

public static void change(Integer i1,Integer i2) throws Exception {
        Field fun=Integer.class.getDeclaredField("value");
        fun.setAccessible(true);
        //防止他自动装箱,我们自己 new 一个,这样的话即使是在那个范围内的整数也会被交换
        Integer swap=new Integer(i1.intValue());
        fun.set(i1,i2);
        fun.set(i2,swap);
}

还没完,你好不好奇在进行转化后,如果给一个 Integer 对象赋值 int 时会不会得到我们想要的效果?

亮瞎了我的眼睛!我看到了啥,Integer p = 100; p 却等于 8。
怎样,看到这里我相信你肯定明白了这荒唐结果背后隐藏的玄机,对 IntegerCache,想不到你原来是这样的!

再补充一点,CSDN 新版的创作中心体验不错,口味正对我这个『外貌党』。

gist 完整版代码
我把完整的代码上传到 Github 了,希望能帮助到你,如有错误请指明。

好啦,完~

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值