第61项:基本类型优先于装箱基本类型

  Java有一个类型系统由两部分组成,包含基本类型(primitives),比如int,double和boolean和引用类型(reference type),例如String和List。每个基本类型都有一个对应的引用类型,称作装箱基本类型(boxed primitive)。装箱基本类型中对应于int、double和boolean的是Integer、Double和Boolean。

  如第6项所述,自动装箱(autoboxing)和自动拆箱(auto-unboxing)难以区分,但这并没有完全抹去基本类型和装箱类型之间的区别。这两种类型之间存在真正的差异,重要的是,你要清楚自己正在使用的是哪种类型,并且要在这两种类型之间进行谨慎的选择。

  在基本类型和装箱基本类型之间有三个主要区别。第一,基本类型只有值,而装箱基本类型具有与它们的值不同的同一性(identities)。换句话说,两个装箱基本类型可以具有相同的值和不同的同一性。第二,基本类型只有功能完备的值,而没个装箱基本类型除了它对应基本类型的所有功能值之外,还有个非功能值:null。最后,基本类型通常比装箱基本类型更节省时间和空间。如果不小心,这三点区别就会让你陷入麻烦中。

  考虑下面这个比较器,它被设计用来表示Integer值的递增数字顺序。(回想以下,比较器的compare方法返回的数值到底是负数、零还是正数,要取决于它的第一个参数是小于、等于还是大于它的第二个参数。)在实践中你并不需要编写这个比较器,因为在Integer中已经实现了它的自然排序比较器,但它展示了一个值得关注的例子:

// Broken comparator - can you spot the flaw?
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);

  这个比较器表面上看起来似乎不错,它可以通过许多测试。例如,它可以通过Collection.sort正确地给一个有一百万个元素的列表进行排序,无论这个列表中是否包含重复的元素。但是这个比较器有着很严重的缺陷。如果你要让自己信服,只要打印naturalOrder.compare(new Integer(42), new Integer(42)).的值。这两个Integer实例都表示相同的值(42),因此这个表达式的值应该为0,但它输出的却是1,这表明第一个Integer的值大于第二个。

  问题出在哪呢?naturalOrder中的第一个测试工作得很好。对表达式first < second执行计算会导致被first和second引用的Integer实例被自动拆箱(auto-unboxed),也就是说,它提取了它们的基本类型值。计算动作要检查产生的第一个int值是否小于第二个。但是假设答案是否定的。下一个测试就是执行计算表达式first == second,它在两个对象引用上执行同一性比较(identity comparison)。如果first和second引用表示同一个int值的不同Integer实例,这个比较操作就会返回false,比较器就会错误地返回1,表示第一个Integer值大于第二个。对装箱基本类型运用==操作符几乎总是错误的

  在实践中,如果你需要一个比较器来描述一个类型的自然顺序,你应该简单地调用Comparator.naturalOrder(),如果你自己编写一个比较器,你应该使用比较器构造方法,或基本类型的静态比较方法(第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章)中,必须使用装箱基本类型作为参数类型,因为Java语言不允许使用基本类型。例如,你不能将变量声明为ThreadLocal<int>类型。因此必须使用ThreadLocal<Integer>代替。最后,在进行反射的方法调用(第65项)时,必须使用装箱基本类型。

  总之,当可以选择的时候,基本类型要优先于装箱基本类型。基本类型更加简单,也更加快速。如果必须使用装箱基本类型,要特别小心!自动装箱减少了使用装箱基本类型的繁琐性,但是并没有减少它的风险 。当程序使用==操作符比较两个装箱基本类型时,它做了个同一性比较,这几乎可以肯定不是你想要的。当程序进行涉及装箱和拆箱基本类型的混合类型计算时,它会进行拆箱,而且,**当你的程序做自动拆箱操作的时候,它可能会(can)抛出一个NullPointerException异常。最后,当程序装箱了基本类型值时,它会导致不必要的高开销。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值