跳到正文
Elaine Blog
返回

时间、金额、精度与序列化

Java 基础

时间和金额的 bug 往往能编译、能运行,甚至多数时候都正确,只在夏令时、跨时区或小数舍入时暴露。解决方法不是记住 更多 API,而是先明确业务语义。

Table of contents

Open Table of contents

时间类型表达不同问题

类型表达什么示例
InstantUTC 时间线上的一个瞬间事件发生时间
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.01.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 与依赖冲突,让构建输入也可复现。


分享这篇文章:

上一篇
注解、反射与动态代理
下一篇
Maven、Gradle 与依赖冲突