Autoboxing/Auto-Unboxing
逐渐挖掘Autoboxing/Auto-Unboxing
更简单的整合两套类型系统
传统上,在Java程序中,可以往一个容器类(无论是Collection还是Map)里直接放入一个对象;但是如果打算放入的是一个数字、字符或布尔值的话,就要先加入一个“生成包裹它们的对象”的步骤。
造成这种现象的原因是,在Java语言当中一直存在着两套非常不同的类型系统:
- 一套是所谓的“引用类型”(Reference Types),包括所有的类和接口。这些类型的数据被看作对象,所以可以用一个Object型的变量来保存。
- 一套是所谓的“基本类型”(Primitive Types),包括:byte、short、int、long、float、double、char和boolean。这些类型的数据不是对象,因此也不能用Object型的变量来保存。
同时采用这样两套类型系统,可以得到一些性能方面的好处——因为基本类型的数据不是对象,所以创建得更快、占用的空间更少、收回它们占用的资源也更容易;但是,这样的做法同时也会造成一些编码方面的问题——例如,不能定义一个变量(或数组),让它既能保存基本类型的数据,又能保存引用类型的数据(类似的,也不能定义一个同时能匹配这两种类型的数据的形参,不过这个问题可以借助Java里的重载机制来回避)。
实际上需要定义“不知道用来保存什么类型的数据”的变量(和形参)时,一般对这个问题采取回避的态度,将它们的类型定义成Object,然后借助可以称为“Boxing”和“Unboxing”的操作来解决Object不能涵盖基本类型的问题。
1. Boxing和Unboxing操作
所谓Boxing操作,是指通过生成一个能包裹基本类型数据的对象,来让基本类型的数据出现在只能接受引用类型的地方。
清单1:手工Boxing的典型情况
for(int i = 0; i < 10; i++) {
integers.add(new Integer(i));
}
用于生成这些的对象的类,被称作“包裹类”(Wrapper Classes)。Java中的包裹类有Byte 、Short、Integer、Long、Float、Double、Character和Boolean(都在java.lang包里定义)等八种,分别用于包裹byte、short、int、long、float、double、char和boolean类型的数据。
而所谓Unboxing操作,则是指调用包裹类对象的相应方法,得到它们所代表的“基本类型的数据”,以便进行进一步的处置。
清单2:手工Unboxing的典型情况
Integer i = (Integer) itr.next();
System.out.println(i.intValue() + 1);
}
而在Java语言的最新版本——J2SE 1.5中,提供了“Autoboxing”和“Auto-Unboxing”的机制,可以让编译器来自动完成这些琐碎的操作,从而用一种更简单的方式,来整合两套类型系统。
2. 使用Autoboxing和Auto-Unboxing
使用Autoboxing和Auto-Unboxing,并不需要什么特别的步骤,一切都会在编译器的安排下自动发生。
现在可以这样来对待一个int型的数据:
清单3:自动完成的Boxing操作
al.add(1);
因为编译器会悄悄的把这段代码转换成接近这个样子:
清单4:作了Autoboxing之后的等价形式
al.add(new Integer(1));
而这样对待一个Integer型的对象也是可以的:
清单5:自动完成的Unboxing操作
int two = one + 1;
因为编译器会悄悄的把这段代码转换成类似这个形状:
清单6:作了Auto-Unboxing之后的等价形式
int two = one.intValue() + 1;
大体上,只要把一个结果类型是基本类型的表达式,放到需要让它们的包裹类出现的位置上,就会诱发Autoboxing;类似的,只要把一个结果类型是包裹类的表达式,放到只允许相应的基本类型出现的位置上,就会诱发Auto-Unboxing。
3. 发生Autoboxing的具体时机
发生Autoboxing的具体时机,主要有这么三种:
- 把基本类型的数据赋给引用类型的变量时。例如把一个int型的数据赋给一个Integer型变量。
清单7:赋给引用类型的变量基本类型的数据
Integer i = 31415; - 把基本类型的数据传给引用类型的参数时。例如给一个定义成Object的参数传递一个boolean型的数据。
清单8:传给引用类型的参数基本类型的数据
HashMap map = new HashMap();
map.put(true, null); - 把基本类型的数据往引用类型上强制转化时。例如在一个long型的数据前面加上(Long)。
清单9:从基本类型的数据到引用类型上强制转化
System.out.println((Long) 27828L);
4. Autoboxing的局限
Autoboxing的机制有一个局限——只能把基本类型的数据往它们自己的包裹类(以及包裹类的上级类)上转化。
类似这样的代码是不能工作的,尽管int型的数据完全可以用一个Long对象来表示:
清单10:不能同时进行自动向上转型和Autoboxing
System.out.println((Long) i);/* 编译时出错 */
这是因为这段代码实际上相当于:
清单11:Autoboxing操作会在自动向上转型之前发生
System.out.println((Long) new Integer(i));/* 编译时出错 */
而Integer并不是Long的子类,所以这个转化无法进行。如果一定要进行这种操作,需要手工追加一次转型:
清单12:需要先强制向上转型,再作Boxing
System.out.println((Long)(long) new Integer(i));
5. 发生Auto-Unboxing的具体时机
发生Auto-Unboxing的具体时机,则主要有这么七种:
- 把包裹类对象赋给基本类型的变量时。例如把一个Integer型的数据赋给一个int型变量。
清单13:赋给基本类型的变量包裹类对象
int i = new Integer(32); - 把包裹类对象传给基本类型的参数时。例如给一个定义成boolean的参数传递一个Boolean型的数据。
清单14:传给基本类型的参数包裹类对象
JFrame frame = new JFrame("^_^");
frame.setSize(320, 200);
frame.setVisible(new Boolean(true)); - 把包裹类对象往基本类型上强制转化时。例如在一个Long型的数据前面加上(long)。
清单15:从包裹类对象到基本类型的强制转化
Long l = new Long(31415L);
System.out.println((long) l); - 把包裹类对象当作运算符的操作数时。例如在两个Byte型的数据之间放上“+”号。
清单16:把包裹类对象当作运算符的操作数
Byte a = new Byte((byte) 1);
Byte b = new Byte((byte) -1);
System.out.println(((a++) << 2) + (~b));/* 输出“4” */
System.out.println(a);/* 输出“2” */ - 用包裹类对象来指定数组的大小时。当然,从语义上说,这个对象的类型必须是Byte、Short、Integer或Character。
清单17:用包裹类对象来指定数组的大小
Character size = new Character('★');
int[] integers = new int[size];/* 生成一个可放9733个int元素的数组 */ - 把包裹类对象在switch语句里使用时。当然,从语义上说,这个对象的类型必须是Byte、Short、Integer或Character。
清单18:在switch语句里使用包裹类对象
Character c = new Character('a');
switch (c) {
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
System.out.println("A Vowel in English");
break;
default:
System.out.println("Not A Vowel in English");
break;
} - 把Boolean对象在if/for/while/do-while语句中作为条件表达式使用时。
清单19:把Boolean对象作为条件表达式
Boolean bool = new Boolean(Math.random() > 0.5);
if (bool) {
System.out.println("Aye!");
} else {
System.out.println("Nay!");
}
6. Auto-Unboxing的局限
Auto-Unboxing的机制则有这样一个局限——只能把包裹类对象往它们对应的基本类型(以及容纳范围更广的类型)上转化。
类似这样的代码是不能工作的,尽管32并未超出byte所能表示的范围:
清单20:不能同时进行Auto-Unboxing和强制向下转型
System.out.println((byte) i);/* 编译时出错 */
这是因为编译器并不认可同时进行Auto-Unboxing和强制向下转型的操作,所以这个转化无法进行。如果一定要进行这种操作,需要手工补充一次转型:
清单21:需要先作Unboxing,再强制向下转型
System.out.println((byte)(int) i);
不过同时进行Auto-Unboxing和强制向上转型的操作是没有问题的,所以下面的代码工作得很正常:
清单22:可以同时进行Auto-Unboxing和强制向上转型
System.out.println((double) i);
7. 其它不能自动转化的情况
除去强制类型转化时的限制之外,还有这样一些情况下不会发生Autoboxing/Auto-Unboxing:
1. 基本类型的数组和包裹类数组之间不会自动转化。这样的代码完全不被编译器接受:
清单23:元素可以,容器不行
Integer[] integers = ints;/* 编译时出错 */
2. 不能对着基本类型的表达式来调用包裹类里的方法。这样的申请会被编译器彻底拒绝:
清单24:没有方法,就是没有方法
byte b = i.byteValue();/* 编译时出错 */
8. null的转化问题
Java里的引用类型可以有一个特别的取值——“null”。试图对null进行Auto-Unboxing操作会导致一个“NullPointerException”。
例如这段代码就会在运行时抛出异常,尽管在编译期间会表现得非常正常:
清单25:表面上,只是普通的赋值
int j = i;/* 运行时错误 */
这是因为这段代码实际上相当于:
清单26:实际上,是在试图调用null的方法
int j = i.intValue();/* 运行时错误 */
而试图调用null的方法是一种不被虚拟机认可的行为。
9. 对重载的影响
Java支持“重载”的机制,允许在同一个类拥有许多名称相同而形参列表不同的方法。然后,由编译器根据调用时的实参来选择到底要执行哪一个。
Autoboxing/Auto-Unboxing机制的引入,稍微增加了一些作这种选择时要考虑的因素——因为可能会有一个方法,既有一个能接受一个Integer型参数的版本,又有一个能接受一个int型参数的版本,而Autoboxing/Auto-Unboxing机制能自动的把实参在这两种类型之间转化,光凭原有的判断规则,二者是难以取舍的。但是,因为同时有这两个版本的做法完全合情合理,又不能在这里给出一个“reference to 被调用的方法名 is ambiguous”的编译错误来推卸责任。这就需要增加一条新的判断规则。
这条新增的规则是,不用进行Autoboxing/Auto-Unboxing的版本,优先于需要进行Autoboxing/Auto-Unboxing的版本。
因此,在这种情况下具体选择哪一个,要看传递的实参最初是什么类型。
清单27:不用进行Autoboxing/Auto-Unboxing的版本优先
{
private static void testOverloading(int i){
System.out.println("int");
}
private static void testOverloading(Integer i){
System.out.println("Integer");
}
public static void main(String[] args)
{
int i = 1;
Integer j = new Integer(1);
testOverloading(i);/* 输出“int” */
testOverloading(j);/* 输出“Integer” */
}
}
10. 值相等和引用相等
在Java语言中有两个不同的“相等”概念——值相等和引用相等。这样就有一个“两个值相等的基本类型数据,经过Autoboxing之后,得到的对象的引用是否相等”的问题。
在《JSR 201: Extending the Java Programming Language with Enumerations, Autoboxing, Enhanced for loops and Static Import》中,对这个问题,是作了这样的规定:
If the value p being boxed is true, false, a byte, an ASCII character, or an integer or short number between -127 and 128, then let r1 and r2 be the results of any two boxing conversions of p. It is always the case that r1 == r2.
这意味着这个答案可能是“是”也可能是“否”,由被Autoboxing的数据的类型和取值来决定。因此在检测两个对象是否代表相同的值的时候,还是有必要调用equals()方法来进行。
不过在J2SDK 1.5 Beta 1和Beta 2里的实际情况,和这稍微有些出入,“Autoboxing之后得到相同的对象引用”的范围被缩小了:
清单28:原来的值相等,经过Autoboxing之后的引用可能相等,也可能不相等
Boolean b1 = b;
Boolean b2 = b;
System.out.println(b1 == b2);/* 输出“true” */
char c = '1';
Character c1 = c;
Character c2 = c;
System.out.println(c1 == c2);/* 输出“false” */
11. 对性能的妨碍
由于Autoboxing机制的实质是“自动创建能代表基本类型数据的对象”,所以,不可避免的会对性能造成一些妨碍。
如果只是利用Autoboxing/Auto-Unboxing机制来保存基本类型的数据(例如把基本类型的数据放到Collection里面之类),这种影响倒还可以忽略,因为这只是把原来需要手工进行的工作自动化了;但是,如果要频繁的借助Autoboxing来给一个包裹类变量赋值,这开销很容易上升到需要加以注意的程度。
注意对包裹类的变量使用“++”和“--”运算符的时候,也会创建新的对象,而不是在修改原来对象的状态。
清单29:是替换不是修改
Integer j = i;/* 让j、i指向同一对象 */
System.out.println(j == i);/* 目前j、i是同一对象,因此输出“true” */
i++;
System.out.println(j == i);/* 现在j、i是不同对象,因此输出“false” */
System.out.println(i);/* 现在i的值是“2” */
System.out.println(j);/* 而j的值仍是“1” */
这个现象是由于Java里的包裹类是“不可变的(immutable)”——即没有提供一种能让自己所代表的值发生变化的途径——而造成的。
在需要大量赋值操作的时候,可以通过适当使用一些基本类型的局部变量来减轻对性能方面的影响。不过,如果性能的瓶颈在于要往一个容器里频繁放入基本类型的数据的话,恐怕就得靠改用一些专门为容纳基本类型的数据而设计的容器类来解决了(例如Jarkata Commons Primitives组件里提供的那些)。
清单30:一段需要往一个容器里频繁放入基本类型的数据的程序
public class WordCounter {
public static void main(String[] args) {
HashMap counts = new HashMap();
for (int i = 0; i < args.length; i++) {
String current = args[i];
if (counts.containsKey(current)) {
counts.put(current, ((Integer) counts.get(current)) + 1);
} else {
counts.put(current, 1);
}
}
for (Iterator itr = counts.keySet().iterator(); itr.hasNext();) {
String key = (String) itr.next();
System.out.println(key + ":" + counts.get(key));
}
}
}
12. 归纳总结
借助J2SE 1.5里提供的Autoboxing/Auto-Unboxing机制,可以用一种更简单的方式,来解决同时存在两套类型系统而造成的一些不方便。不过,这种机制并没有解决所有的相关问题,有些工作还是需要靠手工操作来进行。另外,由于不恰当的使用这一机制会造成一些性能方面的负面影响,所以在使用的时候还要注意一些问题才行。