专栏首页做不甩锅的后端与IntegerCache有关的一个比较坑的面试题

与IntegerCache有关的一个比较坑的面试题

最近在学习有关常量池的内容,正好看到一篇与之有关的面试题的文章。题目如下:

public static void main(String[] args) {

	Integer a = 1;
	Integer b = 2;
	System.out.println("before swap: a="+a+" b="+b);
	swap(a,b);
	System.out.println("before swap: a="+a+" b="+b);
}


private static void swap(Integer a,Integer b) {

}

要求将这两个Integer变量经过swap方法之后完成互换。

对于这个题目,可能一开始想到的就是最简单的办法,直接在swap方法内实现互换即可。不妨尝试一下:

private static void swap(Integer a,Integer b) {
	a = a^b;
	b = a^b;
	a = a^b;
}

但是输出结果如下:

before swap: a=1 b=2
before swap: a=1 b=2

没有成功。也就是说这个题目没有看上去的那么简单。里面肯定有坑。 这里就需要了解下java中引用传递还是值传递了。对于java,如果方法中传递的是基本类型,那么就是值传递。如果方法中是普通对象,那么就是引用传递。但是在java中,有这么一些特殊的类,如String、Integer等,没有set方法的immutable类。实际上也是值传递。 为此我们可以看看Integer的源码:

    /**
     * The value of the {@code Integer}.
     *
     * @serial
     */
    private final int value;

其中Integer的value是final修饰的。那么final修饰的只能被实例化一次。之后不能修改。因此Integer也没有提供可以修改Integer的set方法。因此Integer是一个与String类型的immutable类。所以也是值传递。 因此基于对反射的了解,我们解决这个问题的方法很容易想到了通过反射来解决。 那么我们修改代码如下:

private static void swap(Integer a,Integer b) {
	try {
		Field field = Integer.class.getDeclaredField("value");
		field.setAccessible(true);
		int tmp1 = a.intValue();
		int tmp2 = b.intValue();
		tmp1 = tmp1^tmp2;
		tmp2 = tmp1^tmp2;
		tmp1 = tmp1^tmp2;
		field.set(a,tmp1);
		field.set(b,tmp2);
	} catch (Exception e) {
		e.printStackTrace();
	}
}

再次执行。 结果如下:

before swap: a=1 b=2
before swap: a=2 b=2

这个结果有些让人意外的是,a的结果改成了2,但是b的结果也是2 !!!!

对于这个问题,只能进行单步了。

在debug的时候发现,调用了valeOf方法。也就是说,jvm自动将int的数据通过valueOf方法转换为了Integer。 我们将这个类的class反编译,可以看到:

  public static void main(String[] args) {
    Integer a = Integer.valueOf(1);
    Integer b = Integer.valueOf(2);
    System.out.println("before swap: a=" + a + " b=" + b);
    swap(a, b);
    System.out.println("before swap: a=" + a + " b=" + b);
  }


  
  private static void swap(Integer a, Integer b) {
    try {
      Field field = Integer.class.getDeclaredField("value");
      field.setAccessible(true);
      int tmp1 = a.intValue();
      int tmp2 = b.intValue();
      tmp1 ^= tmp2;
      tmp2 = tmp1 ^ tmp2;
      tmp1 ^= tmp2;
      field.set(a, Integer.valueOf(tmp1));
      field.set(b, Integer.valueOf(tmp2));
    } catch (Exception e) {
      e.printStackTrace();
    } 
  }

实际上在jvm中执行的是上述代码。jvm会自动在这些类型需要转换的地方装箱。哪我们再看看valueOf方法具体干了啥:

   /**
     * Returns an {@code Integer} instance representing the specified
     * {@code int} value.  If a new {@code Integer} instance is not
     * required, this method should generally be used in preference to
     * the constructor {@link #Integer(int)}, as this method is likely
     * to yield significantly better space and time performance by
     * caching frequently requested values.
     *
     * This method will always cache values in the range -128 to 127,
     * inclusive, and may cache other values outside of this range.
     *
     * @param  i an {@code int} value.
     * @return an {@code Integer} instance representing {@code i}.
     * @since  1.5
     */
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

这个方法,会对传入的值进行判断,如果在-128 - 127之间。则直接使用的是IntegerCache的结果。 那么这个题目要将Integer对象值为1的变量a和值为2的变量b互换,如果要成功的话,那就意味着将IntegerCache中的内容互换。 我们继续debug,在第一个set的代码行执行完成之后:

