Effective Java 第三版——61. 基本类型优于装箱的基本类型

Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,所以JDK 最好下载 JDK 9以上的版本。

Effective Java, Third Edition

61. 基本类型优于装箱的基本类型

Java是一个由两部分类型组成的系统,一部分由基本类型组成,如int,double和boolean,还有一部分是引用类型,如String和List。 每个基本类型都有一个相应的引用类型,称为装箱基本类型。 对应于int,double和boolean的包装基本类型是Integer,Double和Boolean。

正如条目6中提到的,自动装箱和自动拆箱模糊了基本类型和装箱基本类型之间的区别,但不会消除它们。这两者之间有真正的区别,重要的是要始终意识到你正在使用的是哪一种,并在它们之间仔细选择。

基本类型和包装基本类型之间有三个主要区别。首先,基本类型只有它们的值,而包装基本类型具有与其值不同的标识。换句话说,两个包装基本类型实例可以具有相同的值但不同的引用标识。第二,基本类型只有功能的值(functional value),而每个包装基本类型类型除了对应的基本类型的功能值外,还有一个非功能值,即null。最后,基本类型比包装的基本类型更节省时间和空间。如果你不小心的话,这三种差异都会给你带来真正的麻烦。

考虑下面的比较器,它的设计目的是表示Integer值的升序数字顺序。(回想一下,比较器的compare方法返回一个负数、零或正数,这取决于它的第一个参数是小于、等于还是大于第二个参数)。你不需要在实践中编写这个比较器,因为它实现了Integer的自然排序,但它提供了一个有趣的例子:

// 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))的值。 两个Integer实例都表示相同的值(42),因此该表达式的值应为0,但它为1,表示第一个Integer值大于第二个值!

那么问题出在哪里呢?naturalOrder中的第一个测试工作得很好。计算表达式i < j会使i和j引用的整数实例自动拆箱;也就是说,它提取它们的基本类型值。计算的目的是检查得到的第一个int值是否小于第二个int值。但假设是否定的。然后,下一个测试计算表达式i==j,该表达式对两个对象执行引用标识比较。如果i和j引用表示相同整型值的不同Integer实例,这个比较将返回false,比较器将错误地返回1,表明第一个整型值大于第二个整型值。将==操作符应用于装箱的基本类型几乎总是错误的

在实践中,如果你需要一个比较器来描述类型的自然顺序,应该简单地调用comparator . naturalorder()方法,如果自己编写一个比较器,应该使用比较器构造方法,或者对基本类型使用静态compare方法(条目 14)。也就是说,可以通过添加两个局部变量来存储与装箱Integer参数对应的原始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是Integer类型,而不是int类型,而且像所有非常量对象引用属性一样,它的初始值为null。当程序计算表达式i==42时,它是在比较Integer和int之间的关系。 几乎在每种情况下,当在基本类型和包装基本类型进行混合操作时,包装基本类型会自动拆箱。如果对一个null对象进行自动拆箱,那么会抛出NullPointerException。正如这个程序所演示的,它几乎可以在任何地方发生。修复这个问题非常简单,只需将i声明为int而不是Integer就可以了。

最后,考虑第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。程序在没有错误或警告的情况下编译,变量被反复装箱和拆箱,导致观察到的性能下降。

在本条目中讨论的所有三个程序中,问题都是一样的:程序员忽略了基本类型和包装基本类型之间的区别,并承担了后果。在前两个项目中,结果是彻底的失败;第三,严重的性能问题。

那么,什么时候应该使用装箱基本类型呢?它们有几个合法的用途。第一个是作为集合中的元素、键和值。不能将基本类型放在集合中,因此必须使用装箱的基本类型。这是一般情况下的特例。在参数化类型和方法(第5章)中,必须使用装箱基本类型作为类型参数,因为该语言不允许使用基本类型。例如,不能将变量声明为ThreadLocal<int>类型,因此必须使用ThreadLocal<Integer>。最后,在进行反射方法调用时,必须使用装箱基本类型(条目 65)。

总之,只要有选择,就应该优先使用基本类型,而不是装箱基本类型。基本类型更简单、更快。如果必须使用装箱基本类型,则需要小心!自动装箱减少了使用装箱基本类型的冗长,但没有降低使用的危险。当程序使用==操作符比较两个装箱的基本类型时,它会执行引用标识比较,这几乎肯定不是你想要的。当程序执行包含装箱和拆箱基本类型的混合类型计算时,它会执行拆箱,当程序执行拆箱时,会抛出NullPointerException。最后,当程序装箱了基本类型,可能会导致代价高昂且创建了不必要的对象。