MessageFormat / Collator 国际化排序源码
概述
MessageFormat 与 Collator 是 java.text 国际化的两个进阶组件:MessageFormat 把"模板 + 参数"合成本地化消息("{0} 有 {1,number} 个苹果"),支持参数按类型(数字/日期/百分比)自动套用子格式化器;Collator 则实现语言感知排序——按语言规则比较字符串(法语重音、德语变音、中文笔画/拼音),不同于 Java 默认的码点序。
MessageFormat 的核心是模式编译与 FormatElement 展开,Collator 的核心是排序元素(collation element)的三级比较链。本文基于 OpenJDK 21 源码拆解两者内部实现。
核心源码解析
① MessageFormat.format("{0} is {1,number,integer}", args) 的解析
java
public final StringBuffer format(Object[] arguments, StringBuffer result, FieldPosition pos) {
...
formatSubarguments(arguments, result, pos); // ① 依次处理每个 FormatElement
return result;
}
// 构造时
public void applyPattern(String pattern) {
StringBuilder[] segments = new StringBuilder[4]; // ② 解析临时缓冲
...
parseAndAppendFormatPatterns(pattern); // ③ 编译成 FormatElement[]
...
}
private void formatSubarguments(Object[] arguments, StringBuffer result, FieldPosition pos) {
for (int i = 0; i < elements.length; i++) {
FormatElement elem = elements[i]; // ④ 遍历格式元素
if (elem.argumentNumber >= 0) {
... subformat(elem, arguments, result, pos); // ⑤ 参数格式化
} else {
result.append(elem.argumentNumber == FormatElement.RECURSIVE ? ... : elem.text); // ⑥ 纯文本
}
}
}applyPattern在构造时把模式串解析成FormatElement[]数组:纯文本段(argumentNumber = -1)与占位符段交错。- 占位符
{n}只取参数原样输出(toString);{n,type,style}则按类型查子格式化器。 formatSubarguments对每个元素分派:文本直接追加,占位符走subformat执行真正的格式化逻辑。
② FormatElement 的 3 个部分
java
private static class FormatElement {
// 占位符内部结构
int argumentNumber; // ① 参数索引:{0} 里的 0
int formatType; // ② 类型:FORMAT_NUMBER / FORMAT_DATE / FORMAT_TIME / FORMAT_CHOICE
int formatStyle; // ③ 样式:FORMAT_DEFAULT / FORMAT_INTEGER / FORMAT_CURRENCY ...
String text; // 纯文本段
Format subFormat; // 预解析的子格式化器
}- 解析
{1,number,integer}:argumentNumber = 1、formatType = number、formatStyle = integer,逗号分隔三段,缺省段取默认值。 - 类型决定子格式化器:
number→NumberFormat(样式integer/currency/percent或自定义模式)、date/time→SimpleDateFormat(样式short/medium/long/full或模式串)、choice→ChoiceFormat。 subFormat在解析时按需创建并缓存(getFormatFor走makeFormat),避免每次格式化都重建;样式字符串可为null(取类型默认)。
③ MessageFormat 的 ChoiceFormat
java
public ChoiceFormat(String newPattern) {
applyPattern(newPattern);
}
// 模式:0#no apples|1#one apple|2#{0,number} apples
// 应用规则:按数值落入的区间选择分支
public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition status) {
...
for (int i = 0; i < choiceLimits.length; i++) {
if (number < choiceLimits[i]) break; // ① 找到第一个大于 number 的界限
...
}
// ② 选择第 i 个格式分支,可能嵌套 {0,number}
}ChoiceFormat模式:界限#分支1|界限#分支2,界限数组与分支数组等长;0#no表示[0, 1)区间输出no。nextDouble/previousDouble处理边界:limits[i]用Math.nextUp(previous)之类微调,保证浮点区间边界精确不重叠。- 分支中可递归引用
{0,number}等占位符,此时ChoiceFormat与MessageFormat互相调用形成嵌套展开。
④ MessageFormat.toPattern() 的反序列化
java
public String toPattern() {
StringBuilder result = new StringBuilder();
for (FormatElement element : elements) {
if (element.argumentNumber < 0) {
result.append('{').append(element.argumentNumber).append('}'); // ① 递归/引用
} else {
result.append('{').append(element.argumentNumber); // ② 占位符还原
if (element.formatType != FormatElement.FORMAT_DEFAULT) {
result.append(',').append(getArgumentTypeName(element.formatType)); // ③ 类型名
if (element.formatStyle != FormatElement.FORMAT_DEFAULT) {
result.append(',').append(getArgumentStyleName(element.formatType,
element.formatStyle)); // ④ 样式名
}
}
result.append('}');
}
if (element.text != null) appendQuotedString(element.text, result); // ⑤ 文本转义还原
}
return result.toString();
}toPattern()是applyPattern的逆过程:遍历FormatElement[],把数值索引还原为{n}、类型/样式还原为,type,style文本。- 纯文本段中的单引号(
'')与花括号需按appendQuotedString规则重新转义,保证toPattern输出的模式可再次applyPattern无损往返。 MessageFormat的getFormats()与setFormats()也是基于该结构的运行时读写入口。
⑤ Collator.getInstance(Locale) 的区域排序
java
public static Collator getInstance(Locale desiredLocale) {
// ① 从 LocaleServiceProvider 取区域排序器
return (Collator) LocaleProviderAdapter.forType(LocaleProviderAdapter.Type.CLDR)
.getCollatorProvider().getInstance(desiredLocale);
}getInstance(Locale)经CollatorProvider按区域创建;中文区(zh_CN)得到按 GB 码位/笔画序 的规则集,法语区重音规则不同。- 实际对象是
RuleBasedCollator的子类实例:构造时解析该语言的排序规则表(如&a < b < c与&é位置绑定)。 - 区域未命中时按父区域链回退;
getDefault()用Locale.getDefault()。Collator抽象类的关键抽象方法是compare(String, String)。
⑥ RuleBasedCollator.compare(String source, String target) 的排序
java
public int compare(String source, String target) {
...
CollationElementIterator sourceCursor = getCollationElementIterator(source); // ① 迭代器
CollationElementIterator targetCursor = getCollationElementIterator(target);
...
int sOrder = 0, tOrder = 0;
boolean initialSec = true;
do {
if (sOrder == 0) {
do {
sOrder = sourceCursor.next(); // ② 取下一排序元素
} while (sOrder == 0);
}
...
int sPrimary = primaryOrder(sOrder); // ③ 分解三级序
int tPrimary = primaryOrder(tOrder);
if (sPrimary != tPrimary) {
return (sPrimary < tPrimary) ? -1 : 1; // ④ 主序不同即决出
}
...
} while (sOrder != CollationElementIterator.NULLORDER);
return 0;
}CollationElementIterator把字符串逐字符(含多字符组合)翻译成排序元素(int 编码),内部实现 DFA 匹配规则表。primaryOrder/secondaryOrder/tertiaryOrder从元素编码中取出三级序:主序区分基本字母,次序区分重音,三次序区分大小写。- 比较先比主序,相同再比次序,再相同比三次序——这正是语言排序"a 与 A 相邻、é 与 e 相邻"的实现根基。
⑦ CollationKey 的比较键
java
public CollationKey getCollationKey(String source) {
...
CollationElementIterator iter = getCollationElementIterator(source);
...
StringBuilder key = new StringBuilder();
int order = iter.next();
while (order != CollationElementIterator.NULLORDER) {
if (strength == PRIMARY) {
order &= CollationElementIterator.PRIMARYORDERMASK; // ① 按强度截断
} else if (strength == SECONDARY) {
order &= CollationElementIterator.SECONDARYORDERMASK;
}
key.append((char) (order >>> 8)); // ② 编码进字节流
key.append((char) (order & 0xFF));
order = iter.next();
}
return new CollationKey(source, key.toString().getBytes(StandardCharsets.UTF_16BE)); // ③ 字节键
}CollationKey把字符串预计算成排序键字节数组:后续任意两键比较退化为byte[]字典序比较(CollationKey.compareTo内部Arrays.compareUnsigned)。- 键长度通常大于源串(每元素 2 字节),但比较成本 O(n) 且无规则匹配开销——适合需要反复比较的场景(如 TreeMap 预排序)。
- 强度通过掩码截断:
PRIMARY只留主序,键忽略重音与大小写差异;键由getCollationKey生成后与Collator实例绑定(不同强度/区域不可混用)。
⑧ Collator 的强度
java
public abstract static class Strength {
public static final int PRIMARY = 0; // ① 只认基本字符
public static final int SECONDARY = 1; // ② 加认重音
public static final int TERTIARY = 2; // ③ 加认大小写
public static final int IDENTICAL = 3; // ④ 完全一致
}PRIMARY:a与á、A都视为相等,仅区分不同字母——用于"只关心字母本身"的宽松比较。SECONDARY:a与á不同但A与a相同——区分重音、忽略大小写,对应多数欧洲语言的排序默认行为。TERTIARY(默认):进一步区分大小写,a与A不同;绝大多数排序场景用这个强度。setStrength改变比较与getCollationKey的截断规则;相同字符串在PRIMARY强度下与不同字符串在更高强度下可能同时"相等",需按业务语义选强度。
⑨ Bidi / Normalizer 的 Unicode 算法
java
// java.text.Bidi:双向算法
Bidi bidi = new Bidi(paragraphText, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT);
bidi.isLeftToRight(); // 是否纯左到右
bidi.getRunLevel(i); // 第 i 段文本的书写方向等级
// java.text.Normalizer:Unicode 标准化
String nfc = Normalizer.normalize(text, Normalizer.Form.NFC); // 组合形式(é 合并)
String nfd = Normalizer.normalize(text, Normalizer.Form.NFD); // 分解形式(e + ́)
String nfkc = Normalizer.normalize(text, Normalizer.Form.NFKC); // 兼容组合
String nfkd = Normalizer.normalize(text, Normalizer.Form.NFKD); // 兼容分解Bidi实现 Unicode 双向算法(UAX #9):处理阿拉伯文/希伯来文等从右向左文本与拉丁文混排的显示顺序,getRunLevel给出每段文字的嵌入方向等级,reorderVisually按视觉顺序重排。Normalizer的四种标准化形式:NFC/NFD处理规范等价(é的预组合与分解),NFKC/NFKD进一步处理兼容等价(全角/半角、连字);比较字符串前统一形式可避免"看似相同实则不同"。- 两者底层都调用
sun.text的 native/表驱动实现,对国际化输入的处理是消息排序、搜索、渲染的常见前置步骤。