⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 网络 「网络」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

面试考察点

这道题考的不是”哪个好”,而是你会不会根据场景做权衡:

  1. 金融精度意识 – 知不知道 double 表示金额是灾难级操作,以及为什么。
  2. 数据类型底层特性 – BigDecimal 的任意精度怎么实现的,Long 表示金额的”定点数”思路是什么。
  3. 实战选型能力 – 不同业务场景(电商 vs 量化交易 vs 内部结算)该怎么选,能不能说出具体的判断依据。

核心答案

默认选 BigDecimal,除非你有充分理由不选。

BigDecimal 能精确表示任意精度的十进制小数,从根本上杜绝浮点数精度丢失。大多数商业系统用它就对了。

Long(以”分”为单位的定点数方案)在高频交易、内部结算、性能瓶颈已证实的场景下是合理选择。但前提是:团队能在整条数据链路上严格维护单位转换,否则一个”元/分”混用的 bug 能让你加班到天亮。

深度解析

原理/机制

BigDecimal 怎么做到精确? 内部用一个 BigInteger(非标度值)加一个 int(标度)表示,值 = unscaledValue × 10^(-scale)。这是十进制表示法,0.1 就是精确的 0.1,不会像 double 那样变成 0.1000000000000000055511...。而且舍入行为完全由你控制(RoundingMode)。

Long 怎么表示金额? 本质是”定点数”——把 123.45 元 存成 12345(单位:分)。所有计算都在整数上进行,只在展示给用户时除以 100 转回”元”。CPU 算整数比算 BigDecimal 快得多,但你得自己维护单位转换。

代码示例

import java.math.BigDecimal;
import java.math.RoundingMode;

public class MoneyDemo {
public static void main(String[] args) {
// ========== 方案一:BigDecimal(推荐) ==========
BigDecimal price = new BigDecimal("19.99"); // 务必用 String 构造器!
BigDecimal qty = new BigDecimal("3");
BigDecimal total = price.multiply(qty); // 59.97,精确无误

// 涉及除法必须指定舍入模式,否则除不尽直接抛 ArithmeticException
BigDecimal discounted = total.divide(new BigDecimal("3"), 2, RoundingMode.HALF_UP);
System.out.println("折后价: " + discounted); // 19.99

// ❌ 反面教材:用 double 构造器
BigDecimal wrong = new BigDecimal(0.1); // 实际值:0.1000000000000000055511...
BigDecimal right = new BigDecimal("0.1"); // 实际值:0.1
System.out.println("double构造: " + wrong);
System.out.println("String构造: " + right);

// ========== 方案二:Long(以"分"为单位) ==========
long priceInCents = 1999L; // 19.99 元
long qtyLong = 3L;
long totalInCents = priceInCents * qtyLong; // 5997,代表 59.97 元

// 展示时转"元"——用 BigDecimal 做除法,别用 double!
String display = BigDecimal.valueOf(totalInCents)
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)
.toPlainString();
System.out.println("总价: " + display + " 元"); // 59.97 元
}
}

对比分析与最佳实践

特性维度 BigDecimal Long (以分为单位)
精度 完美保证,任意精度十进制计算。 可保证,但仅限于最小单位以内。无法直接表示 0.001 元(如厘)。
性能 相对较慢,对象创建、方法调用开销大。 极快,利用 CPU 原生整数运算,内存连续,缓存友好。
内存占用 高,每个对象包含 BigInteger、int 等字段。 极低,8字节(若使用基本类型 long)。
代码复杂度 高,API 复杂,需注意构造方式、舍入模式和对象不可变性。 低,纯算术运算,但需在业务层维护单位转换,容易出错(如忘记转换)。
适用场景 通用场景:涉及复杂小数运算(如利率、税率)、多币种、审计要求高、与外部系统(如银行)交互。 特定场景:内部结算、高频交易(如秒杀)、性能瓶颈明确、且金额单位固定且无需更小小数的场景。

怎么选?看场景

  1. 默认选 BigDecimal:绝大多数业务系统直接用它。务必用 String 构造器(new BigDecimal("19.99")),明确设置 RoundingMode。

  2. 考虑 Long 需要同时满足:单一货币、不涉及”分”以下的小数、性能瓶颈已实测证实、团队能在 DB/缓存/RPC/前端全链路严格维护单位转换。四个条件缺一不可。

  3. 高级玩法:设计一个 Money 值对象,内部封装 BigDecimal 或 Long,对外暴露类型安全的 API。未来需要切换底层实现时,业务代码零改动。

常见误区

  • 用 Double/Float 表示金额 – 绝对禁忌。0.1 + 0.2 != 0.3 在金融场景是灾难。这不是”一分钱”的问题,是审计不过的问题。
  • “BigDecimal 太慢所以不用” – 除非你能拿出压测数据证明 BigDecimal 是瓶颈,否则这就是过早优化。大多数业务系统根本感知不到差异。
  • Long 方案”元/分”混用 – 用 Long 方案最致命的坑。DAO 层存”分”,Controller 层传”元”,某天有人忘了转换,用户的 1 块钱变成了 100 块。规矩只有一条:内部全用”分”,只在最终展示时转”元”。
文章目录
  1. 1. 面试考察点
  2. 2. 核心答案
  3. 3. 深度解析
    1. 3.1. 原理/机制
    2. 3.2. 代码示例
    3. 3.3. 对比分析与最佳实践
    4. 3.4. 怎么选?看场景
    5. 3.5. 常见误区