pursue wind pursue wind
首页
Java
Python
数据库
框架
Linux
中间件
前端
计算机基础
DevOps
项目
面试
书
关于
归档
MacOS🤣 (opens new window)
GitHub (opens new window)
首页
Java
Python
数据库
框架
Linux
中间件
前端
计算机基础
DevOps
项目
面试
书
关于
归档
MacOS🤣 (opens new window)
GitHub (opens new window)
  • EffectiveJava

    • 考虑使用静态工厂方法替代构造方法
    • 当构造方法参数过多时使用builder模式
    • 使用私有构造方法或枚类实现Singleton属性
    • 使用私有构造方法执行非实例化
    • 依赖注入优于硬连接资源(hardwiring resources)
    • 避免创建不必要的对象
    • 消除过期的对象引用
    • 避免使用Finalizer和Cleaner机制
    • 使用try-with-resources语句替代try-finally语句
    • 重写equals方法时遵守通用约定
    • 重写equals方法时同时也要重写hashcode方法
    • 始终重写 toString 方法
    • 谨慎地重写 clone 方法
    • 考虑实现Comparable接口
    • 使类和成员的可访问性最小化
    • 在公共类中使用访问方法而不是公共属性
    • 最小化可变性
    • 组合优于继承
    • 要么设计继承并提供文档说明,要么禁用继承
    • 接口优于抽象类
    • 为后代设计接口
    • 接口仅用来定义类型
    • 类层次结构优于标签类
    • 支持使用静态成员类而不是非静态类
    • 将源文件限制为单个顶级类
    • 不要使用原始类型
    • 消除非检查警告
    • 列表优于数组
    • 优先考虑泛型
    • 优先使用泛型方法
    • 使用限定通配符来增加API的灵活性
    • 合理地结合泛型和可变参数
    • 优先考虑类型安全的异构容器
    • 使用枚举类型替代整型常量
    • 使用实例属性替代序数
    • 使用EnumSet替代位属性
    • 使用EnumMap替代序数索引
    • 使用接口模拟可扩展的枚举
    • 注解优于命名模式
    • 始终使用Override注解
    • 使用标记接口定义类型
    • lambda表达式优于匿名类
    • 方法引用优于lambda表达式
    • 优先使用标准的函数式接口
    • 明智审慎地使用Stream
    • 优先考虑流中无副作用的函数
    • 优先使用Collection而不是Stream来作为方法的返回类型
    • 谨慎使用流并行
    • 检查参数有效性
    • 必要时进行防御性拷贝
    • 仔细设计方法签名
    • 明智审慎地使用重载
    • 明智审慎地使用可变参数
    • 返回空的数组或集合,不要返回 null
    • 明智审慎地返回 Optional
    • 为所有已公开的 API 元素编写文档注释
    • 最小化局部变量的作用域
    • for-each 循环优于传统 for 循环
    • 了解并使用库
    • 若需要精确答案就应避免使用 float 和 double 类型
    • 基本数据类型优于包装类
    • 当使用其他类型更合适时应避免使用字符串
    • 当心字符串连接引起的性能问题
    • 通过接口引用对象
    • 接口优于反射
    • 明智审慎地本地方法
    • 明智审慎地进行优化
    • 遵守被广泛认可的命名约定
    • 只针对异常的情况下才使用异常
    • 对可恢复的情况使用受检异常,对编程错误使用运行时异常
    • 避免不必要的使用受检异常
    • 优先使用标准的异常
    • 抛出与抽象对应的异常
    • 每个方法抛出的异常都需要创建文档
    • 在细节消息中包含失败一捕获信息
    • 保持失败原子性
    • 不要忽略异常
    • 同步访问共享的可变数据
    • 避免过度同步
    • executor 、task 和 stream 优先于线程
    • 并发工具优于 wait 和 notify
    • 文档应包含线程安全属性
    • 明智审慎的使用延迟初始化
    • 不要依赖线程调度器
    • 优先选择 Java 序列化的替代方案
    • 非常谨慎地实现 Serializable
    • 考虑使用自定义的序列化形式
    • 保护性的编写 readObject 方法
    • 对于实例控制,枚举类型优于 readResolve
    • 考虑用序列化代理代替序列化实例
  • On Java 8

  • 书
  • EffectiveJava
pursuewind
2020-11-22

基本数据类型优于包装类

# 61. 基本数据类型优于包装类

Java 有一个由两部分组成的类型系统,包括基本类型(如 int、double 和 boolean)和引用类型(如 String 和 List)。每个基本类型都有一个对应的引用类型,称为包装类型。与 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);
1
2

这个比较器看起来应该可以工作,它将通过许多测试。例如,它可以与 Collections.sort 一起使用,以正确地排序一个百万元素的 List,无论该 List 是否包含重复的元素。但这个比较存在严重缺陷。要使自己相信这一点,只需打印 naturalOrder.compare(new Integer(42), new Integer(42)) 的值。两个 Integer 实例都表示相同的值 (42),所以这个表达式的值应该是 0,但它是 1,这表明第一个 Integer 值大于第二个!

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

在实际使用中,如果你需要一个比较器来描述类型的自然顺序,你应该简单地调用 Comparator.naturalOrder(),如果你自己编写一个比较器,你应该使用比较器构造方法,或者对基本类型使用静态比较方法(详见第 14 条)。也就是说,你可以通过添加两个局部变量来存储基本类型 int 值,并对这些变量执行所有的比较,从而修复损坏的比较器中的问题。这避免了错误的标识比较:

Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
    int i = iBoxed, j = jBoxed; // Auto-unboxing
    return i < j ? -1 : (i == j ? 0 : 1);
};
1
2
3
4

接下来,考虑一下这个有趣的小程序:

public class Unbelievable {
static Integer i;
public static void main(String[] args) {
    if (i == 42)
        System.out.println("Unbelievable");
    }
}
1
2
3
4
5
6
7

不,它不会打印出令人难以置信的东西,但它的行为很奇怪。它在计算表达式 i==42 时抛出 NullPointerException。问题是,i 是 Integer,而不是 int 数,而且像所有非常量对象引用字段一样,它的初值为 null。当程序计算表达式 i==42 时,它是在比较 Integer 与 int。在操作中混合使用基本类型和包装类型时,包装类型就会自动拆箱,这种情况无一例外。如果一个空对象引用自动拆箱,那么你将得到一个 NullPointerException。正如这个程序所演示的,它几乎可以在任何地方发生。修复这个问题非常简单,只需将 i 声明为 int 而不是 Integer。

最后,考虑条目 6 中第 24 页的程序:

// 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);
}
1
2
3
4
5
6
7
8

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

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

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

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

Last Updated: 2023/01/30, 11:01:00
若需要精确答案就应避免使用 float 和 double 类型
当使用其他类型更合适时应避免使用字符串

← 若需要精确答案就应避免使用 float 和 double 类型 当使用其他类型更合适时应避免使用字符串→

Theme by Vdoing | Copyright © 2019-2023 pursue-wind | 粤ICP备2022093130号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
  • 飙升榜
  • 新歌榜
  • 云音乐民谣榜
  • 美国Billboard榜
  • UK排行榜周榜
  • 网络DJ