编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • JVM内存模型与对象
      • 类加载与双亲委派
      • 垃圾回收与GC调优
      • 异常体系与JVM机制
      • 字节码指令集javap实战
      • JIT编译与去优化机制
      • JVM性能诊断工具链
      • OOM八大现场全景剖析
      • JVM参数调优全景图
      • GraalVM与AOT编译原理
      • HashMap底层哈希设计
      • String不可变与常量池
      • ArrayList与LinkedList源码
      • ConcurrentHashMap并发
      • TreeMap与红黑树原理
      • LinkedHashMap与LRU实现
      • Java数字类型原理
        • 1. 案例引入
          • 1.1 一分钱的事故
          • 1.2 == 与 equals 的背叛
          • 1.3 我们要回答什么
        • 2. 数字类型全景
          • 2.1 八种基本类型
          • 2.2 包装类型族谱
          • 2.3 高精度类型
        • 3. Integer 缓存池
          • 3.1 -128 到 127 的秘密
          • 3.2 valueOf 源码剖析
          • 3.3 缓存边界可调
          • 3.4 其他包装类的缓存
        • 4. 自动装箱拆箱
          • 4.1 字节码视角揭秘
          • 4.2 拆箱的 NPE 陷阱
          • 4.3 三元表达式诡异
          • 4.4 Stream 求和踩坑
        • 5. IEEE 754 浮点本质
          • 5.1 二进制无法表示 0.1
          • 5.2 float 与 double 内存布局
          • 5.3 NaN 与 Infinity
          • 5.4 浮点比较为什么不能用 ==
        • 6. BigDecimal 精度
          • 6.1 内部 unscaledValue 与 scale
          • 6.2 String 构造的必要性
          • 6.3 RoundingMode 八种选择
          • 6.4 除法必须指定精度
        • 7. 数值溢出与边界
          • 7.1 整型溢出无声无息
          • 7.2 Math.addExact 防御
          • 7.3 long 转 int 截断
          • 7.4 BigInteger 无界算术
        • 8. 性能与选型
          • 8.1 装箱开销实测
          • 8.2 BigDecimal 性能代价
          • 8.3 三套金额方案对比
        • 9. 现代特性演进
          • 9.1 Valhalla 值类型
          • 9.2 数字字面量增强
          • 9.3 Math 与 StrictMath
        • 10. 综合案例串讲
          • 10.1 一分钱真相揭晓
          • 10.2 一笔金额的一生
          • 10.3 设计哲学回扣
          • 10.4 数字类型速查表
      • Object通用方法的契约
      • 泛型擦除与类型系统
      • 枚举原理与最佳实践
      • 注解原理与编译期处理
      • Lambda与引用底层原理
      • Stream原理与流水线设计
      • Optional设计原理
      • Record密封类与模式
      • 反射机制与动态代理
      • MethodHandle与VarHandle
      • 三大字节码框架对比
      • JavaAgent与Instrumentation机制
      • AOP三种实现路线对比
      • synchronized与锁升级
      • volatile与JMM内存模型
      • 线程池核心源码设计
      • Thread线程生命周期
      • AQS同步框架源码
      • 并发锁三剑客
      • CAS和Atomic深入分析
      • 五大同步器对比
      • CompletableFuture异步
      • IO模型演进BIO到AIO
      • ByteBuffer与堆外内存
      • 序列化原理与替代方案
      • 文件IO与NIO.2
      • 面向对象的真意
      • JDK设计模式上
      • JDK设计模式下
      • SPI与模块化设计
  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Java入门精通
  • 专栏博客
杨充
2026-06-02
目录

Java数字类型原理

# 17.Java数字类型原理

# 目录介绍

  • 1. 案例引入
    • 1.1 一分钱的事故
    • 1.2 == 与 equals 的背叛
    • 1.3 我们要回答什么
  • 2. 数字类型全景
    • 2.1 八种基本类型
    • 2.2 包装类型族谱
    • 2.3 高精度类型
  • 3. Integer 缓存池
    • 3.1 -128 到 127 的秘密
    • 3.2 valueOf 源码剖析
    • 3.3 缓存边界可调
    • 3.4 其他包装类的缓存
  • 4. 自动装箱拆箱
    • 4.1 字节码视角揭秘
    • 4.2 拆箱的 NPE 陷阱
    • 4.3 三元表达式诡异
    • 4.4 Stream 求和踩坑
  • 5. IEEE 754 浮点本质
    • 5.1 二进制无法表示 0.1
    • 5.2 float 与 double 内存布局
    • 5.3 NaN 与 Infinity
    • 5.4 浮点比较为什么不能用 ==
  • 6. BigDecimal 精度
    • 6.1 内部 unscaledValue 与 scale
    • 6.2 String 构造的必要性
    • 6.3 RoundingMode 八种选择
    • 6.4 除法必须指定精度
  • 7. 数值溢出与边界
    • 7.1 整型溢出无声无息
    • 7.2 Math.addExact 防御
    • 7.3 long 转 int 截断
    • 7.4 BigInteger 无界算术
  • 8. 性能与选型
    • 8.1 装箱开销实测
    • 8.2 BigDecimal 性能代价
    • 8.3 三套金额方案对比
  • 9. 现代特性演进
    • 9.1 Valhalla 值类型
    • 9.2 数字字面量增强
    • 9.3 Math 与 StrictMath
  • 10. 综合案例串讲
    • 10.1 一分钱真相揭晓
    • 10.2 一笔金额的一生
    • 10.3 设计哲学回扣
    • 10.4 数字类型速查表

# 1. 案例引入

# 1.1 一分钱的事故

某支付公司清算系统,凌晨对账时发现总账少了 873 元。所有交易明细都在,金额加总却对不上。开发团队定位到一段"看起来很正常"的折扣计算代码:

public class OrderCalculator {
    
    // 计算订单实付金额:原价 × (1 - 折扣率)
    public double calculatePayAmount(double originalPrice, double discountRate) {
        return originalPrice * (1 - discountRate);
    }
    
