一、自动拆装箱的原理
Java中有8大基本数据类型,为了实现面向对象的操作,8大基本类型又都有着各自对应的包装对象类型,而包装类型大体上可以分为三种:Boolean、Character和Number,其中Number类型包括了整型(Byte、Short、Integer、Long)、浮点型(Float、Double)。用于基本数据类型与其对应的包装数据类型之间的转换,这种方式称为自动拆装箱,它是Java5 引入的一种特性,也可以理解为是一种Java语法糖(语法糖概念:借助编译器实现了基于编译器的新语法,增强了Java程序的可读性,便于开发人员使用和维护)。
下面通过示例演示一下自动拆装箱的原理, 以Integer类型为例:
/** TestInt.java源码 */
public class TestInt {
public static void main(String[] args) {
Integer a= 50; // 50为基本类型
int i = a; // a为包装类型
}
}
经过JVM编译之后生成了class文件,这里很容易看出自动拆装箱的实现过程:
/** TestInt.class */
public class TestInt {
public static void main(String[] paramArrayOfString) {
Integer integer = Integer.valueOf(50);
int i = integer.intValue();
}
}
小结一下:
- 自动装箱:通过valueOf()方法实现,将 基本数据类型 转换成 对应的包装数据类型;
- 自动拆箱:通过xxxValue()方法实现,将 包装数据类型 转换成 对应的基本数据类型;
二、自动拆装箱的应用场景
1、Java集合泛型
注意:集合的参数化类型只能是包装类型,不能是对应的基本类型!!!
List<Integer> list = new ArrayList<>();
for (int i = 1; i < 50; i ++){
list.add(i); // 自动装箱
}
Map<Long, Double> map = new HashMap<>();
map.put(1001L, 50.6); // 自动装箱
map.put(1002L, 63.9);
2、进行运算和比较
注意:
- 实现+号的数值运算的两边必须是基本类型,基本类型的==比较是一种数值比较;
- 包装类中均重写了equals()方法,因此equals()则比较的是对象内容,但equals()比较时不参与类型转换;
Integer a = 10;
Integer b = 10;
Integer c = 20;
System.out.println(c == (a + b)); // true
System.out.println(c.equals(a + b)); // true, 整型20与整型20比较
Long d = 20L;
System.out.println(d == (a + b)); // true
System.out.println(d.equals(a + b)); // false, 长整型20L与整型20比较
3、方法的入参与返回值
/** 自动拆箱 */
public int getNum(Integer num) {
return num;
}
/** 自动装箱 */
public Integer findNumber(int number) {
return number;
}
三、自动拆装箱与缓存
还是先看个测试示例,创建a,b,c,d四个对象,然后分别用== 和 equals()比较:
public static void main(String[] args) {
Integer a = 200;
Integer b = 200;
Integer c = 120;
Integer d = 120;
System.out.println(a == b);
System.out.println(a.equals(b));
System.out.println(c == d);
System.out.println(c.equals(d));
}
--------------------------
打印结果:
false
true
true
true
因为包装类Integer重写了equals()方法,对比的结果均为true也很好理解。而 == 比较的则是对象的引用(即内存地址),在Integer类型中,a==b为false,c==d却为true,这说明了c与d是同一个对象!!将上面的换成Short 类型、Long 类型、Byte 类型,得到的仍是相同结果;换成Float类型、Double类型的话,a==b为false,c==d为false,说明了c与d不是同一个对象。
从上述的测试结果来看:Byte 类、Short 类、Integer 类、Long 类应该存在一种类似缓存的功能,打开源码后,发现它们确实都存在一个名叫XxxCache的静态内部类,该缓存内部类中均维护了一个缓存数组,将-128到127区间的数组缓存了起来。当创建包装对象时,如果包装对象的数值在该区间范围的话,则不会再去new新对象了,这样可以减少不必要的创建对象的开销操作。另外,因为Float和Double类型均带有浮点小数,没有特定的经常用到的热点值,没必要使用缓存。
当然,Character类型也是有缓存功能的,只不过是将0到128之间的char类型数据做了缓存,Boolean类则只存在TRUE 和 FALSE两个值,也没必要使用缓存。
值得注意的是,源码中Byte 类、Short 类、Long 类实现缓存的步骤和细节是一样,Integer 类则略显不同,可概括为如下:
/** Byte类、Short类、Long类实现缓存功能 */
private static class XxxCache {
private XxxCache(){}
// 定义缓存数组大小
static final Xxx cache[] = new Xxx[-(-128) + 127 + 1];
// 静态块只在类首次加载时进行一次初始化,将-128到127存到缓存数组中
static {
for(int i = 0; i < cache.length; i++)
// 默认为int,若使用byte和short需要强转
cache[i] = new Xxx((xxx)(i - 128));
}
}
/** Integer类实现缓存功能 */
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;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
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() {}
}
Integer 类实现缓存功能似乎有点特殊了,但核心思路还是类似的。仔细看会发现有个很明显的区别 —— Integer的最大缓存值可配置,在运行时可指定缓存最大值,如 -XX:AutoBoxCacheMax=10000。
小结
自动拆装箱的引入给开发带来了使用便利,尤其是Character、Byte 、Short 、Integer 、Long等类型为常用数值区间做了缓存,避免了不必要的对象创建开销。实际使用中还是需要注意一些细节问题的,比如包装类的==与equals()的比较、包装对象为null要避免NPE情况、避免在循环里进行大量频繁的拆装箱操作等等。自动拆装箱是基础,也是需要必知必懂的,不积硅步无以至千里,点滴付出终将有所收获,共同进步吧 ~