Java 数值、字符串与时间:金额少一分和日期串一天怎么查
金额、字符串和日期都属于“看起来简单,出了错很难查”的类型。它们的坑通常藏在精度、正则语义、索引边界和时区这些细节里。
BigDecimal 与金额
不要用 new BigDecimal(double)
现象
金额计算出现 0.30000000000000004 这类结果,或者对账时少一分钱。
1 | BigDecimal amount = new BigDecimal(0.1); |
根因
double 本身不能精确表示很多十进制小数。new BigDecimal(double) 会保留 double 的二进制近似值,所以精度并没有变好。
正确写法
优先用字符串构造,或者用 BigDecimal.valueOf:
1 | BigDecimal amount = new BigDecimal("0.1"); |
BigDecimal.valueOf(double) 内部先转成字符串,再构造,适合从 double 来源安全转换。金额存储最好从源头就用 DECIMAL,不要让它先经过 double。
equals 和 compareTo 不是一回事
现象
用 BigDecimal 作为 Map 的 key,1.0 和 1.00 有时取不到同一个值;但业务上明明都是 1 元。
根因
BigDecimal.equals 既比较数值,也比较 scale。1.0 和 1.00 数值相等,但 scale 不同,所以 equals 返回 false。compareTo 则只比较数值,返回 0。
1 | new BigDecimal("1.0").equals(new BigDecimal("1.00")); // false |
正确写法
金额比较用 compareTo:
1 | if (a.compareTo(b) == 0) { |
如果要把金额放进 HashMap,先统一 scale,例如都用 setScale(2, RoundingMode.HALF_UP),避免同一个金额有多个 key。
除法必须指定舍入规则
现象
两个 BigDecimal 相除,突然抛 ArithmeticException: Non-terminating decimal expansion。
1 | BigDecimal result = new BigDecimal("10").divide(new BigDecimal("3")); |
根因
BigDecimal 除法如果结果是无限小数,不指定精度和舍入模式时,无法表示结果,于是抛异常。
正确写法
除法时显式指定 scale 和 RoundingMode:
1 | BigDecimal result = new BigDecimal("10") |
金额舍入规则要在业务里统一,不能有的地方向上取整,有的地方向下取整。
String 的细节
split 用的是正则,不是普通分隔符
现象
想按点号拆分文件名,结果得到一个空数组:
1 | String[] parts = "a.b.txt".split("."); |
根因
String.split 的参数是正则表达式,. 在正则里表示任意字符,所以 "a.b.txt" 被拆成了空数组。类似 |、+、*、? 这些字符也需要转义。
正确写法
简单分隔符可以转义,或者用 Pattern.quote:
1 | String[] parts = "a.b.txt".split("\\."); |
如果只是按单个固定字符拆分,Splitter 等工具类通常比手写正则更不容易错。
末尾空串默认会被丢掉
现象
按逗号拆分 "a,b,",期望得到三个元素,结果只有两个:
1 | String[] parts = "a,b,".split(","); |
根因
split 默认会移除结果末尾的空字符串。
正确写法
需要保留末尾空串时,传入负数 limit:
1 | String[] parts = "a,b,".split(",", -1); |
这个坑在解析 CSV、配置文件、消息字段时很常见,因为空字段往往有业务含义。
substring 的下标语义和 SQL 不同
现象
从 Java 里截取字符串时越界,或者截出来的长度和 SQL SUBSTRING 对不上。
根因
Java 的 substring(beginIndex, endIndex) 是左闭右开,endIndex 不包含在内,且索引从 0 开始。很多数据库的 SUBSTRING(str, start, length) 第三个参数是长度,语义完全不同。
1 | String s = "abcdef"; |
正确写法
跨语言处理字符串时,统一在边界做转换,并给 endIndex 加显式校验。需要按长度截取时:
1 | int begin = 1; |
不要凭记忆在两个系统之间复用下标。
日期、时间与时区
LocalDateTime 不代表一个绝对时间点
现象
服务跨时区部署后,用户看到的创建时间和数据库里的时间差了几个小时。
根因
LocalDateTime 只包含年月日时分秒,没有时区信息。同一串 2026-09-07 10:00:00 在 UTC 和上海时区代表不同的时刻。拿它跨系统传输时,接收方不知道它到底是哪个时区。
正确写法
需要表示绝对时间点时使用 Instant 或带时区的 ZonedDateTime:
1 | Instant now = Instant.now(); |
LocalDateTime 适合表达“本地钟表时间”,例如“每天上午 9 点提醒”,而不是用来记录跨系统事件时间。接口返回时间最好同时带时区,或者约定统一使用 UTC。
Jackson 不配置无法处理 LocalDateTime
现象
升级到 Java 8 时间类型后,接口返回 500,日志报 InvalidDefinitionException 或日期被序列化成奇怪格式。
根因
Jackson 默认不识别 LocalDateTime 等 java.time 类型,需要注册 JavaTimeModule。如果只加了模块,还可能出现时间戳数组或时区偏移问题。
正确写法
配置 ObjectMapper:
1 | ObjectMapper mapper = new ObjectMapper(); |
对字段单独指定格式:
1 |
|
接口时间格式和时区要全局统一,避免不同模块返回不同格式。数据库字段最好也明确使用 DATETIME 或 TIMESTAMP 的语义,并约定是否存储 UTC。
排查清单
这类问题先看四个点:
- 金额是否从
double进入 BigDecimal,除法是否指定了 scale 和舍入模式。 split参数是否被当成了正则,是否在意末尾空串。- 字符串索引是否混用了“长度”和“结束下标”。
- 时间是否带时区,
LocalDateTime和Instant是否用在了正确场景。
