乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

大数值运算 详解

本章定位:深入掌握Java中任意精度数值运算的两大核心类——BigInteger和BigDecimal。飞翔科技的Frank负责财务系统开发,每一分钱都必须精确到小数点后两位,浮点运算的精度丢失在金融领域是不可接受的。本章从浮点类型的精度缺陷出发,系统讲解BigDecimal的创建、舍入模式、标度与精度、比较陷阱,以及BigInteger的大数运算,帮助读者写出经得起财务审计的精确计算代码。


定义与语法

属性说明
java.math.BigInteger不可变的任意精度整数,可表示任意大的整数(无溢出风险)
java.math.BigDecimal不可变的任意精度有符号十进制数,用于精确的货币计算
精度(Precision)数值中有效数字的位数,如 123.45 的精度为5
标度(Scale)小数点右边的位数,如 123.45 的标度为2
RoundingMode舍入模式枚举,决定除法等操作中如何处理无法精确表示的结果
compareTo vs equalsBigDecimal中两者不等价:equals比较值和标度,compareTo仅比较数值大小
MathContext封装精度和舍入模式的不可变对象,用于控制运算结果的精度
double精度缺陷IEEE 754浮点数无法精确表示大多数十进制小数,如0.1在二进制中是无限循环小数
BigDecimal(double) 陷阱使用double构造器会"继承"double的精度问题,应使用字符串构造器

核心结论:涉及金额、税率、利率等精确计算时,绝对不能用float/double,必须使用BigDecimal。BigDecimal的核心优势不是"更大的数",而是"任意精度的精确十进制表示"。


完整代码示例

示例一:Frank揭露浮点数精度陷阱——0.1 + 0.2 ≠ 0.3

Frank在给飞翔科技新人培训时,用最经典的浮点数精度问题开场,让白歌和小崔直观感受double的不可靠。

/** 飞翔科技 - 浮点数精度陷阱演示:为什么财务计算不能用float/double */
import java.math.BigDecimal;

public class FloatingPrecisionDemo {
    public static void main(String[] args) {
        System.out.println("========== 飞翔科技财务系统精度验证 ==========\n");

        // 经典案例:0.1 + 0.2
        double d1 = 0.1;
        double d2 = 0.2;
        double dSum = d1 + d2;
        System.out.println("【double 计算】0.1 + 0.2 = " + dSum);
        System.out.println("  预期:0.3");
        System.out.println("  实际:" + dSum + "  ← 出现了精度误差!");

        // 更真实的财务场景
        double price = 188.88;  // 飞翔科技产品单价
        double taxRate = 0.13;   // 增值税率 13%
        double taxDouble = price * taxRate;
        System.out.println("\n【double 税率计算】");
        System.out.println("  单价 " + price + " × 税率 " + taxRate + " = " + taxDouble);
        System.out.println("  期望精确值:24.5544  ← double可能产生微小误差");

        // BigDecimal的正确做法
        BigDecimal priceBD = new BigDecimal("188.88");
        BigDecimal taxRateBD = new BigDecimal("0.13");
        BigDecimal taxBD = priceBD.multiply(taxRateBD);
        System.out.println("\n【BigDecimal 税率计算】");
        System.out.println("  单价 " + priceBD + " × 税率 " + taxRateBD + " = " + taxBD);
        System.out.println("  >>> 精确无比!");

        // 贬低double构造器
        BigDecimal badBD = new BigDecimal(0.1);
        System.out.println("\n【BigDecimal(double) 陷阱】");
        System.out.println("  new BigDecimal(0.1) = " + badBD);
        System.out.println("  >>> 构造器继承了double的精度误差!");
        System.out.println("  >>> 正确写法:new BigDecimal(\"0.1\")");
    }
}
========== 飞翔科技财务系统精度验证 ==========

【double 计算】0.1 + 0.2 = 0.30000000000000004
  预期:0.3
  实际:0.30000000000000004  ← 出现了精度误差!

