我们知道,Java中包含了8种基本数据类型:
这8种基本数据类型的变量不需要使用 new
来创建,它们不会在堆上创建,而是直接在栈内存中存储,因此会比使用对象更加高效。
但是,在某些时候,基本数据类型会有一些制约,例如当有个方法需要Object类型的参数,但实际需要的值却是2、3等数值,这就比较难以处理了。因为,所有引用类型的变量都继承了Object类,都可当成Object类型变量使用,但基本数据类型的变量就不可以了。
为了解决这个问题,Java为这8种基本数据类型分别定义了相应的引用类型,并称之为基本数据类型的 包装类 (Wrapper Class)。包装类均位于java.lang包下,其和基本数据类型的对应关系如下表所示:
基本数据类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
char | Character |
float | Float |
double | Double |
boolean | Boolean |
从上表可以看出,除了int和char有点例外之外,其他的基本数据类型对应的包装类都是将其首字母大写。
在Java SE5之前,把基本数据类型变量变成包装类实例需要通过对应包装类的构造器来实现,即:
Integer i = new Integer(10); 复制代码
把包装器类型转换为基本数据类型需要这样:
int a = i.intValue(); 复制代码
上面的基本数据类型与包装类对象之间的转换有点繁琐,所以从Java SE5开始,为了简化开发,Java提供了自动装箱(Autoboxing)和自动拆箱(AutoUnboxing)功能。
所谓 自动装箱,就是自动将基本数据类型转换为包装器类型;自动拆箱,就是自动将包装器类型转换为基本数据类型 ,下面代码演示自动装箱、拆箱。
// 自动装箱 Integer i = 10; // 自动拆箱 int a = i; 复制代码
我们可以看到,当JDK提供了自动装箱和自动拆箱功能后,大大简化了我们的开发。
需要注意的是,进行自动装箱和自动拆箱时必须注意类型匹配。例如,Integer只能自动拆箱成int,int只能自动装箱成Integer。
通过前文,我们已经知道了什么是包装类及什么是自动装、拆箱。
那么接下来,我们来看一下自动拆、装箱的原理。有如下代码:
public static void main(String[] args) { Integer i = 10;// 自动装箱 int a = i;// 自动拆箱 } 复制代码
对以上代码进行反编译,得到如下代码:
public static void main(String[] args) { Integer i = Integer.valueOf(10); int a = integer.intValue(); } 复制代码
从反编译后得到的代码可以看出,在装箱的时候自动调用的是Integer的 valueOf(int i)
方法,而在拆箱的时候自动调用的是Integer的 intValue()
方法。
其他的也类似,比如Double、Character。感兴趣的同学,可以自己尝试一下。
因此可以用一句话总结装箱和拆箱的实现过程:
自动装箱,都是通过包装类的 valueOf()
方法来实现的。
自动拆箱,都是通过包装类对象的 xxxValue()
来实现的。
我们了解过原理之后,在来看一下,什么情况下,Java会帮我们进行自动拆装箱。前面提到的变量的初始化和赋值的场景就不介绍了,那是最简单的也最容易理解的。
我们主要来看一下,那些可能被忽略的场景
List<Integer> list = new ArrayList<>(); list.add(1); 复制代码
当我们把基本数据类型放入集合类中的时候,代码没有报错,很明显,这里发生了自动装箱。
将上面代码进行反编译,也印证了这一点:
List<Integer> list = new ArrayList<>(); list.add(Integer.valueOf(1)); 复制代码
Integer i = 10; // 输出true System.out.println("10的包装类实例是否大于8?" + (i > 8)); 复制代码
反编译上面代码:
Integer i = Integer.valueOf(10); System.out.println("10的包装类实例是否大于8?" + (i.intValue() > 8)); 复制代码
可以看到,当包装类与基本数据类型进行比较运算时,是先将包装类进行拆箱成基本数据类型,然后进行比较的。
Integer i = 10; Integer j = 20; // 输出30 System.out.println(i + j); 复制代码
反编译上面代码:
Integer i = Integer.valueOf(10); Integer j = Integer.valueOf(20); System.out.println(i.intValue() + j.intValue()); 复制代码
可以看到,两个包装类型之间的运算,会被自动拆箱成基本类型进行。
boolean flag = true; Integer i = 0; int j = 1; int k = flag ? i : j; 复制代码
很多人不知道,其实在第四行,会发生自动拆箱,反编译后代码如下:
boolean flag = true; Integer i = Integer.valueOf(0); int j = 1; int k = flag ? i.intValue() : j; 复制代码
这其实是三目运算符的语法规范:当第二,第三位操作数分别为基本类型和对象时,其中的对象就会拆箱为基本类型进行操作。如果这个时候i的值为 null
,那么就会发生NPE,这一点,是我们日常开发过程中的大坑。
观察以下代码:
public static void main(String[] args) { Map<String, Boolean> map = new HashMap<>(); Boolean b = map != null ? map.get("test") : false; System.out.println(b); } 复制代码
一般情况下,我们会认为以上代码Boolean b的最终得到的值应该是 null
。因为 map.get("test")
的值是 null
,而b又是一个对象,所以得到结果会是 null
。
但是,以上代码会抛出NPE:
Exception in thread "main" java.lang.NullPointerException 复制代码
这是因为, map.get("test") == null
,当用 null
去调用 intValue()
方法时,抛出了NPE。
有如下代码,你知道输出结果是什么吗?
public static void main(String[] args) { Integer a = 1; Integer b = 1; Integer c = 128; Integer d = 128; System.out.println(a == b); System.out.println(c == d); } 复制代码
我们都知道在Java里,当用 ==
来比较两个对象时,比较的是地址,如果两个对象引用指向堆中的同一块内存就返回 true
,否则返回 false
。这一点是完全正确的。
那按照这个理论,上面代码应该输出都是 false
,因为4个变量都是 Integer
类型的对象,但实际输出结果却是这样的:
true false 复制代码
这让人疑惑:同样是两个 int
类型的数值自动装箱成 Integer
对象,如果是两个2自动装箱后就相等;但如果是两个128自动装箱后就不相等,这是为什么呢?
一起来找下这个问题的答案:
Integer a = 1
,根据前文我们知道,这里发生了自动装箱,而自动装箱其实就是调用了 Integer
的 valueOf
方法 valueOf
方法,那我们是不是应该去看看这个方法里到底做了什么事情? valueOf
的具体实现。
来,一起来看JDK里 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
是 Integer
类中定义的一个 private static
的内部类,它维护了一个 Integer
数组 cache
, IntegerCache
源码如下:
/** * Cache to support the object identity semantics of autoboxing for values * between -128 and 127 (inclusive) as required by JLS. * <p> * The cache is initialized on first usage. The size of the cache * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option. * During VM initialization, java.lang.Integer.IntegerCache.high property * may be set and saved in the private system properties in the * sun.misc.VM class. */ 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 Integer.IntegerCache.high >= 127; } private IntegerCache() { } } 复制代码
从上面源码可以看出:
我们终于找到上面问题的答案了:
Integer类初始化时,会把一个-128~127之间的Integer类型对象放入一个名为cache的数组中 缓存
起来。如果以后把一个-128~127之间的基本数据类型自动装箱成一个Integer实例时(即调用valueOf方法),实际上是直接引用了cache数组中的对应元素。但每次把一个不在-128~127范围内的整数自动装箱成Integer实例时,就需要重新 new
一个Integet实例,所以出现了上面那样的运行结果。
缓存是一种非常优秀的设计模式,在Java、JavaEE平台的很多地方都会通过缓存来提高系统的性能。
类型的,Byte、Short、Long、Character也有相同的缓存机制,值得注意的是Double、Float是没有缓存机制的。有兴趣的同学,可以自行查看源码。