Tips
书中的源代码地址:
注意, 书中的有些代码里方法是基于 Java 9 API 中的, 所以 JDK 最好下载 JDK 9 以上的版本.
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);
- }
那么, 什么时候应该使用装箱原语呢? 它们有几个合法的用途. 第一个是作为集合中的元素, 键和值. 不能将原语放在集合中, 因此必须使用装箱的原语. 这是一般情况下的特例. 在参数化类型和方法 (第 5 章) 中, 必须使用装箱原语作为类型参数, 因为该语言不允许使用原语. 例如, 不能将变量声明为 ThreadLocal 类型, 因此必须使用 ThreadLocal. 最后, 在进行反射方法调用时, 必须使用装箱原语(第 65 项).
这个程序比它原本的速度慢得多, 因为它意外地声明了一个局部变量(sum), 它是装箱的基本类型 Long, 而不是基本类型 long. 程序在没有错误或警告的情况下编译, 变量被反复装箱和拆箱, 导致观察到的性能下降.
在本条目中讨论的所有三个程序中, 问题都是一样的: 程序员忽略了基本类型和包装基本类型之间的区别, 并承担了后果. 在前两个项目中, 结果是彻底的失败; 第三, 严重的性能问题.
那么, 什么时候应该使用装箱基本类型呢? 它们有几个合法的用途. 第一个是作为集合中的元素, 键和值. 不能将基本类型放在集合中, 因此必须使用装箱的基本类型. 这是一般情况下的特例. 在参数化类型和方法 (第 5 章) 中, 必须使用装箱基本类型作为类型参数, 因为该语言不允许使用基本类型. 例如, 不能将变量声明为 ThreadLocal<int > 类型, 因此必须使用 ThreadLocal<Integer>. 最后, 在进行反射方法调用时, 必须使用装箱基本类型(条目 65).
总之, 只要有选择, 就应该优先使用基本类型, 而不是装箱基本类型. 基本类型更简单, 更快. 如果必须使用装箱基本类型, 则需要小心! 自动装箱减少了使用装箱基本类型的冗长, 但没有降低使用的危险. 当程序使用 == 操作符比较两个装箱的基本类型时, 它会执行引用标识比较, 这几乎肯定不是你想要的. 当程序执行包含装箱和拆箱基本类型的混合类型计算时, 它会执行拆箱, 当程序执行拆箱时, 会抛出 NullPointerException. 最后, 当程序装箱了基本类型, 可能会导致代价高昂且创建了不必要的对象.
来源: https://www.cnblogs.com/IcanFixIt/p/10582913.html