【double 税率计算】
  单价 188.88 × 税率 0.13 = 24.554400000000003
  期望精确值:24.5544  ← double可能产生微小误差

【BigDecimal 税率计算】
  单价 188.88 × 税率 0.13 = 24.5544
  >>> 精确无比!

【BigDecimal(double) 陷阱】
  new BigDecimal(0.1) = 0.1000000000000000055511151231257827021181583404541015625
  >>> 构造器继承了double的精度误差!
  >>> 正确写法:new BigDecimal("0.1")

场景解读:Frank指着屏幕对新人们说:"看到了吗?double的0.1不是真正的0.1,是它在二进制中最接近的近似值。在金融系统中,每一笔计算都要精确到分,这种误差积累起来就是灾难。"


示例二:朱璐在飞翔科技财务系统实现订单精确计算

朱璐负责飞翔科技订单模块,需要计算含税总额、折扣后金额,并保留两位小数(精确到分)。她运用BigDecimal的多种运算和舍入模式。

/** 飞翔科技 - 订单精确金额计算:含税、折扣、舍入 */
import java.math.BigDecimal;
import java.math.RoundingMode;

public class OrderCalculationDemo {
    public static void main(String[] args) {
        System.out.println("========== 飞翔科技订单结算 ==========\n");

        // 订单明细
        BigDecimal unitPrice    = new BigDecimal("188.88");    // 产品单价
        int           quantity  = 3;                           // 数量
        BigDecimal taxRate     = new BigDecimal("0.13");      // 增值税率 13%
        BigDecimal discountRate = new BigDecimal("0.15");     // 折扣率 15%

        // 1. 计算小计
        BigDecimal quantityBD = new BigDecimal(quantity);
        BigDecimal subtotal = unitPrice.multiply(quantityBD);
        System.out.println("【步骤1:小计】");
        System.out.println("  单价 " + unitPrice + " × 数量 " + quantity + " = " + subtotal);

        // 2. 计算税额(保留2位小数,HALF_UP四舍五入)
        BigDecimal tax = subtotal.multiply(taxRate)
                .setScale(2, RoundingMode.HALF_UP);
        System.out.println("\n【步骤2:税额】");
        System.out.println("  小计 " + subtotal + " × 税率 " + taxRate + " = " + subtotal.multiply(taxRate));
        System.out.println("  税额(保留2位,四舍五入)= " + tax);

        // 3. 计算含税总额
        BigDecimal totalWithTax = subtotal.add(tax);
        System.out.println("\n【步骤3:含税总额】");
        System.out.println("  小计 " + subtotal + " + 税额 " + tax + " = " + totalWithTax);

        // 4. 计算折扣
        BigDecimal discount = totalWithTax.multiply(discountRate)
                .setScale(2, RoundingMode.HALF_UP);
        System.out.println("\n【步骤4:折扣】");
        System.out.println("  含税总额 " + totalWithTax + " × 折扣率 " + discountRate + " = " + discount);

        // 5. 最终应付金额
        BigDecimal finalAmount = totalWithTax.subtract(discount);
        System.out.println("\n【步骤5:最终应付】");
        System.out.println("  含税总额 " + totalWithTax + " - 折扣 " + discount + " = " + finalAmount);

        // 6. 展示不同舍入模式
        BigDecimal value = new BigDecimal("188.885");
        System.out.println("\n【舍入模式对比】原始值:" + value);
        System.out.println("  HALF_UP(四舍五入)  → " + value.setScale(2, RoundingMode.HALF_UP));
        System.out.println("  HALF_DOWN(五舍六入) → " + value.setScale(2, RoundingMode.HALF_DOWN));
        System.out.println("  UP(向远离零方向)    → " + value.setScale(2, RoundingMode.UP));
        System.out.println("  DOWN(向零方向)      → " + value.setScale(2, RoundingMode.DOWN));
        System.out.println("  CEILING(天花板)     → " + value.setScale(2, RoundingMode.CEILING));
        System.out.println("  FLOOR(地板)         → " + value.setScale(2, RoundingMode.FLOOR));
    }
}
========== 飞翔科技订单结算 ==========

