• 原来你是这样的 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 了,希望能帮助到你,如有错误请指明。

    好啦,完~

  • 相关阅读:
    将在线图片转换成base64踩坑记录及静态资源跨域及缓存的处理
    MySQL大表拆分多个表的方式(横向拆分和纵向拆分)及如何解决跨表查询效率问题
    electron-vue项目打包踩坑指南
    如何在npm上发布vue插件
    MVC之前的那点事儿系列(9):MVC如何在Pipeline中接管请求的?
    MVC之前的那点事儿系列(8):UrlRouting的理解
    MVC之前的那点事儿系列(7):WebActivator的实现原理详解
    MVC之前的那点事儿系列(6):动态注册HttpModule
    MVC之前的那点事儿系列(5):Http Pipeline详细分析(下)
    MVC之前的那点事儿系列(4):Http Pipeline详细分析(上)
  • 原文地址:https://www.cnblogs.com/Zhoust/p/14994605.html
Copyright © 2020-2023  润新知