高效Java(第三版) Effective Java
1. 考虑使用静态工厂方法替代构造方法 2. 当构造方法参数过多时使用builder模式 3. 使用私有构造方法或枚类实现Singleton属性 4. 使用私有构造方法执行非实例化 5. 使用依赖注入取代硬连接资源 6. 避免创建不必要的对象 7. 消除过期的对象引用 8. 避免使用Finalizer和Cleaner机制 9. 使用try-with-resources语句替代try-finally语句 10. 重写equals方法时遵守通用约定 11. 重写equals方法时同时也要重写hashcode方法 12. 始终重写 toString 方法 13. 谨慎地重写 clone 方法 14. 考虑实现Comparable接口 15. 使类和成员的可访问性最小化 16. 在公共类中使用访问方法而不是公共属性 17. 最小化可变性 18. 组合优于继承 19. 如果使用继承则设计,并文档说明,否则不该使用 20. 接口优于抽象类 21. 为后代设计接口 22. 接口仅用来定义类型 23. 优先使用类层次而不是标签类 24. 优先考虑静态成员类 25. 将源文件限制为单个顶级类 26. 不要使用原始类型 27. 消除非检查警告 28. 列表优于数组 29. 优先考虑泛型 30. 优先使用泛型方法 31. 使用限定通配符来增加API的灵活性 32. 合理地结合泛型和可变参数 33. 优先考虑类型安全的异构容器 34. 使用枚举类型替代整型常量 35. 使用实例属性替代序数 36. 使用EnumSet替代位属性 37. 使用EnumMap替代序数索引 38. 使用接口模拟可扩展的枚举 39. 注解优于命名模式 40. 始终使用Override注解 41. 使用标记接口定义类型 42. lambda表达式优于匿名类 43. 方法引用优于lambda表达式 44. 优先使用标准的函数式接口 45. 明智审慎地使用Stream 46. 优先考虑流中无副作用的函数 47. 优先使用Collection而不是Stream来作为方法的返回类型 48. 谨慎使用流并行 49. 检查参数有效性 50. 必要时进行防御性拷贝 51. 仔细设计方法签名 52. 明智而审慎地使用重载 53. 明智而审慎地使用可变参数 54. 返回空的数组或集合不要返回null 55. 明智而审慎地返回Optional 56. 为所有已公开的API元素编写文档注释 57. 最小化局部变量的作用域 58. for-each循环优于传统for循环 59. 熟悉并使用Java类库 60. 需要精确的结果时避免使用float和double类型 61. 基本类型优于装箱的基本类型 62. 当有其他更合适的类型时就不用字符串 63. 注意字符串连接的性能 64. 通过对象的接口引用对象 65. 接口优于反射 66. 明智谨慎地使用本地方法 67. 明智谨慎地进行优化 68. 遵守普遍接受的命名约定 69. 仅在发生异常的条件下使用异常 70. 对可恢复条件使用检查异常,对编程错误使用运行时异常 71. 避免不必要地使用检查异常 72. 赞成使用标准异常 73. 抛出合乎于抽象的异常 74. 文档化每个方法抛出的所有异常 75. 在详细信息中包含失败捕获信息 76. 争取保持失败原子性 77. 同步访问共享的可变数据 78. 避免过度同步 79. EXECUTORS, TASKS, STREAMS 优于线程 80. 优先使用并发实用程序替代wait和notify 81. 线程安全文档化 82. 明智谨慎地使用延迟初始化 83. 不要依赖线程调度器 84. 其他替代方式优于Java本身序列化 85. 非常谨慎地实现SERIALIZABLE接口 86. 考虑使用自定义序列化形式 87. 防御性地编写READOBJECT方法 88. 对于实例控制,枚举类型优于READRESOLVE 89. 考虑序列化代理替代序列化实例

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

Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于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);
}

这个程序比它原本的速度慢得多,因为它意外地声明了一个局部变量(sum),它是装箱的基本类型Long,而不是基本类型long。程序在没有错误或警告的情况下编译,变量被反复装箱和拆箱,导致观察到的性能下降。

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

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

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