因为一个小小的Integer问题导致阿里一面没过,遗憾!

面试题:new Integer(112)和Integer.valueOf(112)的区别

面试官考察点猜想

这道题,考察的是对Integer这个对象原理的理解,关于这道题的变体有很多,我们会一一进行分析。

理解这道题,对于实际开发过程中防止出现意想不到的Bug很有用,建议大家认真思考和解读。

背景知识详解

关于Integer的实现

Integer是int的一个封装类,它的构造实现如下。

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

    /**
     * Constructs a newly allocated {@code Integer} object that
     * represents the specified {@code int} value.
     *
     * @param   value   the value to be represented by the
     *                  {@code Integer} object.
     */
    public Integer(int value) {
        this.value = value;
    }

Integer中定义了一个int类型的value属性。由于该属性是final类型,因此需要通过构造方法来赋值。这个逻辑非常简单,没有太多要关注得。

结论: 当通过new关键字构建一个Integer实例时,和所有普通对象的实例化相同,都是在堆内存地址中分配一块空间。

Integer.valueOf

Integer.valueOf方法,是把一个字符串转换为Integer类型,该方法定义如下

public static Integer valueOf(String s) throws NumberFormatException {
  return Integer.valueOf(parseInt(s, 10));
}

这个方法调用另外一个重载方法,该方法定义如下。

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

从这段代码中发现,如果i的值是在IntegerCache.lowIntegerCache.high这个区间范围,则通过下面这段代码返回Integer对象实例。

IntegerCache.cache[i+(-IntegerCache.low)];

否则,使用new Integer(i)创建一个新的实例对象。

IntegerCache是什么?

从它的命名来看,不难猜出它应该和缓存有关系,简单猜测就是:如果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的值允许通过系统属性来调整
    String integerCacheHighPropValue =
      sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
    //如果配置了high的属性值,则取两者中最大的一个值作为IntegerCache的最高区间值。
    if (integerCacheHighPropValue != null) {
      try {
        int i = parseInt(integerCacheHighPropValue);
        i = Math.max(i, 127);
        // Maximum array size is Integer.MAX_VALUE
        h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
      } catch( NumberFormatException nfe) {
        // If the property cannot be parsed into an int, ignore it.
      }
    }
    high = h;
    //创建一个数组容器
    cache = new Integer[(high - low) + 1];
    int j = low;
    //遍历初始化每一个对象
    for(int k = 0; k < cache.length; k++)
      cache[k] = new Integer(j++);

    // range [-128, 127] must be interned (JLS7 5.1.7)
    assert IntegerCache.high >= 127;
  }

  private IntegerCache() {}
}

上述代码的实现逻辑非常简单:

  1. IntegerCache的取值区间为: IntegerCache.low=-128, IntegerCache.hign=127,其中hign是可以通过系统参数来调整。
  2. 创建一个Integer数组,循环初始化这个区间中的每一个值。

Integer为什么这么设计? 但凡涉及到Cache的,一定和性能有关,在Integer这个对象中,常用的数值区间是在-128到127之间,所以为了避免对这个区间范围内的数据频繁创建和销毁对象,所以构建了一个缓存。意味着后续只要不是通过new关键字创建的Integer实例,在这个区间内的数值都会从IntegerCache中获取。

image-20211029101125958

问题解答

面试题:new Integer(112)和Integer.valueOf(112)的区别

理解了上面的原理后,再来解答这个问题就很容易了。

  • new Integer,是创建一个Integer对象实例。

  • Integer.valueOf(112),Integer默认提供了Cache机制,在-128到127区间范围内的数据,通过valueOf方法不需要创建新的对象实例,只需要从缓存中获取即可。

问题总结

Integer这个对象的变形面试题比较多,其中一个面试题比较典型。

有两个Integer变量a,b,通过swap方法之后,交换a,b的值,请写出swap的方法。

public class SwapExample {
 
    public static void main(String[] args){
        Integer a=1;
        Integer b=2;
        System.out.println("交换前:a="+a+",b="+b);
        swap(a,b);
        System.out.println("交换后:a="+a+",b="+b);
    }
 
