SimpleDateFormat / NumberFormat / DecimalFormat 格式源码
概述
java.text 的格式化三件套:SimpleDateFormat 负责日期时间格式化(yyyy-MM-dd HH:mm:ss),NumberFormat 是数字/货币/百分比的抽象基类,DecimalFormat 是其十进制实现(千分位、小数位、货币符号)。三者都遵循双向协议:format 把对象转成字符串,parse 反向解析,且都基于语言环境(Locale)呈现本地化差异。
java.text 的这套设计早在 JDK 1.1 就定型:格式字符串逐字符解析成模式段,数字经 DigitList 做高精度舍入,日期经 Calendar 做字段换算。本文基于 OpenJDK 21 源码拆解两条格式化链路的内部实现。
核心源码解析
① SimpleDateFormat 的 format(Date) 流程
java
public final StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition pos) {
pos.beginIndex = pos.endIndex = 0;
return format(date, toAppendTo, pos.getFieldDelegate());
}
private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) {
calendar.setTime(date); // ① 把 Date 装入 Calendar
boolean inQuote = false;
...
for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8; // ② 模式标签
int count = compiledPattern[i++] & 0xff;
switch (tag) {
case TAG_QUOTE_ASCII_CHAR: ... // ③ 转义文本
case TAG_QUOTE_CHARS: ...
default: // ④ 日期字段
subFormat(tag, count, delegate, toAppendTo, ...);
}
}
return toAppendTo;
}calendar.setTime(date)把时间点拆成Calendar的年月日时分秒字段——之后所有模式片段都从Calendar读取。- 模式串在构造时被编译成整数数组
compiledPattern:每项高 8 位是字段标签(TAG_YEAR等),低 8 位是该字段的重复次数,运行期不再解析字符串。 subFormat按字段类型取值:年取calendar.get(YEAR)、月取MONTH + 1、时区按getTimeZone()换算,再按重复次数补零/补全输出。
② SimpleDateFormat.parse(String, ParsePosition) 的解析流程
java
public Date parse(String text, ParsePosition pos) {
...
CalendarBuilder calb = new CalendarBuilder(); // ① 字段累积器
int ambiguity = 0;
...
for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
switch (tag) {
...
default: {
subParse(text, pos, ..., tag, count, ...); // ② 逐字段反向解析
break;
}
}
}
...
Date parsedDate = calb.establish(calendar).getTime(); // ③ 建立日历
pos.setIndex(...); // ④ 推进解析位置
return parsedDate;
}- 解析是格式化的镜像:按编译模式逐段匹配文本,
subParse把截出的子串按字段语义换算成数值(如yy两位数年份映射到世纪)。 CalendarBuilder暂存各字段值并记录歧义字段(如MM与HH都可能是两位数);establish(calendar)统一校准默认值(如HH:mm缺省日期取当天)。ParsePosition双向通信:index记录已解析位置,出错时errorIndex指向失败字符;parse(String)便捷方法对全文强制成功,否则抛ParseException。
③ DateFormatSymbols 的时区/月份名称
java
public DateFormatSymbols() {
initializeData(Locale.getDefault(Locale.Category.FORMAT));
}
private void initializeData(Locale desiredLocale) {
Locale locale = getSupportedLocale(desiredLocale); // ① 匹配可用区域
...
resourceBundle = LocaleData.getDateFormatData(locale); // ② 读本地化资源
eras = resourceBundle.getStringArray("Eras");
months = resourceBundle.getStringArray("MonthNames");
shortMonths = resourceBundle.getStringArray("MonthAbbreviations");
weekdays = resourceBundle.getStringArray("DayNames");
...
}DateFormatSymbols是SimpleDateFormat的本地化符号表:月份名、星期名、纪元、AM/PM、时区缩写,全部从LocaleData的ResourceBundle加载。- 中文本地化后
months是"一月"到"十二月"、weekdays是"星期日"到"星期六";英文则是January系列。 getInstanceRef(Locale)走内部缓存(ConcurrentHashMap),避免每个SimpleDateFormat重复加载资源包;SimpleDateFormat构造时自动取一份引用。
④ SimpleDateFormat 的模式字母
java
// 常用模式字母及含义(pattern 字符,编译成 tag)
public static final char ERA_FIELD = 'G'; // 纪元
public static final char YEAR_FIELD = 'y'; // 年
public static final char MONTH_FIELD = 'M'; // 月(数字或名称)
public static final char DATE_FIELD = 'd'; // 日
public static final char HOUR_OF_DAY1_FIELD = 'k'; // 1-24 小时
public static final char HOUR_OF_DAY0_FIELD = 'H'; // 0-23 小时
public static final char MINUTE_FIELD = 'm'; // 分
public static final char SECOND_FIELD = 's'; // 秒
public static final char MILLISECOND_FIELD = 'S'; // 毫秒
public static final char TIMEZONE_FIELD = 'z'; // 时区- 字母重复次数决定格式:
yy(两位年)、yyyy(四位年)、MM(两位月)、MMM(缩写月名)、MMMM(全称月名)。 HH是 0-23 小时制、hh是 1-12 小时制(配合aAM/PM)、kk是 1-24 小时制——同字段不同进制由字母区分。- 未列入表的字母(如
X的 ISO 时区、Z的 RFC822 时区偏移)由TimeZoneNames的别名机制补充,非法字母抛IllegalArgumentException。
⑤ NumberFormat.getNumberInstance(Locale) 的工厂
java
public final static NumberFormat getNumberInstance(Locale inLocale) {
return getInstance(inLocale, NUMBERSTYLE); // ① 数字样式
}
private static NumberFormat getInstance(Locale desiredLocale, int choice) {
Locale l = (Locale) locProvider.getAvailableLocales(); // ② 匹配区域
...
NumberFormat numberFormat = null;
if (numberFormat == null) {
numberFormat = getNumberFormatForLocale(l, choice); // ③ 反射创建
}
...
return numberFormat;
}
private static NumberFormat getNumberFormatForLocale(Locale l, int choice) {
...
NumberFormat numberFormat;
switch (choice) {
case NUMBERSTYLE -> numberFormat = new DecimalFormat(l); // ④ 默认十进制
case CURRENCYSTYLE -> ...; // new DecimalFormat 货币模式
case PERCENTSTYLE -> ...; // new DecimalFormat 百分比模式
}
...
}- 工厂按样式创建
DecimalFormat并注入对应默认模式:数字样式#,##0.###、货币样式¤#,##0.00(¤是货币占位符)、百分比样式#,##0%。 - 区域匹配逻辑:显式
Locale需在LocaleProviderAdapter的可用区域中命中,未命中回退到父区域链(zh_CN→zh→ 默认)。 - 创建同时传入
DecimalFormatSymbols(locale),把小数点、千分位、货币符号等本地化符号一次性绑定。
⑥ DecimalFormat.format(double, StringBuffer, FieldPosition) 的舍入
java
public StringBuffer format(double number, StringBuffer result, FieldPosition fieldPosition) {
...
// ① 分解 double 为长整数形式,交给 DigitList
boolean isNegative = (number < 0.0) || (number == 0.0 && 1 / number < 0.0);
if (isNegative) number = -number;
...
if (Double.isNaN(number)) { ... }
...
digitList.set(number, getMaximumFractionDigits(), !isCurrency); // ② 数字转 DigitList
...
subformat(result, fieldDelegate, isNegative, false, ...); // ③ 按模式输出
...
}DigitList是DecimalFormat的核心:把double拆成十进制数字序列(digits[])与小数指数,规避二进制浮点误差,保证0.1 + 0.2之类的值能按模式精确呈现。digitList.set(number, maxFractionDigits, !isCurrency)内部先doubleToLongBits取二进制表示,再按指定小数位数做十进制舍入并记录有效位。subformat按模式段输出:整数部分按groupingSize插千分位、小数部分从DigitList取数字补零到最小位数、%/货币符号按DecimalFormatSymbols落地。
⑦ DecimalFormat 的 RoundingMode
java
private transient RoundingMode roundingMode = RoundingMode.HALF_EVEN; // ① 默认值
public void setRoundingMode(RoundingMode roundingMode) {
if (roundingMode == null) throw new NullPointerException();
this.roundingMode = roundingMode;
digitList.setRoundingMode(roundingMode); // ② 同步到 DigitList
}- 默认
HALF_EVEN(银行家舍入):恰好 0.5 时向最近的偶数舍入,减少累计统计偏差;System.out.printf("%.2f")走Formatter则是HALF_UP。 - 舍入真正执行在
DigitList.round():按精度位数考察截断位,结合roundingMode决定进位还是舍去。 RoundingMode八种模式(UP/DOWN/CEILING/FLOOR/HALF_UP/HALF_DOWN/HALF_EVEN/UNNECESSARY)与BigDecimal共享枚举,语义一致。
⑧ DecimalFormatSymbols 的语言环境差异
java
public DecimalFormatSymbols(Locale locale) {
initialize(locale);
}
private void initialize(Locale locale) {
...
LocaleProviderAdapter adapter = LocaleProviderAdapter.getAdapter(DecimalFormatSymbolsProvider.class, locale);
...
decimalSeparator = resourceBundle.getString("decimalSeparator"); // ① 小数点
groupingSeparator = resourceBundle.getString("groupingSeparator"); // ② 千分位
currencySymbol = resourceBundle.getString("currencySymbol"); // ③ 货币符号
intlCurrencySymbol = resourceBundle.getString("intlCurrencySymbol");
minusSign = resourceBundle.getString("minusSign"); // ④ 负号
percent = resourceBundle.getString("percent"); // ⑤ 百分号
...
}- 符号全部来自语言环境资源:英文区
./,/$;德语区,是小数点、.是千分位;中文区¥;欧元区€。 currencySymbol(¥)是本地显示符号,intlCurrencySymbol(CNY)是国际三字母代码,%在部分区域也有本地化变体。- 用户可通过
setDecimalSeparator等 setter 覆盖默认符号;DecimalFormat内部保存的是符号对象引用,修改会立即影响后续格式化。
⑨ NumberFormat 的缓存
java
// NumberFormat 内部缓存:按 Locale 弱引用缓存实例
private static final HashMap<Locale, SoftReference<NumberFormat>[]> cachedNumberFormatData = ...;
// 实际结构(DateFormatHash 类似思路):
// Map<Locale, SoftReference<NumberFormat>>,键为 Locale,值为软引用
static NumberFormat getCachedInstance(Locale locale, int choice) {
...
NumberFormat[] formats = ref.get(); // ① 取软引用
if (formats == null || formats[choice] == null) {
NumberFormat format = getNumberFormatForLocale(locale, choice); // ② 未命中重建
...
ref = new SoftReference<>(formats);
cachedNumberFormatData.put(locale, ref); // ③ 放入缓存
}
return formats[choice];
}- 缓存键是
Locale、值是SoftReference<NumberFormat>[](按 4 种样式索引):软引用保证内存压力下缓存可被 GC 回收,不造成内存泄漏。 - 命中时直接返回共享实例——
NumberFormat本身线程安全(内部用DigitList本地副本),多线程可安全共享同一实例。 getAvailableLocales()遍历LocaleServiceProvider汇总 JDK 内建与 SPI 提供的可用区域,供上层枚举能力。