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

    好啦,完~

  • 相关阅读:
    Java实现 LeetCode 833 字符串中的查找与替换(暴力模拟)
    Java实现 LeetCode 833 字符串中的查找与替换(暴力模拟)
    Java实现 LeetCode 833 字符串中的查找与替换(暴力模拟)
    Java实现 LeetCode 832 翻转图像(位运算)
    Java实现 LeetCode 832 翻转图像(位运算)
    Java实现 LeetCode 832 翻转图像(位运算)
    Java实现 LeetCode 831 隐藏个人信息(暴力)
    Java实现 LeetCode 831 隐藏个人信息(暴力)
    Java实现 LeetCode 831 隐藏个人信息(暴力)
    how to use automapper in c#, from cf~
  • 原文地址:https://www.cnblogs.com/Zhoust/p/14994605.html
Copyright © 2011-2022 走看看