    private static void swap(Integer a,Integer b){
       //doSomething
    }
}

基础不是很好的同学,可能会很直接的按照”正确的逻辑“来编写程序,可能的代码如下。

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

程序逻辑,理论上是没问题,定义一个临时变量存储a的值,然后再对ab进行交换。而实际运行结果如下

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

Integer对象的重新赋值思考

Integer作为封装对象类型,通过函数传递该引用以后,理论上来说,main方法中定义的ab,以及传递到swap方法中的a、和b,指向同一个内存地址,那么按照上述代码的实现,理论上来说也是成立的。

image-20211029103929388

Java中有两种参数传递类型。

  • 值传递,传递的是数据的副本,方法执行中形式参数值的改变不影响实际参数的值。
  • 引用传递,传递的是内存地址的引用,在方法执行中,由于引用对象的地址指向同一块内存,所以对于对象数据的修改,会影响到引用了该地址的变量。

这么设计的好处,是为了减少内存的占用,提升访问效率和性能。

那么Integer作为封装类型,为什么传递的是副本,而不是引用呢?

我们来看一下Integer中value值得定义,可以发现该属性是final修饰,意味着是不可更改。

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

结论:在Java中,只有一种参数传递方式,就是值传递。但是,当参数传的是基本类型时,传的是值的拷贝,对拷贝变量的修改不影响原变量;当传的是引用类型时,传的是引用地址的拷贝,但是拷贝的地址和真实地址指向的都是同一个真实数据,因此可以修改原变量中的值;当传的是Integer类型时,虽然拷贝的也是引用地址,指向的是同一个数据,但是Integer的值不能被修改,因此无法修改原变量中的值。

因此,上述代码之所以没有交换成功,是因为传递到swap方法中的ab,会创建一个变量副本,这个副本中的值虽然发生了交换,但不影响原始值。

image-20211029114043119

了解了这块知识之后,我们的问题就变成了,如何对一个修饰了final关键字的属性进行数据修改。那就是通过反射来实现,实现代码如下.

public class SwapExample  {

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

    private static void swap(Integer a,Integer b){
        try {
            Field field=Integer.class.getDeclaredField("value");
            Integer temp= a;
            field.setAccessible(true); //针对private修饰的变量,需要通过该方法设置。
            field.set(a,b);
            field.set(b,temp);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

那这段代码运行完是否能达到预期呢? 上述程序运行结果如下:

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

从结果来看,确实是发生了变化,但是变化并不完整,因为b=1这个预期值并没有出现。为什么呢?其实还是和今天分享得主题有关系,我们来逐步看一下。

  1. Integer temp=a这个地方,基于IntegerCache的原理,这里并不会产生一个新的temp实例,意味着temp变量和a变量指向的内存地址是同一个。
  2. 当通过field.set方法,把a内存地址的值通过反射修改成b以后,那么此时a的值应该是2。注意:由于内存地址的值变成了2,而temp这个变量又指向该内存地址,因此temp的值自然就变成了2.
  3. 接着使用filed.set(b,temp)修改b属性的值,此时temp的值时2,所以得到的结果b也变成了2.
private static void swap(Integer a,Integer b){
  try {
    Field field=Integer.class.getDeclaredField("value");
    Integer temp= a;  
    field.setAccessible(true); //针对private修饰的变量,需要通过该方法设置。
    field.set(a,b);
    field.set(b,temp);
  } catch (NoSuchFieldException e) {
    e.printStackTrace();
  } catch (IllegalAccessException e) {
    e.printStackTrace();
  }
}

理解了原理后,我们只需要修改Integer temp=a这段代码,改成下面这种写法。保证temp变量是一个独立的实例。

Integer temp=new Integer(a);

修改以后运行结果如下

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

Mic说: 只有基本功足够扎实,才能对任何问题的本质一眼看透,解决这些问题的时候也能得心应手。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,548评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,069评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,985评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,305评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,324评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,030评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,639评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,552评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,081评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,194评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,327评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,004评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,688评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,188评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,307评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,667评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,337评论 2 358

推荐阅读更多精彩内容