【步骤1:小计】
  单价 188.88 × 数量 3 = 566.64

【步骤2:税额】
  小计 566.64 × 税率 0.13 = 73.6632
  税额(保留2位,四舍五入)= 73.66

【步骤3:含税总额】
  小计 566.64 + 税额 73.66 = 640.30

【步骤4:折扣】
  含税总额 640.30 × 折扣率 0.15 = 96.05

【步骤5:最终应付】
  含税总额 640.30 - 折扣 96.05 = 544.26

【舍入模式对比】原始值:188.885
  HALF_UP(四舍五入)  → 188.89
  HALF_DOWN(五舍六入) → 188.88
  UP(向远离零方向)    → 188.89
  DOWN(向零方向)      → 188.88
  CEILING(天花板)     → 188.89
  FLOOR(地板)         → 188.88

示例三:孔蓝用BigInteger计算大数阶乘——飞翔科技密码学RSA密钥长度演示

孔蓝在飞翔科技安全部门需要向产品经理解释RSA密钥的安全性。她用BigInteger计算大数阶乘,展示天文数字级的大数运算能力。

/** 飞翔科技 - 使用BigInteger计算大数阶乘(演示任意精度整数运算) */
import java.math.BigInteger;

public class BigFactorialDemo {
    public static void main(String[] args) {
        System.out.println("========== 飞翔科技 大数阶乘计算(BigInteger) ==========\n");

        // 计算 1! 到 50!,展示BigInteger的增长
        for (int n = 1; n <= 50; n++) {
            BigInteger factorial = factorial(n);
            if (n <= 10 || n % 10 == 0) {
                System.out.printf("  %2d! = %s%n", n, formatBigInteger(factorial));
            }
        }

        // 单独展示更大的阶乘
        System.out.println("\n--- 超大阶乘 ---");
        BigInteger fact100 = factorial(100);
        System.out.println("  100! = " + fact100);
        System.out.println("  位数:" + fact100.toString().length() + " 位");

        // BigInteger的位运算(RSA密钥相关)
        BigInteger twoPow256 = BigInteger.ONE.shiftLeft(256);  // 2^256
        System.out.println("\n【RSA密钥空间对比】");
        System.out.println("  2^256  = " + twoPow256);
        System.out.println("  位数:  " + twoPow256.toString().length() + " 位(十进制)");
        System.out.println("  >>> AES-256的密钥空间,远超宇宙中原子总数(~10^80)");
    }

    /** 飞翔科技 - 使用BigInteger计算阶乘 */
    public static BigInteger factorial(int n) {
        BigInteger result = BigInteger.ONE;
        for (int i = 2; i <= n; i++) {
            result = result.multiply(BigInteger.valueOf(i));
        }
        return result;
    }

    /** 飞翔科技 - 格式化大整数输出(超过40字符时截断显示) */
    public static String formatBigInteger(BigInteger bi) {
        String s = bi.toString();
        if (s.length() <= 40) return s;
        return s.substring(0, 20) + "..." + s.substring(s.length() - 17)
                + " (" + s.length() + " 位)";
    }
}
========== 飞翔科技 大数阶乘计算(BigInteger) ==========

   1! = 1
   2! = 2
   3! = 6
   4! = 24
   5! = 120
   6! = 720
   7! = 5040
   8! = 40320
   9! = 362880
  10! = 3628800
  20! = 2432902008176640000
  30! = 26525285981219105863...00000000000000000 (33 位)
  40! = 81591528324789773434...00000000000000000 (48 位)
  50! = 30414093201713378043...00000000000000000 (65 位)

--- 超大阶乘 ---
  100! = 93326215443944152681...00000000000000000
  位数:158 位

