面试题:new Integer(112)和Integer.valueOf(112)的区别
这道题,考察的是对Integer这个对象原理的理解,关于这道题的变体有很多,我们会一一进行分析。
理解这道题,对于实际开发过程中防止出现意想不到的Bug很有用,建议大家认真思考和解读。
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类型,该方法定义如下
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.low
和IntegerCache.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() {} }
上述代码的实现逻辑非常简单:
hign
是可以通过系统参数来调整。Integer为什么这么设计? 但凡涉及到Cache的,一定和性能有关,在Integer这个对象中,常用的数值区间是在-128到127之间,所以为了避免对这个区间范围内的数据频繁创建和销毁对象,所以构建了一个缓存。意味着后续只要不是通过
new
关键字创建的Integer实例,在这个区间内的数值都会从IntegerCache中获取。
面试题: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
的值,然后再对a
和b
进行交换。而实际运行结果如下
交换前:a=1,b=2 交换后:a=1,b=2
Integer作为封装对象类型,通过函数传递该引用以后,理论上来说,main方法中定义的a
和b
,以及传递到swap方法中的a
、和b
,指向同一个内存地址,那么按照上述代码的实现,理论上来说也是成立的。
Java中有两种参数传递类型。
这么设计的好处,是为了减少内存的占用,提升访问效率和性能。
那么Integer作为封装类型,为什么传递的是副本,而不是引用呢?
我们来看一下Integer中value值得定义,可以发现该属性是final修饰,意味着是不可更改。
/** * The value of the {@code Integer}. * * @serial */ private final int value;
结论:在Java中,只有一种参数传递方式,就是值传递。但是,当参数传的是基本类型时,传的是值的拷贝,对拷贝变量的修改不影响原变量;当传的是引用类型时,传的是引用地址的拷贝,但是拷贝的地址和真实地址指向的都是同一个真实数据,因此可以修改原变量中的值;当传的是Integer类型时,虽然拷贝的也是引用地址,指向的是同一个数据,但是Integer的值不能被修改,因此无法修改原变量中的值。
因此,上述代码之所以没有交换成功,是因为传递到swap方法中的a
和b
,会创建一个变量副本,这个副本中的值虽然发生了交换,但不影响原始值。
了解了这块知识之后,我们的问题就变成了,如何对一个修饰了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
这个预期值并没有出现。为什么呢?其实还是和今天分享得主题有关系,我们来逐步看一下。
Integer temp=a
这个地方,基于IntegerCache的原理,这里并不会产生一个新的temp实例,意味着temp变量和a
变量指向的内存地址是同一个。field.set
方法,把a
内存地址的值通过反射修改成b
以后,那么此时a
的值应该是2
。注意:由于内存地址的值变成了2,而temp
这个变量又指向该内存地址,因此temp
的值自然就变成了2.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说: 只有基本功足够扎实,才能对任何问题的本质一眼看透,解决这些问题的时候也能得心应手。