    // 累加订单总额
    public double sum(List<Order> orders) {
        double total = 0.0;
        for (Order order : orders) {
            total += order.getPayAmount();
        }
        return total;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

跑一个最小复现:

double price = 100.0;
double rate = 0.1;
double pay = price * (1 - rate);
System.out.println(pay);          // 输出:89.99999999999999
                                  // 期望:90.0
1
2
3
4
5

100 × 0.9 = 89.99999999999999 ——计算机算错了?不,是 IEEE 754 给我们上了一课。

事故复盘时,技术负责人在白板上画了三个问号:

追问 ①:double 为什么算不准 100 × 0.9?计算机不是绝对精确的吗?
追问 ②:把 double 换成 BigDecimal 就完美了?为什么有人写 new BigDecimal(0.1) 还是不准?
追问 ③:如果用 long 存"分"代替 double 存"元",能不能彻底避坑?代价是什么?
1
2
3

# 1.2 == 与 equals 的背叛

同一周,订单系统又爆出第二个事故——VIP 用户列表去重失败:

Map<Integer, User> vipMap = new HashMap<>();
Integer userId1 = 200;
Integer userId2 = 200;
vipMap.put(userId1, new User("Alice"));
vipMap.put(userId2, new User("Alice"));
System.out.println(vipMap.size());      // 输出:1(Map 去重,正常)

// 但另一个判等场景出问题:
Integer a = 100, b = 100;
Integer c = 200, d = 200;
System.out.println(a == b);             // true
System.out.println(c == d);             // ★ false !!!
1
2
3
4
5
6
7
8
9
10
11
12

100 相等,200 不相等——Java 给开发者埋的最阴险陷阱之一。

事故复盘加了三个追问:

追问 ④:为什么 Integer 100 == 100 是 true,200 == 200 反而是 false?
追问 ⑤:自动装箱拆箱的字节码到底做了什么?为什么会引发 NPE?
追问 ⑥:Integer 缓存池为什么是 -128~127?这个边界是怎么定的?
追问 ⑦:除了 Integer,Long/Short/Character/Boolean 都有缓存池吗?
1
2
3
4

# 1.3 我们要回答什么

第 23 篇要把"Java 数字类型"这件事讲透——从 IEEE 754 二进制本质到 BigDecimal 精度控制、从 Integer 缓存到自动装箱字节码。

带着 7 个核心问题展开:

① IEEE 754 为什么算不准 0.1?二进制的根本局限在哪里?     → 第5章
② BigDecimal(double) vs BigDecimal(String) 差在哪?         → 第6章
③ Integer -128~127 缓存的边界由什么决定?                   → 第3章
④ 自动装箱字节码做了什么?为什么会引发 NPE?                → 第4章
⑤ 整型溢出为什么是"沉默的杀手"?怎么防御?                  → 第7章
⑥ 金额场景应该选 double / BigDecimal / long(分) 中的谁?    → 第8章
⑦ 浮点比较应该怎么写?为什么不能用 ==?                     → 第5章
1
2
3
4
5
6
7

本篇路线:

数字类型全景 (第2章) ─── 8基本+8包装+2高精度
    ↓
Integer 缓存 (第3章)        ←—— -128~127 的边界推导
自动装箱拆箱 (第4章)         ←—— invokestatic Integer.valueOf
    ↓
IEEE 754 浮点本质 (第5章)    ←—— 0.1 为什么是无限循环二进制
BigDecimal 精度 (第6章)      ←—— unscaledValue + scale 的设计
数值溢出与边界 (第7章)        ←—— Math.addExact 与 BigInteger
    ↓
性能与选型 (第8章)            ←—— 三套金额方案实测对比
现代特性演进 (第9章)          ←—— Valhalla / 数字字面量 / StrictMath
    ↓
综合案例串讲 (第10章)
1
2
3
4
5
6
7
8
9
10
11
12
13

# 2. 数字类型全景

# 2.1 八种基本类型

Java 有 8 种基本类型,其中 6 种是数字(boolean 与 char 不算严格数字):

类型 字节 位数 范围 默认值
byte 1 8 -128 ~ 127 0
short 2 16 -32768 ~ 32767 0
int 4 32 -2³¹ ~ 2³¹-1(约 ±21 亿) 0
long 8 64 -2⁶³ ~ 2⁶³-1(约 ±922 亿亿) 0L
float 4 32 IEEE 754 单精度 0.0f
double 8 64 IEEE 754 双精度 0.0d
char 2 16 0 ~ 65535(无符号) '\u0000'
boolean — — true / false false

关键观察:

  • 整型(byte/short/int/long)使用二进制补码——加法运算与无符号一致,硬件友好
  • 浮点(float/double)使用 IEEE 754 标准——这是 §5 章一切问题的源头
  • char 是 16 位无符号整数(UTF-16 编码单元)——本质上也能参与算术

疑惑:为什么 boolean 不告知字节数?

论证:JVM 规范说 boolean 在内存中与 int 等价(4 字节),但在 boolean 数组里被压缩成 1 字节——具体由 JVM 实现决定。这是"逻辑类型"和"存储类型"分离的典型设计。

# 2.2 包装类型族谱

每个基本类型对应一个包装类——为了适配"一切皆对象"的世界(集合泛型、null 语义、反射):

                   Number (abstract)
                       │
       ┌────────┬──────┼──────┬────────┐
       │        │      │      │        │
   Integer  Long  Short  Byte  Float  Double  ← 都继承 Number
   
   Boolean  Character                          ← 不继承 Number
1
2
3
4
5
6
7

Number 抽象类的契约——强制子类提供 6 种类型转换方法:

public abstract class Number implements Serializable {
    public abstract int intValue();
    public abstract long longValue();
    public abstract float floatValue();
    public abstract double doubleValue();
    public byte byteValue() { return (byte) intValue(); }      // 默认实现
    public short shortValue() { return (short) intValue(); }   // 默认实现
}
1
2
3
4
5
6
7
8

结论:Number 是"数字之间互转"的契约——类似 Comparable 是"可比较"的契约。BigDecimal 与 BigInteger 也继承 Number,所以 ((Number) bigDecimal).doubleValue() 是合法的。

# 2.3 高精度类型

java.math 包下两个"无界精度"类型:

类型 用途 内部存储 性能
BigInteger 任意精度整数 int[] 表示位数据 比 long 慢 50~200 倍
BigDecimal 任意精度小数 BigInteger + 标度 (scale) 比 double 慢 100~500 倍

它们是不可变对象——任何运算返回新对象,永不修改原值(与 String 同源设计哲学,§5 篇有详述)。

结论:基本类型是"硬件原生类型"——快但有边界;包装类型是"对象化包装"——慢但能进集合;BigInteger/BigDecimal 是"数学纯净类型"——无界但极慢。三层之间的取舍贯穿本篇。

# 3. Integer 缓存池

# 3.1 -128 到 127 的秘密

回到 §1.2 案例——Integer 100 == 100 是 true,Integer 200 == 200 反而是 false。

真相:JDK 在 Integer 类初始化时,预创建了 -128 ~ 127 之间所有 256 个 Integer 对象,缓存在 IntegerCache.cache[] 数组里。任何在此范围内的装箱操作都返回同一个缓存对象——所以 == 比较地址也成立。

疑惑:为什么是 -128 ~ 127?这个边界怎么定的?

论证——三个层面的考量:

  1. byte 的天然边界:-128 ~ 127 正好是 1 字节有符号整数的全部取值——这是计算机最常见的小整数范围
  2. 统计学考量:实际程序中 90% 以上的整数运算都在小整数范围(循环计数器、数组索引、状态码)
  3. 内存预算:256 个 Integer 对象 ≈ 4KB——常驻内存可接受

结论:-128 是"必须缓存",127 是"性价比上界"——超过 127 的整数出现频率骤降,缓存收益不再划算。这是一个工程权衡,不是数学必然。

# 3.2 valueOf 源码剖析

打开 Integer.valueOf 源码:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)   // -128 <= i <= 127
        return IntegerCache.cache[i + (-IntegerCache.low)];  // 命中缓存
    return new Integer(i);                                  // 未命中,新建对象
}

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];
    
    static {
        // high 默认 127,可通过 -XX:AutoBoxCacheMax=N 调高
        int h = 127;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                h = Math.min(i, Integer.MAX_VALUE - (-low) - 1);
            } catch (NumberFormatException nfe) { }
        }
        high = h;
        cache = new Integer[(high - low) + 1];
        int j = low;
        for (int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

关键观察:

  1. < 与 <= 的边界:判断条件是 >= -128 && <= 127——边界值 -128 和 127 都命中缓存
  2. 下界固定为 -128:JLS 规范强制要求,不可调
  3. 上界可调:通过 -XX:AutoBoxCacheMax=2000 可以扩大缓存——某些缓存密集场景(如 ID 范围)有用

# 3.3 缓存边界可调

实战调优:某交易系统订单 ID 集中在 1~10000 范围——把 AutoBoxCacheMax 调到 10000:

java -XX:AutoBoxCacheMax=10000 -jar app.jar
1

收益:

  • 减少 9873 次 new Integer() 对象分配
  • GC 压力下降——年轻代回收频率减少

代价:

  • 永久占用 10001 × 16B ≈ 160 KB 老年代
  • 不要无脑扩大——如果业务整数分布在百万级,缓存反而成累赘

结论:缓存大小是一个性能 vs 内存的权衡——默认 127 是 JDK 给出的"最安全配置",扩大需要业务证据。

# 3.4 其他包装类的缓存

不仅 Integer 有缓存——5 个包装类都有,但范围各异:

包装类 缓存范围 可调? 原因
Byte -128 ~ 127(全部) 否 byte 总共就 256 个值
Short -128 ~ 127 否 与 Integer 对齐
Integer -128 ~ 127 是(AutoBoxCacheMax) 最常用
Long -128 ~ 127 否 大数情况太多,不可调
Character 0 ~ 127(ASCII) 否 覆盖 ASCII 字符
Boolean TRUE / FALSE 两个常量 — 总共就两个值
Float 不缓存 — 浮点数不可枚举
Double 不缓存 — 同上

关键陷阱:

Long a = 100L, b = 100L;
System.out.println(a == b);            // true(命中缓存)

Long c = 200L, d = 200L;
System.out.println(c == d);            // false(超出缓存)

Long e = 200L, f = 200L;
System.out.println(e.equals(f));       // ★ true(应该用 equals)
1
2
3
4
5
6
7
8

结论:包装类型的判等永远用 equals,永远不要用 ==——这是本章最重要的告诫。

# 4. 自动装箱拆箱

# 4.1 字节码视角揭秘

自动装箱(Autoboxing)和拆箱(Unboxing)是编译器糖——JVM 不认识它,只有 javac 在编译期插入转换调用。

装箱字节码:

Integer a = 100;
1
javap -c 输出:
0: bipush  100              ← 把 int 100 压栈
2: invokestatic Integer.valueOf(int) → Integer    ← ★ 调用 valueOf 装箱
5: astore_1                 ← 存入局部变量
1
2
3
4

拆箱字节码:

int b = a;                  // a 是 Integer
1
0: aload_1                  ← 加载 Integer 引用
1: invokevirtual Integer.intValue() → int          ← ★ 调用 intValue 拆箱
4: istore_2
1
2
3

结论:装箱 = Integer.valueOf(int),拆箱 = Integer.intValue()——简单到一行代码。但简单背后藏着大量陷阱(接下来 3 节)。

# 4.2 拆箱的 NPE 陷阱

最常见的踩坑代码:

Map<String, Integer> userScores = new HashMap<>();
// 用户没有评分,userScores.get("Alice") 返回 null
int score = userScores.get("Alice");      // ★ NullPointerException !
1
2
3

字节码视角:

invokevirtual HashMap.get(Object) → Object  ← 返回 null
checkcast Integer                            ← 类型转换 OK
invokevirtual Integer.intValue()             ← ★ 在 null 上调用方法 → NPE
1
2
3

疑惑:为什么编译器不能识别这个 NPE?

论证:编译器无法静态推断"Map 是否包含 Alice"——必须运行时才知道返回值。自动拆箱把"可能为 null"的引用强制转成"必须有值"的基本类型,类型系统在这里有断层。

修复方案:

// 方案 A:默认值
int score = userScores.getOrDefault("Alice", 0);

// 方案 B:Optional
int score = Optional.ofNullable(userScores.get("Alice")).orElse(0);

// 方案 C:显式检查
Integer s = userScores.get("Alice");
int score = (s == null) ? 0 : s;
1
2
3
4
5
6
7
8
9

结论:任何"包装类型 → 基本类型"赋值都是一个潜在 NPE 点——CodeReview 时要重点检查。

# 4.3 三元表达式诡异

阿里 P3C 规范明确禁止的写法:

Integer a = 1;
boolean flag = false;
Integer result = flag ? a : 0;
// 期望:result = 0
// 实际:报 NullPointerException 如果 a 是 null

// 另一个诡异
Integer x = null;
Integer y = true ? x : 0;          // ★ NPE!
1
2
3
4
5
6
7
8
9

真相:三元表达式 cond ? a : b 当 a 和 b 类型不同(一个 Integer 一个 int)时,编译器会把两边统一成 int——对 Integer 调用 intValue(),null 上调用方法 → NPE。

字节码证据:

ifeq 14                  ← 条件判断
aload x                  
invokevirtual Integer.intValue()    ← ★ 强制拆箱,null 时崩
goto 17
14: iconst_0
17: invokestatic Integer.valueOf(int)   ← 重新装箱赋给 result
1
2
3
4
5
6

结论:三元表达式两端的类型不匹配,会触发隐式拆箱——是 NPE 的隐藏制造工厂。

# 4.4 Stream 求和踩坑

Stream API 上的装箱性能炸弹:

// 反例:列表求和
List<Integer> nums = Arrays.asList(1, 2, 3, /* ... 1 万个 */);

// ❌ 慢
int sum1 = nums.stream().mapToInt(Integer::intValue).sum();
// 还行——已经显式转 IntStream

// ❌❌ 更慢
int sum2 = nums.stream().reduce(0, Integer::sum);
// 全程在 Stream<Integer> 上跑,每次 sum 都要装箱拆箱

// ❌❌❌ 最慢
int sum3 = nums.stream().collect(Collectors.summingInt(Integer::intValue));
1
2
3
4
5
6
7
8
9
10
11
12
13

JMH 实测对比(1 万元素):

方案 耗时 装箱次数
普通 for 循环 + int 8 μs 0
mapToInt(...).sum() 12 μs 1 万次拆箱
stream().reduce(0, Integer::sum) 45 μs 2 万次装箱+拆箱
IntStream.range(0, n).sum() 9 μs 0

结论:Stream 处理基本类型集合时,永远先调 mapToInt / mapToLong / mapToDouble 转成原始 Stream——避免 Stream 的全程装箱。这条规则在热点代码上能带来 5 倍性能差距。

# 5. IEEE 754 浮点本质

# 5.1 二进制无法表示 0.1

回到 §1.1 的核心谜题——100.0 × 0.9 = 89.99999999999999。

疑惑:为什么计算机算不准 0.1?

论证——0.1 的二进制展开:

十进制 0.1 = 1/10

转二进制:0.1 × 2 = 0.2  → 0
        0.2 × 2 = 0.4  → 0
        0.4 × 2 = 0.8  → 0
        0.8 × 2 = 1.6  → 1
        0.6 × 2 = 1.2  → 1
        0.2 × 2 = 0.4  → 0      ← ★ 进入循环
        0.4 × 2 = 0.8  → 0
        ...

二进制 0.1 = 0.000110011001100110011...(无限循环)
1
2
3
4
5
6
7
8
9
10
11
12

根本原因:十进制中"有限小数"的概念,到二进制里可能变成"无限循环小数"——只有分母是 2 的幂的有理数(如 0.5、0.25、0.125)才能在二进制里精确表示。

double 用 64 位存储,尾数部分只有 52 位——超过 52 位的尾数被截断——0.1 在 double 里实际是:

0.1 ≈ 0.1000000000000000055511151231257827021181583404541015625
1

结论:double 算不准 0.1,不是 BUG,是数学必然。任何要求"十进制精确"的场景都不能用 double——这是 §6 BigDecimal 存在的理由。

# 5.2 float 与 double 内存布局

IEEE 754 把浮点数拆成 3 段:

double (64 位):
┌─┬───────────┬────────────────────────────────────────────────┐
│S│  Exponent │              Mantissa                          │
│1│   11 位   │                52 位                            │
└─┴───────────┴────────────────────────────────────────────────┘
 符号位      指数(偏移127)           尾数(隐含 1.xxx)

值 = (-1)^S × 1.Mantissa × 2^(Exponent - 1023)

float (32 位):
┌─┬─────────┬───────────────────────────┐
│S│Exponent │       Mantissa             │
│1│  8 位    │        23 位              │
└─┴─────────┴───────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14

精度推导:

  • float 23 位尾数 + 1 位隐含 = 24 位——log10(2^24) ≈ 7.2 位十进制
  • double 52 位尾数 + 1 位隐含 = 53 位——log10(2^53) ≈ 15.9 位十进制

关键陷阱:

float f = 1234567.89f;
System.out.println(f);          // 输出:1234567.9 ← 精度丢失
                                //(float 只能精确 7 位)

double d = 1234567890123.456;
System.out.println(d);          // 输出:1.234567890123456E12 ← 还能撑住
                                //(double 精确 15 位)
1
2
3
4
5
6
7

结论:金额、汇率、利率这种"超过 7 位有效数字"的场景,永远不要用 float——精度直接崩盘。

# 5.3 NaN 与 Infinity

IEEE 754 定义了三种"非数字状态":

double posInf = 1.0 / 0.0;        // +Infinity
double negInf = -1.0 / 0.0;       // -Infinity
double nan    = 0.0 / 0.0;        // NaN

System.out.println(posInf);        // Infinity
System.out.println(negInf);        // -Infinity
System.out.println(nan);           // NaN

// 反直觉:NaN != NaN !
System.out.println(nan == nan);    // ★ false !
System.out.println(Double.isNaN(nan));   // ★ 必须用 isNaN 判断
1
2
3
4
5
6
7
8
9
10
11

疑惑:NaN 为什么不等于自己?

论证:IEEE 754 规定 NaN 是"非数"——它代表"未定义的运算结果"(如 0/0、负数开方、无穷-无穷)。两个"未定义"显然不能相等——这是数学定义,不是 BUG。

结论:判断浮点是否是 NaN 必须用 Double.isNaN()——x == x 在 NaN 上永远 false。这是一个不会"错"的运行时陷阱——因为它不抛异常。

# 5.4 浮点比较为什么不能用 ==

直接看反例:

double a = 0.1 + 0.2;
double b = 0.3;
System.out.println(a == b);        // ★ false !
System.out.println(a);             // 0.30000000000000004
System.out.println(b);             // 0.3
1
2
3
4
5

正确比较方式——指定容差 epsilon:

public static boolean doubleEquals(double a, double b) {
    return Math.abs(a - b) < 1e-9;     // 容差 10^-9
}

// 或者用 BigDecimal 比较
BigDecimal ba = BigDecimal.valueOf(a);
BigDecimal bb = BigDecimal.valueOf(b);
System.out.println(ba.compareTo(bb) == 0);
1
2
3
4
5
6
7
8

注意:BigDecimal.equals 严格比较 scale——new BigDecimal("1.0").equals(new BigDecimal("1.00")) 是 false。要比较数值必须用 compareTo() == 0——这是 §6.1 的核心知识。

结论:浮点比较的三条铁律:

  1. 永远不用 ==
  2. 容差比较 + 阈值
  3. 高精度要求换 BigDecimal

# 6. BigDecimal 精度

# 6.1 内部 unscaledValue 与 scale

BigDecimal 的内部表示极其优雅——用整数 + 小数位数表达任意小数:

public class BigDecimal extends Number implements Comparable<BigDecimal> {
    private final BigInteger intVal;     // 无符号大整数(unscaledValue)
    private final int scale;             // 小数位数
    
    // 数值 = intVal × 10^(-scale)
    // 例:123.45 = unscaledValue=12345, scale=2
}
1
2
3
4
5
6
7

示例:

数值 unscaledValue scale
123.45 12345 2
100 100 0
100.00 10000 2
0.001 1 3
1.23E5 123 -3

关键陷阱:new BigDecimal("100").equals(new BigDecimal("100.00")) 是 false——scale 不同被视为不等。

BigDecimal a = new BigDecimal("100");
BigDecimal b = new BigDecimal("100.00");
System.out.println(a.equals(b));         // ★ false(scale 不同)
System.out.println(a.compareTo(b) == 0); // true(数值相等)
1
2
3
4

结论:BigDecimal 的判等永远用 compareTo 而非 equals——这是另一个静默杀手。

# 6.2 String 构造的必要性

回到 §1.1 案例最阴险的版本:

// ❌ 错误用法
BigDecimal a = new BigDecimal(0.1);
System.out.println(a);
// 输出:0.1000000000000000055511151231257827021181583404541015625
// ↑ 把 double 的精度缺陷"完整"复制进 BigDecimal

// ✅ 正确用法
BigDecimal b = new BigDecimal("0.1");
System.out.println(b);
// 输出:0.1(精确)

// ✅ 也可以
BigDecimal c = BigDecimal.valueOf(0.1);
// 内部相当于 new BigDecimal(Double.toString(0.1))
1
2
3
4
5
6
7
8
9
10
11
12
13
14

疑惑:为什么 new BigDecimal(double) 还存在?这不是反人类设计吗?

论证:JDK 文档明确警告——

The results of this constructor can be somewhat unpredictable. It might be assumed that writing new BigDecimal(0.1) creates a BigDecimal which is exactly equal to 0.1, but it is actually equal to 0.1000000000000000055511151231257827021181583404541015625.

JDK 保留这个构造器是为了"完整暴露 double 的真实值"——某些科学计算场景需要看 double 的精确十进制表示。但业务代码 99% 应该用 String 构造或 valueOf。

结论:金额场景的铁律——new BigDecimal(String) 或 BigDecimal.valueOf(...),永远不写 new BigDecimal(double)。

# 6.3 RoundingMode 八种选择

BigDecimal 的除法、setScale 必须指定舍入模式:

模式 含义 示例(保留 1 位)
UP 远离 0 进位 1.55 → 1.6, -1.55 → -1.6
DOWN 向 0 截断 1.55 → 1.5, -1.55 → -1.5
CEILING 向 +∞ 进位 1.55 → 1.6, -1.55 → -1.5
FLOOR 向 -∞ 进位 1.55 → 1.5, -1.55 → -1.6
HALF_UP 四舍五入 1.55 → 1.6
HALF_DOWN 五舍六入 1.55 → 1.5
HALF_EVEN ★ 银行家舍入 1.55 → 1.6, 1.45 → 1.4
UNNECESSARY 断言无需舍入(否则抛异常) —

HALF_EVEN(银行家舍入)的奥秘:当尾数恰好是 5 时,让结果的最后一位变成偶数——这种规则在大量数据上的累计偏差为 0,比四舍五入更"公平"。

BigDecimal a = new BigDecimal("1.5").setScale(0, RoundingMode.HALF_EVEN);   // 2(向偶数)
BigDecimal b = new BigDecimal("2.5").setScale(0, RoundingMode.HALF_EVEN);   // 2(向偶数)
BigDecimal c = new BigDecimal("3.5").setScale(0, RoundingMode.HALF_EVEN);   // 4(向偶数)
1
2
3

结论:金融行业默认 HALF_EVEN,零售业务默认 HALF_UP——这是行业惯例,不是技术规则。

# 6.4 除法必须指定精度

最容易踩的坑:

BigDecimal a = new BigDecimal("1");
BigDecimal b = new BigDecimal("3");
BigDecimal c = a.divide(b);              // ★ ArithmeticException!
// 信息:Non-terminating decimal expansion; no exact representable decimal result.
1
2
3
4

真相:1/3 = 0.333... 是无限小数——BigDecimal 无法精确表示——直接抛异常。

正确写法:

BigDecimal c = a.divide(b, 4, RoundingMode.HALF_UP);
// 0.3333(保留 4 位小数)
1
2

结论:BigDecimal 的除法永远要指定 scale 和 RoundingMode——这是除法和加减乘的最大差异。

# 7. 数值溢出与边界

# 7.1 整型溢出无声无息

整数溢出是 Java 中最危险的 BUG——不抛异常,不警告,静默错误:

int a = Integer.MAX_VALUE;       // 2147483647
int b = a + 1;                   // ★ -2147483648(变成负数)
System.out.println(b);

// 真实事故
int totalSeconds = days * 24 * 60 * 60;     // days=30000 时溢出
                                            // 实际值变成负数——后续计算全错
1
2
3
4
5
6
7

字节码视角:iadd 指令是 32 位补码加法——溢出时高位被丢弃,符号位翻转。JVM 不会检查溢出——这是为了硬件级性能。

真实历史:1996 年阿丽亚娜 5 号火箭发射失败——把 64 位浮点强转 16 位整数时溢出——损失 3.7 亿美元。整数溢出是航天级的杀手。

# 7.2 Math.addExact 防御

JDK 8 引入 Math.addExact / subtractExact / multiplyExact / negateExact ——溢出时抛 ArithmeticException:

try {
    int sum = Math.addExact(Integer.MAX_VALUE, 1);
} catch (ArithmeticException e) {
    System.out.println("溢出了:" + e.getMessage());
    // 输出:溢出了:integer overflow
}
1
2
3
4
5
6

底层实现:

public static int addExact(int x, int y) {
    int r = x + y;
    if (((x ^ r) & (y ^ r)) < 0) {        // ★ 异或检查符号位
        throw new ArithmeticException("integer overflow");
    }
    return r;
}
1
2
3
4
5
6
7

疑惑:((x ^ r) & (y ^ r)) < 0 这个检查为什么有效?

论证:

  • 同号相加溢出时,结果与原操作数符号相反
  • (x ^ r) < 0 表示 x 与结果异号
  • 当且仅当 x 与 r 异号 且 y 与 r 异号 时——溢出发生
  • & < 0 检查"两个异号关系都成立"——三行代码精准捕获溢出

结论:金融、计费、统计场景永远用 Math.addExact 替代 +——这是防御性编程的铁律。

# 7.3 long 转 int 截断

long big = 10_000_000_000L;       // 100 亿,超过 int 范围
int small = (int) big;            // ★ 1410065408(被截断)
System.out.println(small);

// 安全转换
try {
    int safe = Math.toIntExact(big);
} catch (ArithmeticException e) {
    System.out.println("无法转 int:" + big);
}
1
2
3
4
5
6
7
8
9
10

结论:任何"宽 → 窄"的强转都要用 Math.toIntExact / toShortExact——避免静默截断。

# 7.4 BigInteger 无界算术

需要超过 long 的整数运算时,BigInteger 是唯一选择:

BigInteger a = new BigInteger("99999999999999999999999999999999");
BigInteger b = new BigInteger("88888888888888888888888888888888");
BigInteger c = a.multiply(b);
System.out.println(c);
// 输出 60+ 位的精确大整数

// 阶乘 100!(普通 long 早就溢出了)
BigInteger factorial = BigInteger.ONE;
for (int i = 1; i <= 100; i++) {
    factorial = factorial.multiply(BigInteger.valueOf(i));
}
System.out.println(factorial);
// 输出 158 位大数
1
2
3
4
5
6
7
8
9
10
11
12
13

性能代价:BigInteger 加法 O(n)、乘法 O(n²) 或 O(n log n)——比 long 慢 50~200 倍。

结论:BigInteger 是"算法竞赛 + 加密 + 极端精度"的工具,业务代码用 long 几乎都够。

# 8. 性能与选型

# 8.1 装箱开销实测

JMH 微基准——10⁷ 次加法操作:

@Benchmark
public int sumPrimitive() {
    int sum = 0;
    for (int i = 0; i < 10_000_000; i++) sum += i;
    return sum;
}

@Benchmark
public Integer sumBoxed() {
    Integer sum = 0;
    for (int i = 0; i < 10_000_000; i++) sum += i;
    return sum;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
方案 耗时 内存分配
int sum 6 ms 0
Integer sum 95 ms ~120 MB(每次装箱新建 Integer)

性能比 16 倍,GC 压力天差地别——这就是装箱的真实代价。

结论:循环计数器、累加变量、热点路径上永远用基本类型——只有进集合(List)或 null 语义需要时才用包装类。

# 8.2 BigDecimal 性能代价

同样的 10⁷ 次加法:

@Benchmark
public BigDecimal sumBigDecimal() {
    BigDecimal sum = BigDecimal.ZERO;
    for (int i = 0; i < 10_000_000; i++) sum = sum.add(BigDecimal.valueOf(i));
    return sum;
}
1
2
3
4
5
6
方案 耗时 相对性能
int + 6 ms 1x
Integer +(装箱) 95 ms 16x
double + 8 ms 1.3x
BigDecimal.add 2800 ms 466x

结论:BigDecimal 比 double 慢约 350 倍——金额场景这是必要代价,但绝不要在中间计算用 BigDecimal——只在最终展示时转 BigDecimal 格式化。

# 8.3 三套金额方案对比

回到 §1 的金额计算事故——三种工程方案对比:

方案 精度 性能 内存 适用场景
double ❌ 不准(§5) ⭐⭐⭐⭐⭐ 8B/数 ❌ 永远不要用于金额
BigDecimal ✅ 精确 ⭐ 80~120B/对象 ✅ 业务展示 + 复杂计算
long(分) ✅ 精确 ⭐⭐⭐⭐⭐ 8B/数 ✅ 高频核心计算(如撮合引擎)

长 = 分 方案的最佳实践:

// 内部统一存"分",展示时转"元"
public class Money {
    private final long cents;            // 内部全部用分(long 精确)
    
    public Money(BigDecimal yuan) {
        this.cents = yuan.movePointRight(2).longValueExact();
    }
    
    public BigDecimal toYuan() {
        return BigDecimal.valueOf(cents).movePointLeft(2);
    }
    
    public Money plus(Money other) {
        return new Money(this.cents + other.cents);   // ← 纯 long 加法
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

优势:

  • 加减乘除全是 long 原生运算——速度顶级
  • long 范围 ±922 亿亿分 ≈ ±9.2×10¹⁶ 元——绝对够用
  • 序列化、网络传输只需 8 字节

结论:高频交易、撮合引擎、清算系统首选 long(分);ERP、财务报表、税务计算首选 BigDecimal——按"性能 vs 易用性"权衡。

# 9. 现代特性演进

# 9.1 Valhalla 值类型

OpenJDK Valhalla 项目——Project Valhalla 计划引入"值类型"(Value Class):

// 未来语法(Valhalla 预览)
value class Point {
    int x, y;
}

Point p = new Point(1, 2);
// JVM 内部当成基本类型处理——无对象头、无指针、可栈上分配
// 但语义上仍然是对象(有方法、可继承接口)
1
2
3
4
5
6
7
8

核心改变:

  • 消除装箱代价——List<Integer> 性能可与 int[] 相当
  • 消除指针开销——大型数据数组遍历快 10 倍以上
  • 保留对象语义——可定义方法、实现接口、用泛型

进度:JEP 401(Value Classes and Objects)—— JDK 24+ 预览,2026 年正式 GA。

结论:Valhalla 是 Java 性能未来 5 年最重要的演进——一旦 GA,包装类的性能负担将彻底消除。

# 9.2 数字字面量增强

JDK 7 引入的两个语法糖:

// 1. 数字下划线分隔——提升可读性
long bigNumber = 1_000_000_000L;     // 10 亿
int hex      = 0xFF_FF_FF_FF;        // 32 位掩码
double pi    = 3.14_159_26;          // 圆周率

// 2. 二进制字面量
int flags    = 0b1010_1100;          // 位标志
byte mask    = (byte) 0b1111_0000;
1
2
3
4
5
6
7
8

结论:现代 Java 数字字面量必加下划线——1000000 容易看错,1_000_000 一眼万年。

# 9.3 Math 与 StrictMath

Java 提供两套数学库:

类 实现 跨平台一致 性能
Math 平台相关(CPU 指令优化) ❌ 可能不同 快 2~5 倍
StrictMath 算法严格规定(fdlibm) ✅ 完全一致 慢但精确

疑惑:为什么需要"不一致"的 Math?

论证:CPU 厂商对 sin/cos/sqrt 等都有硬件加速指令(如 x86 的 FSQRT),但不同 CPU 的硬件实现尾位精度可能差异——StrictMath 强制软件实现,保证不同机器上结果完全相同(金融对账场景需要)。

结论:业务代码默认 Math;跨机器对账、回测、加密签名必用 StrictMath。

# 10. 综合案例串讲

# 10.1 一分钱真相揭晓

回到第 1 章的两起事故,逐条揭晓:

① IEEE 754 为什么算不准 0.1:0.1 在二进制中是无限循环小数 0.0001100110011...,double 只有 52 位尾数能容纳——超过部分被截断——导致 100 × 0.9 = 89.99999999999999(§5.1)。这不是 BUG,是数学必然——任何分母不是 2 的幂的有理数都无法二进制精确表示。

② BigDecimal(double) vs BigDecimal(String):new BigDecimal(0.1) 把 double 的全部精度缺陷(0.1000000000000000055511151...)原样复制进 BigDecimal——BigDecimal 精确,但起点已经错了。new BigDecimal("0.1") 直接从字符串解析——精确就是精确(§6.2)。金额场景永远 String 构造或 valueOf。

③ Integer -128~127 缓存边界:byte 的天然边界 + 90% 整数运算落在小整数 + 4KB 内存预算——三个工程权衡共同确定(§3.1)。下界 -128 不可调,上界 127 可通过 -XX:AutoBoxCacheMax 扩大。

④ 自动装箱字节码:装箱 = invokestatic Integer.valueOf(int),拆箱 = invokevirtual Integer.intValue()(§4.1)。拆箱在 null 引用上调用 intValue → NPE——这就是 §4.2 中 Map.get + int = 的崩溃链路。三元表达式两端类型不匹配会触发隐式拆箱(§4.3)——这是 NPE 的隐藏制造工厂。

⑤ 整型溢出沉默杀手:int + 1 在 MAX_VALUE 处变成 MIN_VALUE,JVM 不抛异常(§7.1)——阿丽亚娜 5 号火箭、Y2K 问题都是溢出酿成的灾难。用 Math.addExact 替代 +——三行异或检查精准捕获溢出(§7.2)。

⑥ 金额场景三套方案:double 永远不可(精度差);BigDecimal 精确但慢 350 倍——适合 ERP/财报;long(分) 精确且最快——适合撮合引擎/清算(§8.3)。原则:性能 vs 易用性二选一。

⑦ 浮点比较:== 在 NaN 上是 false(§5.3)、在精度损失场景错误(§5.4)——必须用容差比较或 BigDecimal.compareTo == 0。BigDecimal.equals 比 scale,compareTo 比数值——这又是另一个静默坑(§6.1)。

§1.1 一分钱事故复盘:开发用 double 算折扣,每次乘法损失 1e-16 精度——5000 笔订单累加后偏差 873 元。修复方案:金额内部全部 long(分),对外展示用 BigDecimal 格式化——零精度损失,性能不降反升。

# 10.2 一笔金额的一生

把 订单 100 元 × 9 折 这个最常见的业务计算串成一棵树,回扣前 22 篇:

T 0     用户下单:原价 100.00 元
        [05篇] String "100.00" 通过 RPC 进入服务端
        
T+1ms   解析为 BigDecimal
        BigDecimal price = new BigDecimal("100.00");
        ★ 必须 String 构造(§6.2)
        
T+2ms   折扣率读取
        BigDecimal rate = new BigDecimal("0.9");
        
T+3ms   计算实付金额
        BigDecimal pay = price.multiply(rate)
                              .setScale(2, RoundingMode.HALF_EVEN);
        ★ 银行家舍入(§6.3)
        ★ 内部 BigInteger.multiply(§7.4)—— O(n²)
        
T+5ms   转 long(分) 进入核心账本
        long cents = pay.movePointRight(2).longValueExact();
        // 9000 分(即 90.00 元)
        ★ 内部计算全部 long——加减极快(§8.3)
        
T+6ms   累加到日总账
        totalCents = Math.addExact(totalCents, cents);   ← 防溢出(§7.2)
        
T+10ms  入库 MySQL DECIMAL(18,2)
        BigDecimal yuanForDB = BigDecimal.valueOf(cents).movePointLeft(2);
        ps.setBigDecimal(1, yuanForDB);
        ★ JDBC DECIMAL 类型自动映射 BigDecimal
        
T+15ms  返回前端
        String json = "\"payAmount\": \"90.00\"";
        ★ 用 String 传输——避免 JSON 数字精度丢失
        
GC 视角:
        [01篇] BigDecimal 是中等寿命对象,频繁创建撑大年轻代
        ★ 高频路径用 long——避免 GC 压力
        
JIT 视角:
        [14篇] long 加减是 C2 最爱——会被向量化成 SIMD
        BigDecimal.add 内部分支多——只能保守编译
        
并发视角:
        [09篇] long 是 64 位——读写非原子(除非 volatile)
        总账类用 LongAdder 替代——避免 cache line 争抢
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

这条时间线串起本篇 80% 知识点——一笔订单背后是 IEEE 754、BigDecimal 内部结构、long 溢出防护、HALF_EVEN 舍入、装箱代价、JIT 优化的连锁反应。

# 10.3 设计哲学回扣

跳出技术细节,提炼三条贯穿数字类型设计的工程哲学:

  1. 抽象有代价:基本类型快但无 null 语义、无对象方法;包装类型有完整对象语义但慢 16 倍;BigDecimal 精确但慢 350 倍——每一层抽象都在拿性能换功能。这条原则与卷一第 14 篇"JIT 内联是把抽象代价摊平的最后救赎"一脉相承——抽象不是免费的,工程师的本职是看穿成本。

  2. 不变性是精度的护城河:BigDecimal、BigInteger、所有包装类——全部不可变。任何运算返回新对象,永不修改原值。这与第 5 篇 String、第 22 篇 LinkedHashMap 都同源——多线程安全、可缓存、可预测。金融系统、加密系统、配置系统的核心数据全部不可变——这是工业级系统的安全底线。

  3. 正确性优先于性能:金额场景宁可慢 350 倍(BigDecimal)也要精确——一分钱事故能毁掉一家支付公司。整型溢出宁可抛异常(addExact)也要捕获——沉默的错误比明显的崩溃更危险。这条原则贯穿所有金融、医疗、航天、加密系统——Bug 一旦写下,错误会以光速传染,性能可以以毫秒计时挽回。

# 10.4 数字类型速查表

最后一张表——任何数字场景 5 秒决策:

场景 推荐类型 不推荐 理由
循环计数器 int / long Integer 装箱代价 16 倍
数组索引 int Integer 同上
集合 Key/Value Integer / Long 不得不 集合不能存基本类型
金额(核心) long(分) double 精度 + 性能双赢
金额(展示) BigDecimal double 精度第一
利率/汇率 BigDecimal double 精度第一
物理计算 double float 精度更高
图形/游戏坐标 float double 节省内存
大整数(加密、组合数) BigInteger long 无溢出
ID 生成 long int 21 亿不够用
位运算掩码 int / long byte/short 自动提升为 int

数字类型铁律 7 条:

1. 金额永远不用 double——用 long(分) 或 BigDecimal
2. BigDecimal 永远 String 构造,永远 compareTo 比数值
3. 包装类永远 equals 比较,永远不用 ==
4. 浮点永远不用 == 比较,永远用容差或 BigDecimal
5. 整数运算用 Math.addExact 防溢出(金融/计费场景)
6. 循环和热点路径永远用基本类型,绝不装箱
7. 现代 Java 用 1_000_000 写法,永远加下划线
1
2
3
4
5
6
7

至此第 23 篇完成——我们用 1.6 万字把 IEEE 754 浮点本质、Integer 缓存池、自动装箱字节码、BigDecimal 精度控制、整型溢出防御、long(分) 金额方案讲透。卷二容器与基础数据结构还有最后一篇——Object 通用方法。下一篇我们顺着"hashCode 和 equals 为什么必须一致"这条线,进入 第 24 篇:Object通用方法的契约——把 hashCode/equals 一致性、toString/clone/finalize 的废与立、wait/notify 与监视器机制一次讲透。

上次更新: 2026/06/10, 11:13:41
LinkedHashMap与LRU实现
Object通用方法的契约

← LinkedHashMap与LRU实现 Object通用方法的契约→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式