【RSA密钥空间对比】
  2^256  = 115792089237316195423570985008687907853269984665640564039457584007913129639936
  位数:  78 位(十进制)
  >>> AES-256的密钥空间,远超宇宙中原子总数(~10^80)

示例四:黄俪处理利率除不尽问题——分期付款精确计算

黄俪在飞翔科技消费金融模块中,需要将总金额等额分期。除法除不尽是常态,舍入模式的选择直接影响每期金额和总差额。

/** 飞翔科技 - 分期付款计算:处理除法除不尽与舍入 */
import java.math.BigDecimal;
import java.math.RoundingMode;

public class InstallmentDemo {
    public static void main(String[] args) {
        System.out.println("========== 飞翔科技分期付款计算 ==========\n");

        BigDecimal totalAmount = new BigDecimal("18888.88");  // 总金额
        int periods = 12;                                      // 12期

        System.out.println("分期总额:" + totalAmount);
        System.out.println("期数:" + periods + " 期");

        // 方案A:每期等额(HALF_UP)
        BigDecimal perPeriodA = totalAmount.divide(
                new BigDecimal(periods), 2, RoundingMode.HALF_UP);
        BigDecimal totalA = perPeriodA.multiply(new BigDecimal(periods));
        BigDecimal diffA = totalAmount.subtract(totalA);

        System.out.println("\n【方案A:HALF_UP 等额分期】");
        System.out.println("  每期金额:" + perPeriodA);
        System.out.println("  12期合计:" + totalA);
        System.out.println("  差额(银行吸收):" + diffA);

        // 方案B:前11期等额,最后一期补差额
        BigDecimal perPeriodB = totalAmount.divide(
                new BigDecimal(periods), 2, RoundingMode.DOWN);
        BigDecimal total11 = perPeriodB.multiply(new BigDecimal(periods - 1));
        BigDecimal lastPeriod = totalAmount.subtract(total11);

        System.out.println("\n【方案B:前N-1期DOWN + 最后一期补差】");
        System.out.println("  前11期每期:" + perPeriodB);
        System.out.println("  最后1期:" + lastPeriod);
        System.out.println("  12期合计:" + total11.add(lastPeriod));

        // 方案C:展示除不尽的原始结果
        BigDecimal raw = totalAmount.divide(new BigDecimal(periods), 10, RoundingMode.HALF_UP);
        System.out.println("\n【原始除法结果(保留10位)】");
        System.out.println("  18888.88 ÷ 12 = " + raw);
        System.out.println("  无限循环小数:1574.07333...");
    }
}
========== 飞翔科技分期付款计算 ==========

分期总额:18888.88
期数:12 期

【方案A:HALF_UP 等额分期】
  每期金额:1574.07
  12期合计:18888.84
  差额(银行吸收):0.04

【方案B:前N-1期DOWN + 最后一期补差】
  前11期每期:1574.07
  最后1期:1574.11
  12期合计:18888.88

【原始除法结果(保留10位)】
  18888.88 ÷ 12 = 1574.0733333333
  无限循环小数:1574.07333...

深度原理分析

一、BigDecimal运算与舍入模式全景图

下图展示了BigDecimal的四种基本运算在不同舍入模式下的行为差异。核心矛盾在于:除法经常产生无限小数,必须用setScale或MathContext截断。

关键点:

  • 加法/减法/乘法:结果精确,标度自动确定。但乘积标度 = 标度之和,可能需要后续setScale。
  • 除法:必须指定标度和舍入模式,否则除不尽时抛ArithmeticException。
  • 舍入模式选择:金融系统默认用HALF_UP(银行家有时用HALF_EVEN以减少累积误差)。

二、compareTo vs equals 的本质区别

