开始前,会这些就够了
- 理解
Object[] 参数数组与下标;能读字符串字面量。 - 知道数字的显示格式可以依赖地区。
- 先读 字符串与
StringBuilder:文本长度并不总是字符数:先理解字符串中的实际字符,再区分模板语法和普通参数。
通知模板写成 User's {0},参数传入 Ada,结果却是 Users {0}。参数明明在数组里,为什么既丢了单引号,又留下占位符?原因需要从模板解释开始找,而不是先怀疑数据库或用户姓名。
这一篇把模板、参数数组和地区规则固定为可以直接查看的值。程序只生成字符串,不发送通知。每组对照改变一个条件,最后再看一个容易一起出现的问题:对象的 locale 改了,数字输出为什么还没变。
把例子带到自己的电脑上
下载完整练习包
需要 JDK 17 或更新版本,使用 Java 标准库;制作时实测 Windows / JDK 17.0.20.1 / 21.0.12。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- MessagePatternDemo.java:完整可运行示例
- README.md:运行说明与独立练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和产物验证记录
- SHA256SUMS.txt:内容摘要
在这个文件夹打开 PowerShell,逐行运行
java -version
javac -version
javac -encoding UTF-8 --release 17 MessagePatternDemo.java
java -cp . MessagePatternDemo解压到独立练习文件夹,在含 MessagePatternDemo.java 的目录打开终端运行。Windows 固定内存模板与参数;单引号、字面占位符、普通参数不再解释、缺参、花括号错误、number子格式重建;JDK17/21各9项断言。未测choice、日期、复数规则、并发或发送通知。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
第一步:单引号在模板里可以是一条指令
第一组模板的单引号只有一个。实际输出是 Users {0},说明后面的占位符没有按参数位置执行。将它改为连续两个单引号,输出才成为 User's Ada。
模板解释器会把单引号用于引用文字;连续两个单引号表示一个实际的单引号。未闭合的引用可能被当成到模板结尾才结束,所以不是所有错误文案都会在创建对象时抛异常。
因此编译通过、对象创建成功和最终文案正确是不同的检查。发布模板之前至少要用实际参数生成一遍,检查哪些字符被引用、哪些占位符真正生效。
第二步:参数中的花括号,不等于模板中的花括号
程序把模板写为带引用的 {0} 再接一个正常 {0},输出分别是原文 {0} 和 Ada。这组对照展示同样几个字符处在不同模板位置时,含义可以不同。
下一组把 {1}'$1 当作普通字符串参数放入 user={0}。输出原样保留这段参数,没有再把里面的 {1} 执行一次。这是本例普通字符串参数的一次格式化结果;没有使用 choice 子格式的递归能力。
所以不要在参数数据上盲目执行模板转义,更不要把用户数据先拼进模板再重新解释。让模板负责结构,让参数负责填入的值,能让这两条路径更容易核对。
第三步:占位符仍在,不只一种原因
缺少第一个引用结尾时,本例的 {0} 因引用规则保留;参数数组缺少第二个元素时,本例的 {1} 也保留。肉眼看到花括号不能直接断定是同一个错误。
源码单独保存了缺参结果和不平衡花括号的异常。排查时应记录模板原文、参数数量、各参数类型以及实际输出,区分引用、缺参和语法错误。
真实通知还应检查业务上哪些参数必须存在。这里仅展示库的行为,不把输出中残留占位符视为可以发送给用户的成功消息。
第四步:地区属性和已经创建的数字格式不是一个对象
最后一组使用带 number 类型的模板。创建时选择 US,得到 1,234.50;只调用 setLocale(GERMANY) 后,结果仍是 1,234.50;再应用原模板,才得到 1.234,50。
这说明本例数字子格式在模板解析时已经创建。修改父对象的地区属性,不会自动改写此前创建的子格式。代码重新应用模板,让数字子格式按新地区重建。
本例每次创建独立的格式器,没有让多个线程同时修改同一个对象。范围只包含普通字符串、一个数字子格式和两种地区;日期、choice、多语言复数及通知发送没有实跑。
运行单文件,按实际输出逐项核对
保存为 MessagePatternDemo.java,在源码目录执行下方命令;需要完整 JDK 17 或更新版本。无需 Maven、网络或第三方依赖。
2026 年 10 月 18 日在 Windows 上分别以 Microsoft OpenJDK 17.0.20.1 与 21.0.12、--release 17 编译运行,输出一致。每项断言失败都会终止程序;下方是本次真实输出。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.text.MessageFormat;
import java.util.Locale;
public class MessagePatternDemo {
static int checks;
static void check(boolean ok) { if(!ok) throw new AssertionError(); checks++; }
static String render(String pattern, Object... values) {
return new MessageFormat(pattern, Locale.US).format(values);
}
public static void main(String[] args) {
String broken=render("User's {0}","Ada");
String fixed=render("User''s {0}","Ada");
check(broken.equals("Users {0}")); check(fixed.equals("User's Ada"));
System.out.println("single_quote="+broken+"; doubled_quote="+fixed);
String literal=render("'{0}' / {0}","Ada");
check(literal.equals("{0} / Ada"));
System.out.println("quoted_placeholder="+literal);
String data=render("user={0}","{1}'$1");
check(data.equals("user={1}'$1"));
System.out.println("argument_data="+data);
String missing=render("first={0}; second={1}","Ada");
check(missing.equals("first=Ada; second={1}"));
System.out.println("missing_argument="+missing);
boolean invalid=false;
try { new MessageFormat("value={0",Locale.US); } catch(IllegalArgumentException e) { invalid=true; }
check(invalid); System.out.println("unbalanced_brace=IllegalArgumentException");
String pattern="{0,number,#,##0.00}";
MessageFormat format=new MessageFormat(pattern,Locale.US);
String before=format.format(new Object[]{1234.5});
format.setLocale(Locale.GERMANY);
String afterSet=format.format(new Object[]{1234.5});
format.applyPattern(pattern);
String afterApply=format.format(new Object[]{1234.5});
check(before.equals("1,234.50")); check(afterSet.equals(before)); check(afterApply.equals("1.234,50"));
System.out.println("number: US="+before+"; setLocale_only="+afterSet+"; reapply_pattern="+afterApply);
System.out.println("PASS checks="+checks);
}
}
怎样运行:在源码所在目录,先单独执行 javac -encoding UTF-8 --release 17 MessagePatternDemo.java;编译成功后,再执行 java -cp . MessagePatternDemo。两条命令分别回车。2026-10-18 已在 Windows、JDK 17.0.20.1 / 21.0.12 编译运行此份源码并通过检查。Windows 固定内存模板与参数;单引号、字面占位符、普通参数不再解释、缺参、花括号错误、number子格式重建;JDK17/21各9项断言。未测choice、日期、复数规则、并发或发送通知。
运行后对照结果
single_quote=Users {0}; doubled_quote=User's Ada
quoted_placeholder={0} / Ada
argument_data=user={1}'$1
missing_argument=first=Ada; second={1}
unbalanced_brace=IllegalArgumentException
number: US=1,234.50; setLocale_only=1,234.50; reapply_pattern=1.234,50
PASS checks=9记住这三点
- 模板中的单引号和普通参数数据中的单引号走不同解释路径。
- 未替换的占位符可能来自引用,也可能来自缺少参数。
- 修改 locale 不会自动更新已经创建的子格式。
别把例子用过头
- 把翻译文案当作任意字符串,不做带参数的输出检查。
- 将用户数据拼入模板后再次解释。
- 共享可变格式器,并假设只改 locale 就能切换所有输出。
自己试一小步
怎样给一组通知模板做发布前检查,分清缺参、引用和数据原样保留?
- 自己编写四个含单引号、花括号和多个占位符的模板,先写预期句子。
- 分别使用完整参数、缺少参数和带特殊字符的普通字符串参数。
- 新增一种显式地区的数字显示,比较新建格式器与修改旧格式器。
- 提交模板表、参数表、实际输出和未覆盖的格式类型。
做到这些,就算完成
- 模板语法与参数原文分开记录。
- 检查结果不能只依赖是否抛异常。
- 没有发送通知,也没有把普通字符串参数结论推广到choice递归。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- MessageFormat:引用规则与子格式生命周期 (新标签页)↗
支持模板单引号、缺参行为和
setLocale/applyPattern区别。 - Locale:显式地区与默认地区 (新标签页)↗
支持区分JVM默认值和显式指定地区。
- NumberFormat:地区相关数字显示 (新标签页)↗
支持数字子格式按地区产生不同分隔形式。