暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

看似简单的一道面试题,竟然考察了这么多知识点!!!

风济海 2021-09-11
701

最近看到一道挺有意思的面试题,考察了好几个Java基础知识点,题目如下:

    /**
    * @author 风济海
    * @description 交换两个Integer的值
    */
    public class SwapDemo {
        public static void main(String[] args)  {
            Integer a = 1,b = 2;
    System.out.println("交换前:a="+a+", b="+b);
            swap(a,b);
    System.out.println("交换后:a="+a+", b="+b);
        }
    **
         * 交换两个Integer的值(实现该方法)
    */
        private static void swap(Integer a1, Integer b1) {
        }
    }


    下面根据“解法”逐个分析这道题所考察的知识点。


    01

    方案1:中间变量


    看到题目中swap方法参数类型是引用类型,如果对Integer底层不够了解,可能首先想到的实现方式,是通过中间变量来实现,具体如下:

      private static void swap(Integer a1, Integer b1) {
      Integer tmp = a1;
      a1 = b1;
      b1 = tmp;
      }

      运行main方法的结果如下:

        交换前:a=1, b=2
        交换后:a=1, b=2


        从运行结果看,显然这种实现方式是错误的,原因是什么呢?

        虽然Integer是引用类型,但是Integer是使用final类型的int进行数据存储,final类型的变量不能够被重新赋值。

          private final int value;
          public Integer(int value) {
          this.value = value;
          }

          所以swap方法参数接收到的实际上是变量a、b的副本



          02

          方案2:反射方式


          本质上Integer还是对象,我们一般操作对象的变更是通过set方法,但是从上面介绍Integer中value属性是final类型,所以只能通过反射(第二个基础知识点)的方式来实现。

            private static void swap2(Integer a1, Integer b1) throws NoSuchFieldException, IllegalAccessException {
            Field field = Integer.class.getDeclaredField("value");
            对所有属性设置访问权限,包括私有成员变量,以及被final修饰的属性
                field.setAccessible(true);
            int tmp = a1.intValue();
            field.set(a1,b1);
            field.set(b1,tmp);
            }

            运行结果:

              交换前:a=1, b=2
              交换后:a=2, b=2

              发现a的值变了,但是b却没变,是啥原因呢?往下看。


              03

              方案3:反射方式优化


              b没变的原因涉及到了Java中的自动装箱,a的类型是Integer,而1的类型是int,会自动装箱,可通过javap -c SwapDemo.class查看字节码:

                  ......
                public static void main(java.lang.String[]);
                Code:
                0: iconst_1
                1: invokestatic #2 Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
                4: astore_1
                5: iconst_2
                6: invokestatic #2 Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
                9: astore_2
                10: getstatic #3 Field java/lang/System.out:Ljava/io/PrintStream;
                ......

                从字节码可以看出,在装箱的时候自动调用的是IntegervalueOf()方法(拆箱调用的是intValue()方法),查看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);
                  }

                  从源码发现有个IntegerCache类,它的部分源码:

                    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;
                    ......
                            high = h;
                    ......
                        }
                    private IntegerCache() {}
                    }

                    从上面两段代码可以看出来,通过valueOf()方法创建对象的时候,如果数值在[-128,127]之间,则返回指向IntegerCache.cache[]的引用,否则返回的是一个新创建的Integer对象。


                    延伸:IntegerCache这么设计的好处,1)提高效率,2)减少内存分配


                    回到题目,swap2方法中的实现结果为啥b还是2,是因为:原本a经过自动装箱所对应数组IntegerCache[]下标为129的元素值为1,当执行完field.set(a1,b1)这行代码时,该下标为129位置的值被改为2,所以,在执行field.set(b1,tmp)这行代码时,先获取tmp的值,经过自动装箱获取到的值是缓存数组中下标为129的已经被改的2,所以执行完field.set(b1,tmp),b的结果还是2。


                    因此,正确的实现方式也就明确了:避免装箱(其实就是避免走缓存),有两种方式,第一种:

                      private static void swap3(Integer a1, Integer b1) throws NoSuchFieldException, IllegalAccessException {
                      Field field = Integer.class.getDeclaredField("value");
                      / 对所有属性设置访问权限,包括私有成员变量,以及被final修饰的属性
                      field.setAccessible(true);
                      // 避免装箱
                      Integer tmp = new Integer(a1);
                      field.set(a1,b1);
                      field.set(b1,tmp);
                      }

                      第二种,使用setInt()赋值:

                        private static void swap4(Integer a1, Integer b1) throws NoSuchFieldException, IllegalAccessException {
                        Field field = Integer.class.getDeclaredField("value");
                        // 对所有属性设置访问权限,包括私有成员变量,以及被final修饰的属性
                        field.setAccessible(true);
                        int tmp = a1;
                        field.setInt(a1,b1);
                        field.setInt(b1,tmp);
                        }


                        这两种实现方式执行结果:

                          交换前:a=1, b=2
                          交换后:a=2, b=1


                          04

                          考察知识点总结


                          涉及知识点

                          1、Java方法参数的传递方式

                          2、包装类型缓存

                          3、自动装箱拆箱

                          4、Java反射机制


                          这道题目的是用来考察面试者对这几个知识点的掌握程度,但是需要特别注意的是:上面的正确的实现方式会更改IntegerCache缓存中的值,对正常的使用有影响,不建议在实际项目中这样做。


                          如果你还有别的实现方式,欢迎留言交流~


                          -END-


                          如果觉得本文对你有用

                          请长按二维码,关注 风济海,顺便点个 在看 

                          转发至 朋友圈,是对我最大的支持



                          文章转载自风济海,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                          评论