这是BigDecimal最隐蔽的陷阱之一:

  • equals:比较值和标度都相等才返回true。2.0和2.00标度不同,equals返回false。
  • compareTo:仅比较数值大小,忽略标度。2.0和2.00 compareTo返回0。
  • 最佳实践:将BigDecimal放入HashSet/HashMap时注意:2.0和2.00会被视为不同的key!建议先stripTrailingZeros()或统一标度后再放入散列集合。

三、BigDecimal的不可变性与链式调用

与String一样,BigDecimal是不可变的。所有运算都返回新对象,原对象不变。这保证了线程安全,但也意味着必须接收返回值:

BigDecimal a = new BigDecimal("100");
a.add(new BigDecimal("1"));    // ❌ 错误!返回值被丢弃,a仍然是100
a = a.add(new BigDecimal("1")); // ✅ 正确!必须赋值接收

四、性能考量

BigDecimal的精确性以性能为代价:

  • BigInteger/BigDecimal运算比基本类型慢数十倍到数百倍。
  • 对于不需要任意精度的场景(如科学计算),double是更好的选择。
  • 对于货币/金融计算,正确性 > 性能,必须使用BigDecimal。
  • 大量BigDecimal运算时,可复用MathContext对象减少开销。

易错场景

反例一:❌ 使用BigDecimal(double)构造器

// ❌ 致命错误!new BigDecimal(double) 继承了double的精度问题
BigDecimal bad1 = new BigDecimal(0.1);
System.out.println(bad1);
// 输出:0.1000000000000000055511151231257827021181583404541015625
// 这根本不是0.1!

BigDecimal bad2 = new BigDecimal(188.88);
// 同样存在不可预料的精度偏差
// ✅ 正确:始终使用字符串构造器
BigDecimal good1 = new BigDecimal("0.1");
System.out.println(good1);  // 输出:0.1  ← 精确!

// ✅ 或使用valueOf(内部也使用字符串转换,但有缓存)
BigDecimal good2 = BigDecimal.valueOf(188.88);  // 适用于double字面量
// 注意:BigDecimal.valueOf(0.1) 也能给出精确0.1

反例二:❌ 除法不指定标度和舍入模式

// ❌ 除不尽时抛 ArithmeticException
BigDecimal a = new BigDecimal("10");
BigDecimal b = new BigDecimal("3");
BigDecimal result = a.divide(b);  // ArithmeticException!
// Non-terminating decimal expansion; no exact representable decimal result.
// ✅ 正确:始终指定标度和舍入模式
BigDecimal result = a.divide(b, 2, RoundingMode.HALF_UP);  // 3.33

// ✅ 或使用MathContext
MathContext mc = new MathContext(4, RoundingMode.HALF_UP);
BigDecimal result2 = a.divide(b, mc);  // 3.333

反例三:❌ 将BigDecimal放入HashSet/HashMap时不统一标度

// ❌ 标度不同导致equals不等,被视为不同元素
Set<BigDecimal> prices = new HashSet<>();
prices.add(new BigDecimal("2.0"));
prices.add(new BigDecimal("2.00"));
System.out.println(prices.size());  // 输出:2 ← 意料之外!
// equals比较:2.0(scale=1) ≠ 2.00(scale=2)
// ✅ 方案一:统一标度后再放入散列集合
Set<BigDecimal> prices = new HashSet<>();
prices.add(new BigDecimal("2.0").setScale(2));  // 2.00
prices.add(new BigDecimal("2.00").setScale(2)); // 2.00
System.out.println(prices.size());  // 输出:1 ← 正确

// ✅ 方案二:使用TreeSet(基于compareTo,忽略标度)
Set<BigDecimal> pricesTree = new TreeSet<>();
pricesTree.add(new BigDecimal("2.0"));
pricesTree.add(new BigDecimal("2.00"));
System.out.println(pricesTree.size());  // 输出:1 ← 正确

反例四:❌ 混淆BigDecimal的compareTo和equals在条件判断中的使用

