带参数的消息是国际化里最常用的东西,也是最容易出隐蔽 bug 的地方:数字被加了千分位、撇号吞掉占位符、复数规则表达不了、实例不能共享。这篇逐项实测这些行为。
[locale]
en_US amy 于 October 4, 2026 购买 1,234,567 件,共 $1,234.50(26%)
de_DE amy 于 4. Oktober 2026 购买 1.234.567 件,共 1.234,50 €(26 %)
fr_FR amy 于 4 octobre 2026 购买 1 234 567 件,共 1 234,50 €(26 %)
zh_CN amy 于 2026年10月4日 购买 1,234,567 件,共 ¥1,234.50(26%)
ja_JP amy 于 2026年10月4日 购买 1,234,567 件,共 ¥1,234(26%)
ar_EG amy 于 ٤ أكتوبر ٢٠٢٦ 购买 ١٬٢٣٤٬٥٦٧ 件,共 ١٬٢٣٤٫٥٠ ج.م.(٢٦٪)
默认 Locale(format):en_CN;new MessageFormat(pattern) 不带 Locale 用它
同一个 pattern,{1,date} 不带样式 / short / medium / long / full(Locale.US):
{0,date} → Oct 4, 2026
{0,date,short} → 10/4/26
{0,date,medium} → Oct 4, 2026
{0,date,long} → October 4, 2026
{0,date,full} → Sunday, October 4, 2026
{0,time} → 3:30:00 PM
{0,date,yyyy-MM-dd HH:mm} → 2026-10-04 15:30 [number]
{0} 直接放整数:
2026 → 2,026
1234567 → 1,234,567
1234567.891 → 1,234,567.891
12345678901234 → 12,345,678,901,234
3.0 → 3
1.0E-7 → 0
2147483647 → 2,147,483,647
年份 2026 在 pattern 里:"{0}年" → 2,026年;德语 → 2.026年;法语 → 2 026年(法语分组分隔符的码点:8239)
{0,number,#} → 2026;{0,number,integer} → 1,234,567;{0,number,#.##} 对 3.14159 → 3.14;传 String.valueOf → 2026年
BigDecimal 与 BigInteger 也按数字格式化:12,345,678,901,234,567,890.123;String 原样:12345
Boolean / null / 数组:true|null|[I@… [quote]
It's {0} → Its {0}
It''s {0} → It's ok
'{0}' 是占位符 → {0} 是占位符
''{0}'' → 'ok'
'{'0'}' → {0}
a '' b → a ' b
{0} don't {1} → ok dont {1}
'It''s' {0} → It's ok
{{0}} → IllegalArgumentException: can't parse argument number: {0}
5 { 6 → IllegalArgumentException: Unmatched braces in the pattern.
JDK 里 MessageFormat.format("It's {0}", "ok") = Its {0}
MessageFormat.format("{0}\\n", ..) 反斜杠不是转义:a\nb [choice]
n=0 → 没有文件
n=1 → 一个文件
n=2 → 2 个文件
n=1000 → 1,000 个文件
n=-1 → 没有文件
n=0.5(1# 之前、0# 之后)→ 没有文件;n=1.0 → 一个文件;n=1.5 → 2 个文件;n=NaN → 没有文件;n=-Infinity → 没有文件
ChoiceFormat(limits {0,1,2}) → 0.9: zero | 1: one | 2: other | 5: other | -3: zero
极限写法 ChoiceFormat.nextDouble(1) = 1.0000000000000002;'1<' 等价于下限 nextDouble(1)
俄语复数需要 one/few/many 三种(1, 2-4, 5-20 …),choice 只能用数值区间:
1 → файл 俄语规则应为 файл
2 → файла 俄语规则应为 файла
5 → файлов 俄语规则应为 файлов
11 → файлов 俄语规则应为 файлов
21 → файл 俄语规则应为 файл
22 → файла 俄语规则应为 файла
25 → файлов 俄语规则应为 файлов
101 → файлов 俄语规则应为 файл ×
102 → файлов 俄语规则应为 файла ×
111 → файлов 俄语规则应为 файлов
1001 → файлов 俄语规则应为 файл ×
11 个数里 choice 给出错误形式的有 3 个(区间模型只能写到有限个界限,25 以后一律是 файлов)
嵌套:1,234 files in 3 folders [args]
参数个数不够:1 + 2 = {2}
参数多于占位符:1
同一个参数多次:x x x
参数乱序:B 在 A 之前
null:null;整个 args 为 null:{0}
pattern {0 → IllegalArgumentException: Unmatched braces in the pattern.
pattern {a} → IllegalArgumentException: can't parse argument number: a
pattern {-1} → IllegalArgumentException: negative argument number: -1
pattern {0,foo} → IllegalArgumentException: unknown format type: foo
pattern {0,number,#.##.#} → IllegalArgumentException: Multiple decimal separators in pattern "#.##.#"
pattern } → }
pattern {0,choice,abc} → ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
pattern {1000} → {1000}
MessageFormat.toPattern() = {2} {0} {1};getFormatsByArgumentIndex().length = 3;getFormats().length = 3
toPattern() 的往返:{0,number,#0.#} and {1,date,short};解析 parse("3.5 and 10/4/26") 得到 [3.5, Sun Oct 04 00:00:00 CST 2026] [thread]
8 个线程共用同一个 MessageFormat 实例,各格式化 20000 次(共 160000 次):结果不对 80146 次,抛异常 114 次;第一个错误:n=400000 date=2001-09-14 (期望 n=400000 date=2006-01-26)
每次新建 MessageFormat:结果不对 0 次
共用实例但用 synchronized 包住:结果不对 0 次 [perf]
第 4 轮:MessageFormat.format(静态,每次解析) 1078 ns;复用实例(单线程)313 ns;String.format 389 ns
第 5 轮:MessageFormat.format(静态,每次解析) 1102 ns;复用实例(单线程)327 ns;String.format 491 ns
第 6 轮:MessageFormat.format(静态,每次解析) 1094 ns;复用实例(单线程)319 ns;String.format 507 ns
(自写计时循环,不是 JMH;sink=true)