DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

Java String.format 小数点在另一台机器上变成逗号

核对进程默认 FORMAT 区域,在要求固定小数点的调用中传入 Locale,避免全局设置或字符串替换影响其他输出。

同一段 String.format("%.2f", price),在一台机器上输出 12.50,换到另一台机器却变成 12,50。格式串里的点号没有被改动,数值也没有变化,差异来自格式化使用的区域设置。

%.2f 中的 .2 表示保留两位小数,不是指定输出必须使用英文句点。不显式传入 Locale 时,String.format 使用 JVM 默认的 FORMAT 区域;小数分隔符会按该区域替换。

以下示例按 Java 8 及以上编写。问题针对数值的字符串表示,不涉及修改数值本身,也不需要为了一个导出字段调整整个操作系统的语言。

在出问题的进程中查看格式化区域

查看开发机设置不足以确定服务进程的默认值。可以在实际 JVM 中输出:

import java.util.Locale;

System.out.println(Locale.getDefault(Locale.Category.FORMAT));

这是可放进已有方法的诊断片段。默认区域由 JVM 启动环境初始化,也可能被应用代码改动;检查运行进程、启动配置以及项目中对 Locale.setDefault 的调用。

如果同一进程中某些调用使用 Locale.US、某些省略 Locale,输出也可能不一致。应追踪生成最终字符串的调用位置,不能只在数值解析入口改设置。

只为要求句点的输出指定 Locale

如果文件或接口契约要求以 . 表示小数点,在该次调用中传入明确的区域:

import java.util.Locale;

public class DecimalOutput {
    public static void main(String[] args) {
        double price = 12.5;

        String dot = String.format(Locale.US, "%.2f", price);
        String comma = String.format(Locale.GERMANY, "%.2f", price);

        System.out.println(dot);
        System.out.println(comma);
    }
}

这里预期输出依次为 12.50 和 12,50。两种表示都符合各自区域的规则,选择哪一种取决于接收方要求。面向用户的本地化界面不应为了统一内部文件格式而全部改成美国区域。

调用级 Locale 不会修改其他线程的默认区域。相比之下,Locale.setDefault(Locale.US) 会影响同一 JVM 内依赖默认区域的其他功能;即使随后恢复,也可能让并发请求在中间时段观察到不同设置。因此不要用“临时改全局、格式化、改回来”的办法处理单个字段。

不要在输出末尾统一替换逗号

将字符串中的全部逗号替换成句点,会混淆小数分隔符与分组分隔符。例如带 , 标志的 %,.2f 会按区域插入千位分组字符。1,234.50 与 1.234,50 不能靠全局字符替换可靠地转换。

格式串本身也要符合目标契约。若接收方不允许千位分组,应避免使用 %,.2f,不能仅指定 Locale 后就认为输出适合所有数字解析器。

%.2f 还会对显示结果舍入到两位小数。修好小数分隔符不代表这种舍入适合保存完整数值;NaN、无穷值也不是所有文件或接口允许的数字。结构化数据应由相应序列化组件处理,不要把一段面向显示的格式化字符串直接当作任意协议中的数值。

核对完整输出,而不只看一个价格

在隔离样例中用显式的不同 Locale 比较输出,覆盖零、负数、大于一千的值,以及需要舍入的小数。固定格式出口应始终满足接收方约定;用户界面则应继续采用它自己的区域规则。

这项修复通常只改格式化调用。保留原代码和导出样例便于回退,避免批量替换已经生成的文件内容。如果旧文件已被下游误读,需从原始数值重新生成或核对,单独恢复代码不能修复已经导入的数据。