ITEM 61: PREFER PRIMITIVE TYPES TO BOXED PRIMITIVES
Java有两种类型系统,由基本类型(如int、double和boolean)和引用类型(如String和List)组成。每个原语类型都有一个对应的引用类型,称为装箱原语。与 int、double和 boolean 对应的装箱原语是Integer、Double和 Boolean。
正如 item 6 中提到的,自动装箱和自动拆箱模糊了原始类型和装箱原始类型之间的区别。这两者之间有真正的区别,重要的是你要知道你用的是哪一种,并在两者之间仔细选择。
原语和装箱类型之间有三个主要区别。首先,原语只有它们的值,而装箱的原语有不同于它们的值的标识。换句话说,两个装箱的原语实例可以具有相同的值和不同的标识。第二,原语类型只有全功能值,而每个装箱的原语类型除了对应原语类型的所有功能值外,还有一个非功能值,即null。最后,原语比盒装原语更节省时间和空间。如果你不小心的话,这三种差异会给你带来真正的麻烦。
考虑下面的比较器,它的设计目的是表示整数值上的升序数字。(回想一下,comparator 的 compare 方法返回一个负数、零或正数,具体取决于它的第一个参数是小于、等于还是大于第二个参数。) 你不需要在实践中编写这个比较器,因为它实现了整数的自然排序,但它是一个有趣的例子:
// Broken comparator - can you spot the flaw?
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
这个比较器看起来应该可以工作,它将通过许多测试。例如,它可以用于Collections.sort : 正确地对一个百万元素列表排序,不管这个列表是否包含重复的元素。但这个比较器存在严重缺陷。要发现这一点,只需打印naturalOrder.compare(new Integer(42), new Integer(42))的值。
这两个整数实例表示相同的值(42),因此这个表达式的值应该是0,但它是1,这表明第一个整数值大于第二个整数值!
那么问题是什么呢?naturalOrder 中的第一个测试工作正常。对表达式 i < j 求值会使 i 和 j 引用的整数实例自动取消装箱;也就是说,它提取它们的原始值。计算的目的是检查结果 int 值的第一个值是否小于第二个值。但假设它不是。然后,下一个测试计算表达式 i==j,该表达式对两个对象引用执行身份比较。
如果 i 和 j 引用表示相同 int 值的不同整数实例,那么这个比较将返回 false,而comparator 将错误地返回 1,表明第一个整数值大于第二个整数值。对装箱原语应用== 操作符几乎总是错误的。
在实践中,如果您需要一个比较器来描述类型的自然顺序,您应该简单地调用Comparator.naturalOrder(),如果您自己编写一个比较器,您应该使用比较器构造方法,或者对基本类型使用静态比较方法(item 14)。也就是说,您可以通过添加两个局部变量来存储与装箱的整数参数相对应的原始 int 值,并对这些变量执行所有的比较,从而修复损坏的比较器中的问题。这避免了错误的身份比较:
Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> { int i = iBoxed, j = jBoxed; // Auto-unboxing
return i < j ? -1 : (i == j ? 0 : 1);
};
接下来,想想这个有趣的小程序:
public class Unbelievable {
static Integer i;
public static void main(String[] args) {
if (i == 42)
System.out.println("Unbelievable");
}
}
不,它不会打印出 "Unbelievable" —— 但它所做的几乎同样奇怪。它在计算表达式i==42 时抛出 NullPointerException。问题是,i 是整数,而不是整型的,而且像所有的非常量对象引用字段一样,它的初始值是 null。当程序计算表达式 i==42 时,它是在比较整数和整型数。如果一个空对象引用是自动 unboxed 的,那么您将得到一个NullPointerException。正如这个程序所演示的,它几乎可以在任何地方发生。修复这个问题非常简单,只需将 i 声明为 int 而不是整数。最后,考虑第 24 页第 6 项中的程序:
// Hideously slow program! Can you spot the object creation?
public static void main(String[] args) {
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}
这个程序比它应有的速度慢得多,因为它意外地将一个局部变量(sum)声明为装箱的原始类型 Long,而不是原始类型 long。程序编译时没有错误或警告,变量被反复装箱或不装箱,导致观察到的性能下降。
在本项目中讨论的所有三个程序中,问题都是一样的:程序员忽略了原语和装箱原语之间的区别,并承受了后果。在前两个项目中,结果是彻底的失败;第三个项目存在严重的性能问题。
那么什么时候应该使用装箱原语呢?它们有几个合法的用途。第一种是作为集合中的元素、键和值。您不能将原语放入集合中,因此必须使用盒装的原语。这是一般情况下的一个特例。在参数化类型和方法(item 5)中,必须使用装箱的原语作为类型参数,因为该语言不允许使用原语。例如,不能将变量声明为 ThreadLocal<int> 类型,因此必须使用 ThreadLocal<Integer> 。最后,在进行反射方法调用时,必须使用装箱原语(item 65)。总之,只要有选择,就应该优先使用原语,而不是装箱类型。原始类型更简单、更快。如果必须使用装箱类型,请小心!自动装箱减少了使用装箱类型的冗长,但没有减少危险。当您的程序将两个装箱原语与 == 操作符进行比较时,它将执行标识比较,这几乎肯定不是您想要的。当您的程序执行包含已装箱和未装箱原语的混合类型计算时,它将进行拆箱,而当您的程序进行拆箱时,它将抛出NullPointerException。最后,当您的程序将原语值框起来时,可能会导致代价高昂且不必要的对象创建。
网友评论