时间和金额的 bug 往往能编译、能运行,甚至多数时候都正确,只在夏令时、跨时区或小数舍入时暴露。解决方法不是记住 更多 API,而是先明确业务语义。
Table of contents
Open Table of contents
时间类型表达不同问题
| 类型 | 表达什么 | 示例 |
|---|---|---|
Instant | UTC 时间线上的一个瞬间 | 事件发生时间 |
LocalDate | 不带时区的日历日期 | 生日、账期日 |
LocalDateTime | 不带时区的日期时间 | 尚未确定地区的表单输入 |
ZonedDateTime | 日期时间和地区时区规则 | 上海门店 9:00 开门 |
Duration | 基于秒的时间长度 | 请求超时 |
var occurredAt = java.time.Instant.now();
var shanghai = java.time.ZoneId.of("Asia/Shanghai");
var displayed = occurredAt.atZone(shanghai);
System.out.println(displayed);
数据库保存事件时间通常使用 UTC Instant,显示时再转换用户时区。业务规则若是“每个地区当地午夜结算”,则必须保存
地区时区,而不只是固定的 +08:00 偏移。
金额不要使用 double
二进制浮点不能精确表示多数十进制小数。金额使用 BigDecimal,并从字符串构造:
var unitPrice = new java.math.BigDecimal("19.90");
var total = unitPrice.multiply(java.math.BigDecimal.valueOf(3));
var average = total.divide(java.math.BigDecimal.valueOf(7), 2,
java.math.RoundingMode.HALF_UP);
new BigDecimal(0.1) 会把 double 已有误差带进来。除法可能产生无限小数,必须指定小数位和舍入模式。舍入规则是
业务规则,财务、税务和展示可能不同,不能散落在代码里。
equals 同时比较数值和 Scale,所以 1.0 与 1.00 不相等;只比较数值时使用 compareTo(...) == 0。
JSON 是跨服务契约
**序列化(Serialization)**把内存对象转换成可传输格式,反序列化则反向转换。不要直接把数据库实体作为外部 JSON: 内部字段重命名或关联变化会意外破坏客户端。
{
"orderId": "O-1001",
"occurredAt": "2026-09-01T06:00:00Z",
"amount": "59.70",
"currency": "CNY"
}
时间使用 ISO 8601 且包含 Z 或时区;金额可使用字符串避免不同语言数字精度差异;货币必须单独携带。新增字段通常可
向后兼容,删除、重命名或改变字段含义需要版本策略。
测试边界
至少测试月末、闰日、夏令时切换地区、负数、除不尽金额以及 JSON 未知字段。时间测试注入 Clock,不要在业务方法中
到处直接调用 Instant.now(),否则测试会依赖真实时间。
下一步
阅读Maven、Gradle 与依赖冲突,让构建输入也可复现。