field.set(a, Integer.valueOf(tmp1));

此时我们可以看看这个IntegerCache是多少,为此为了debug方便我们加上一行:

Integer c = 5;

在这行执行valueOf方法的时候,查看IntegerCache中的内容: idea比较好的就是在debug的过程中可以查看任何变量的值,你只需要在代码中选中即可。

如果出现显示不全,那么移动到最下面,双击。

可以看到数组中129和130两个位置都是2。

这就说明我们实际上修改第一个位置是完成了。第一个值变成了2,但是在第二次set的时候出现了问题,反编译之后的写法,实际上是Integer.ValueOf,那么去从IntegerCache中查找1对应的位置,但是此时已经修改为了2,那么得到2后再修改,任然还是2。 因此,为了解决这个问题,我们的代码做出修改,让set的时候不再走IntegerCache。修改为如下:

	private static void swap(Integer a,Integer b) {
		try {
			Field field = Integer.class.getDeclaredField("value");
			field.setAccessible(true);
			int tmp1 = a.intValue();
			int tmp2 = b.intValue();
			tmp1 = tmp1^tmp2;
			tmp2 = tmp1^tmp2;
			tmp1 = tmp1^tmp2;
			field.set(a,new Integer(tmp1));
			field.set(b,new Integer(tmp2));
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

这样就解决了此问题。我们可以再看一下反编译之后的代码:

  private static void swap(Integer a, Integer b) {
    try {
      Field field = Integer.class.getDeclaredField("value");
      field.setAccessible(true);
      int tmp1 = a.intValue();
      int tmp2 = b.intValue();
      tmp1 ^= tmp2;
      tmp2 = tmp1 ^ tmp2;
      tmp1 ^= tmp2;
      field.set(a, new Integer(tmp1));
      field.set(b, new Integer(tmp2));
    } catch (Exception e) {
      e.printStackTrace();
    } 
  }

可以看到通过new一个新对象的方法,就避免了再次进入IntegerCache。

总结: 以上这个面试题的坑不少,主要有:

  • 1.java中的方法传递和值传递。对于不可变类型实际上还是值传递。
  • 2.反射修改私有变量需要setAccessible(true)。
  • 3.自动装箱调用ValueOf方法会走IntegerCache。
  • 4.如果不想走IntegerCache,那么new一个新的对象。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 有关JAVA自动装箱-拆箱的分析

    在java常量与常量池 中已经介绍过一些java自动装箱与拆箱的例子。现在单独对自动装箱/拆箱进行总结。

    冬天里的懒猫
  • 解决elasticsearch“Too many open files in system”问题

    在前两次集群扩容的过程中,总是会出现Too many open files in system问题。对于这个问题,困扰了一段时间。由于elasticsearch...

    冬天里的懒猫
  • 在springboot中对kafka进行读写操作

    只需要在dependencies中增加 spring-kafka的配置即可。完整效果如下:

    冬天里的懒猫
  • java学习之路:16.掌握Integer,Long,Short对象的创建以及其类提供的各种方法

    java.lang包中的Integer类,Long类,和Short类分别将int,long,short类型封装成一个类,由于这些类都市Number的子类,区别就...

    花狗Fdog
  • 奇怪的Java题:为什么128 == 128返回为false,而127 == 127会返回为true?

    奇怪的Java题:为什么128 == 128返回为false,而127 == 127会返回为true?

    互扯程序
  • 面试官六连问拆箱装箱Integer那些事,给我整懵圈了!

    小萌:由于基本数据类型不是对象,所以java并不是纯面向对象的语言,好处是效率较高(全部包装为对象效率较低)。Java是一个面向对象的编程语言,基本类型并不具有...

    乔戈里
  • 夯实Java基础系列8:深入理解Java内部类及其实现原理

    本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看

    Java技术江湖
  • 再谈Integer对象比较

    在上一篇文章中介绍了Integer的特性及面试中常考的几个知识点,本篇文章再做些扩充。

    小诸葛
  • 2020Java核心面试题--基础题

    答:byte、short、 int 、long、 float double、 boolean、 char

    宇宙之一粟
  • 搞清楚一道关于Integer的面试题

    面试题3中的a=99相当于a=new Integer(99);重新给把一个新的对象引用地址给了a,所以a变了,最后输出是99。

    田维常

扫码关注云+社区

领取腾讯云代金券