// ❌ 在TreeMap/TreeSet中看似没问题,但在业务逻辑中可能出错
BigDecimal price1 = new BigDecimal("188.88");
BigDecimal price2 = new BigDecimal("188.880");
if (price1.equals(price2)) {  // false!标度不同
    System.out.println("价格相等");
}
// ✅ 业务逻辑中判断数值相等:使用compareTo
if (price1.compareTo(price2) == 0) {  // true!数值相等
    System.out.println("价格相等");
}

// ✅ 最佳实践:创建时统一标度
BigDecimal price1 = new BigDecimal("188.88").setScale(2, RoundingMode.HALF_UP);
BigDecimal price2 = new BigDecimal("188.880").setScale(2, RoundingMode.HALF_UP);

面试考点

Q1: 为什么float/double不能用于精确的货币计算?BigDecimal如何解决?

答:float和double使用IEEE 754二进制浮点表示法,绝大多数十进制小数(如0.1、0.2)在二进制中是无限循环小数,必须截断存储,因此产生了舍入误差。例如,0.1在double中实际存储为0.10000000000000000555...。在货币计算中,这些微小误差经过多次运算会累积放大,导致金额偏差。

BigDecimal通过内部使用十进制表示(BigInteger存储未缩放值 + int存储标度),可以将0.1、188.88等十进制小数精确存储,运算结果经过标度控制后保持精确。它牺牲了性能换取了精度,是金融计算的唯一正确选择。

Q2: BigDecimal的equals和compareTo有什么区别?什么场景用哪个?

答:

  • equals:比较数值和标度都相等才返回true。new BigDecimal("2.0").equals(new BigDecimal("2.00")) 返回false(标度不同)。
  • compareTo:仅比较数值大小,忽略标度。new BigDecimal("2.0").compareTo(new BigDecimal("2.00")) 返回0。

使用场景:

  • HashSet/HashMap:自然使用equals/hashCode。但注意标度问题,建议统一标度后再放入。
  • TreeSet/TreeMap:使用compareTo,不受标度影响。
  • 业务判断(如金额比较):使用compareTo,因为188.88和188.880在业务上是等价的。
  • 排序:使用compareTo。

Q3: BigInteger和BigDecimal的区别是什么?各自适用场景?

答:

  • BigInteger:不可变的任意精度整数。没有小数部分,没有标度概念。适用于:大整数运算(阶乘、幂运算)、密码学(RSA大素数)、ID生成、溢出安全的计数。
  • BigDecimal:不可变的任意精度十进制数。有标度和小数部分。适用于:货币计算、利率计算、需要精确小数的科学计算。

注意:BigInteger也能表示任意大的数,但它不能表示小数。BigDecimal内部使用BigInteger存储未缩放值,外加一个int标度。

Q4: BigDecimal的舍入模式有哪些?金融系统一般用哪种?为什么?

答:JDK 8定义了8种舍入模式,最常用的6种:

  1. HALF_UP:四舍五入(最常用)。>=0.5入,<0.5舍。
  2. HALF_DOWN:五舍六入。>0.5入,<=0.5舍。
  3. HALF_EVEN:银行家舍入。向最近的偶数舍入,可减少大量运算中的累积误差。
  4. UP:向远离零方向舍入。正数取大,负数取小(绝对值变大)。
  5. DOWN:向零方向截断。直接丢弃多余位。
  6. CEILING/FLOOR:向正无穷/负无穷方向舍入。

金融系统:普通业务通常用HALF_UP(四舍五入,直观且符合日常习惯)。对累积误差敏感的统计/精算场景可能用HALF_EVEN(银行家舍入)。关键在于全系统统一舍入模式,避免A模块用HALF_UP、B模块用HALF_DOWN导致数据不一致。

Frank总结道:"记住了——金额计算三原则:第一,永远用BigDecimal;第二,永远用字符串构造器;第三,除法永远指定标度和舍入模式。违反任何一条,迟早都会有财务对不上账的那一天。"

上一页
Random:伪随机数生成器
下一页
日期时间API 详解