String.format 默认 Locale 变了,接口里的小数点也会变
接口在开发机返回 12.50,部署后却返回 12,50。数值计算没有变,变化可能只发生在最后的 String.format("%.2f", value)。这个重载使用默认格式地区,小数点是展示规则的一部分。
日志、人类可读报表与机器字段常被塞进同一个工具方法,问题就从这里开始。调用方看见返回类型是 String,很难知道它依赖进程地区,也难以分辨输出究竟是展示值还是可重新解析的数值协议。
下面的 Java 17 程序在受控范围内临时切换格式地区,再恢复原值。它只为了让行为显露出来,应用库不应通过修改全局默认地区来修复局部格式化。
import java.util.Locale;
public class FormatLocale {
public static void main(String[] args) {
Locale old = Locale.getDefault(Locale.Category.FORMAT);
try {
Locale.setDefault(Locale.Category.FORMAT, Locale.FRANCE);
System.out.println("default=" + String.format("%.2f", 12.5));
System.out.println("root=" + String.format(Locale.ROOT, "%.2f", 12.5));
System.out.println("tr_upper=" + String.format(Locale.forLanguageTag("tr-TR"), "%S", "i"));
System.out.println("root_upper=" + String.format(Locale.ROOT, "%S", "i"));
} finally {
Locale.setDefault(Locale.Category.FORMAT, old);
}
}
}
保存为 FormatLocale.java,运行 java FormatLocale.java,结果是:
default=12,50
root=12.50
tr_upper=İ
root_upper=I
显式地区是一项协议决定
第一条输出把默认格式地区设为法国,数值中的分隔符变成逗号。第二条传入 Locale.ROOT,不再受这个默认值影响。两条语句使用同一个数值与同一格式串,差异来自格式化上下文。
如果接口约定是 JSON 数字,优先让序列化器按数字类型输出,而不是自己拼一段带地区格式的字符串。如果字段本来就是字符串,比如导出文件中的固定点号小数,可以显式选择无地区偏好的格式上下文,并把小数位数、舍入和正负号一起约定。一个 Locale.ROOT 只消除了地区变化,不会把二进制浮点数自动变成精确金额。
报表则可能确实应该用用户地区:千位分组、小数分隔符和月份名称都属于展示体验。此时地区应来自当前用户或文档参数,明确传入,而不是碰巧继承服务器的启动环境。服务同时处理多个地区时,修改全局默认值会让并发请求互相影响。
格式串里的 %,.2f 还会启用分组。分组适合人阅读,却可能让机器字段多出协议没有允许的字符。排查乱码般的数字时,既要看 Locale,也要看格式标记,不能只搜索小数点替换代码。
大写转换也不是固定的 ASCII 规则
程序用土耳其地区格式化 %S,小写 i 会得到带点的大写 I。Locale.ROOT 的结果则是普通 I。这个例子提醒的是范围:地区敏感行为不限于逗号和点号,大小写及日期文本也可能变化。
若字符串是 HTTP 头名称、配置键或程序内部协议标记,先确认协议规定的字符集及大小写规则,再使用相应规范化方法。不要把面向自然语言的格式化当成通用标识转换。反过来,人名和自然语言内容也不应为了机器方便全部按ASCII规则重写。
示例只改变 Locale.Category.FORMAT,并在 finally 中恢复。Java 的默认地区还有类别区分,测试里只改一种设置,却期待所有地区行为同时变化,容易产生错误结论。正式排查应记录调用使用的显式地区、默认格式地区与实际输出样本,而不是只看操作系统界面语言。
文本格式正确,还剩下两个边界
输出写到字节流时,字符编码仍要单独指定。Locale 决定如何表示,Charset 决定怎样编码,两者没有自动绑定关系。一个小数文本用对点号,并不能保证报表里的中文也被正确写出。
格式化精度也不等于业务精度。%.2f 改变展示的小数位,并没有把原值存回为两位小数,也不能替代金额计算中的舍入策略。对账代码如果先格式化再解析,很可能把展示规则绕回数值计算,增加一次不必要的语义转换。
验证这类工具方法,应该在至少两个具有不同格式规则的地区下运行同一组协议样本,断言的是精确字符,而不是“看起来像数字”。测试不应永久改动全局默认值;能显式传 Locale 的代码,就直接传入并逐一核对。最终留下的工具接口最好让用途一眼可见:面向人的展示接收地区,面向机器的序列化遵守固定协议,不把这项选择藏在服务器配置里。