第六十一条:基本类型优先于装箱基本类型

2021-03-22  本文已影响0人  Js_Gavin

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

如第6条提到的,自动装箱和自动拆箱模糊了但并没有完全抹去基本类型和装箱基本类型之间的区别。这两种类型之间真正是有差别的,要很清楚在使用的是哪种类型,并且要对这两种类型进行谨慎的选择,这些都是非常重要。

在基本类型和装箱类型之间有三个主要的区别。
第一:基本类型只有值,而装箱基本类型则具有与它们的值不同的同一性。换句话说,两个装箱基本类型可以具有相同的值和不同的同一性。

第二:基本类型只有函数值,而每个装箱类型则都有一个非函数值,除了它对基本类型的所有函数值之外,还有个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正确的给一个有100万个元素的列表进行排序,无论这个列表中是否包含重复的元素。但是这个比较器有严重的缺陷。如果你要自己信服,只要打印naturalOrder.Compare(new Integer(42),new Integer(42))的值便可以分晓。这两个Integer实例都表示相同的值(42),因此这个表达式的值应该为0,但它输出的却是1,这表明第一个Integer值大于第二个。

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

事实上,如果需要用比较器描述一个类型的自然顺序,只要调用Comparator.naturalOrder()即可,如果自己编写比较器,则应该使用比较器构造方法,或者在基本类型上使用静态比较方法。也就是说,修正这个问题的做法是添加两个局部变量,来保存对于装箱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即可。

最后,以第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条)时,必须使用装箱基本类型

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

上一篇下一篇

猜你喜欢

热点阅读