DecimalFormat 是 Java 中一个用于格式化十进制数字 的类,属于 java.text 包。你可以把它理解成一个"数字美化工具",它能按照你设定的模式,把数字变成你想要显示的字符串,也能把符合格式的字符串解析回数字。
它有什么用?
简单来说,三大用途:
-
把数字格式化成特定样式的字符串
比如:保留两位小数、显示千分位逗号、加货币符号、显示成百分比等。
-
把字符串解析回数字
将符合格式的字符串(如
"1,234.56")解析成Number对象。 -
统一显示风格
在报表、票据、金额展示等场景,避免手动拼接字符串导致格式不一致。
常用模式符号
DecimalFormat 通过模式字符串来控制格式,最常用的符号:
| 符号 | 含义 | 示例模式 | 数字 1234.5 的结果 |
|---|---|---|---|
0 |
必须显示的位(不足补零) | 00000.000 |
01234.500 |
# |
有数字才显示,无则不显示 | ####.### |
1234.5 |
. |
小数点 | #.00 |
1234.50 |
, |
分组分隔符(千分位) | #,###.## |
1,234.5 |
% |
乘以100并显示为百分数 | #.##% |
123450% (1234.5*100) |
¤ |
货币符号(受Locale影响) | ¤ #,##0.00 |
¥ 1,234.50(中文环境) |
0与#的区别 :0会强制补零,#不会。比如数字1.2,用#.00得到1.20,用#.##则得到1.2。
简单示例
java
import java.text.DecimalFormat;
public class Demo {
public static void main(String[] args) throws ParseException {
double num = 1234567.89;
// 1. 带千分位,保留两位小数
DecimalFormat df1 = new DecimalFormat("#,###.00");
System.out.println(df1.format(num)); // 1,234,567.89
// 2. 百分比(自动乘100)
DecimalFormat df2 = new DecimalFormat("#.##%");
System.out.println(df2.format(0.1234)); // 12.34%
// 3. 货币格式(会使用默认Locale)
DecimalFormat df3 = new DecimalFormat("¤ #,##0.00");
System.out.println(df3.format(num)); // ¥ 1,234,567.89
// 4. 把字符串解析成数字
DecimalFormat df4 = new DecimalFormat("#,###.00");
Number parsed = df4.parse("1,234,567.89");
System.out.println(parsed.doubleValue()); // 1234567.89
}
}
注意事项
DecimalFormat不是线程安全的 ,多线程环境不要共用同一个实例,建议每次新建或使用ThreadLocal。- 解析时如果字符串格式不匹配,会抛出
ParseException。
构造函数
java
public DecimalFormat() {
// Get the pattern for the default locale.
Locale def = Locale.getDefault(Locale.Category.FORMAT);
LocaleProviderAdapter adapter = LocaleProviderAdapter.getAdapter(NumberFormatProvider.class, def);
if (!(adapter instanceof ResourceBundleBasedAdapter)) {
adapter = LocaleProviderAdapter.getResourceBundleBased();
}
String[] all = adapter.getLocaleResources(def).getNumberPatterns();
// Always applyPattern after the symbols are set
this.symbols = DecimalFormatSymbols.getInstance(def);
applyPattern(all[0], false);
}
这段构造函数,本质上是 根据 JVM 的默认语言/区域设置,自动选择一个适合当前环境的数字格式 ,并完成初始化。它让你不传任何参数 new DecimalFormat() 时,就能得到一个"本地化"的数字格式器。
逐行拆解它的逻辑:
1. 确定当前的区域设置
java
Locale def = Locale.getDefault(Locale.Category.FORMAT);
这里不是简单取系统默认区域,而是专门取 FORMAT 类别 的 Locale。
Java 把默认区域分为 DISPLAY(界面显示语言)和 FORMAT(数字、日期等格式化的地区习惯)。比如你可以界面用英文,但数字格式用德文(千分位是 .,小数点是 ,)。这个构造函数明确采用格式化习惯对应的区域 def。
2. 获取本地化数据适配器
java
LocaleProviderAdapter adapter = LocaleProviderAdapter.getAdapter(NumberFormatProvider.class, def);
if (!(adapter instanceof ResourceBundleBasedAdapter)) {
adapter = LocaleProviderAdapter.getResourceBundleBased();
}
这一步是在找"本地化数据源"。
- 先从 JDK 的 SPI 机制获取针对
def区域和NumberFormatProvider的适配器。 - 如果获得的适配器不是基于传统
ResourceBundle的(例如是通过 ICU4J 等 SPI 扩展提供的),则强制回退到 JDK 内置的资源包实现 。
这是为了保证DecimalFormat的模式字符串(pattern)完全来自 JDK 标准资源,避免第三方提供的数据格式与后续符号设置不兼容。
3. 拿到该区域的数字格式模式数组
java
String[] all = adapter.getLocaleResources(def).getNumberPatterns();
从资源中取出 def 区域的所有数字模式。
典型地,这个数组至少包含以下元素(索引从0开始):
all[0]-- 通用数字格式(如"#,##0.###")all[1]-- 货币格式all[2]-- 百分比格式
等等。这里只取索引 0,也就是默认的通用数字模式。
4. 设置格式符号并应用模式
java
this.symbols = DecimalFormatSymbols.getInstance(def);
applyPattern(all[0], false);
DecimalFormatSymbols保存了与区域相关的符号:小数点、千分位分隔符、负号、百分号等。
用def区域获取后,符号就会与该区域习惯一致(例如德国的小数点是逗号)。- 然后调用
applyPattern(all[0], false)把刚刚拿到的模式字符串解析并应用到当前DecimalFormat对象。
第二个参数false表示这是"常规"应用,不是国际化或特定阶段转换。
总结一下这个构造函数的设计意图:
当你写 new DecimalFormat() 时,它不需要你指定任何格式,就会自动:
- 感知你 JVM 的
FORMAT区域设置; - 使用 JDK 内置的、最标准的本地化数字模式;
- 配上对应区域的符号(小数点、分组符等),
从而创建出一个随时可以format()的本地化数字格式器。
这样做的好处是,你可以在不同国家用户的机器上,自然地按当地习惯显示数字,而不用手动拼接 #,###.## 这类模式。
详细用法
DecimalFormat 的类注释写得非常详尽,它不仅说明了这个类是什么,更是一份内置的模式语法手册。结合这段注释,DecimalFormat 可以概括为:一个由"模式字符串"和"本地化符号集"共同驱动的、功能丰富的十进制数字格式化/解析引擎。
下面我按注释的结构,提炼核心知识点,并重点介绍它的高级用法。
1. 注释核心内容解读
设计原则与获取方式
注释第一段就强调:不要直接调用 DecimalFormat 的构造器,而应使用 NumberFormat 的工厂方法 (如 getInstance())。因为工厂方法可能返回其他子类,这样更符合面向接口编程。如果确实需要定制,先获取再向下转型:
java
NumberFormat numFormat = NumberFormat.getInstance(loc);
if (numFormat instanceof DecimalFormat decFormat) {
decFormat.setDecimalSeparatorAlwaysShown(true);
}
模式语法与特殊字符
注释用扩展巴科斯范式给出了完整的模式语法,并附有一张特殊字符表。核心概念:
- 正负子模式 :
正模式;负模式,例如"#,##0.00;(#,##0.00)"。负模式省略时,默认在正模式前加-。 0与#:0强制占位(不足补零),#有数字才显示。E:科学记数法分隔符,后跟最少指数位数(如E0)。%:乘以 100 并显示为百分数。‰(U+2030):乘以 1000 并显示为千分比。¤(U+00A4) :货币符号,单次替换为货币符号,双写¤¤替换为国际货币代码(如CNY)。':用于引用特殊字符,如"'#'#"将 123 格式化为#123。,和.:分组分隔符与小数点,支持本地化。
科学记数法的特殊规则
这是注释中最"高级"的部分之一,它揭示了模式的强大控制力:
- 工程记数法 :如果 最大整数位数 > 最小整数位数 且 > 1 ,则指数会被强制调整为最大整数位数的倍数。
典型模式"##0.#####E0":最大整数位 3,最小整数位 1,指数会被调整为 3 的倍数。
12345→"12.345E3"
123456→"123.456E3" - 普通科学记数法 :其他情况下,通过调整指数来满足最小整数位数。
模式"00.###E0"格式化0.00123→"12.3E-4"。
注释还给出了尾数有效位数的计算公式,非常严谨。
舍入与解析
- 默认舍入模式为
HALF_EVEN(银行家舍入),可通过setRoundingMode修改。 - 解析支持 Unicode 所有十进制数字,并能解析出
BigDecimal、Long或Double(通过setParseBigDecimal等控制)。
2. 高级用法详解(结合注释)
以下用法都源于注释中描述的特性,但往往容易被忽略。
高级用法一:自定义正负格式(会计括号等)
利用正负子模式,可以实现负数用括号括起来、添加 CR/DR 后缀等。
java
DecimalFormat df = new DecimalFormat("#,##0.00;(#,##0.00)");
System.out.println(df.format(1234.5)); // "1,234.50"
System.out.println(df.format(-1234.5)); // "(1,234.50)"
// 自定义前缀后缀
df.applyPattern("#,##0.00;-#,##0.00 DR");
System.out.println(df.format(-100.0)); // "-100.00 DR"
高级用法二:工程记数法与可控科学记数法
如前所述,通过巧妙设置最大/最小整数位数实现工程记数法(指数为 3 的倍数)。
java
DecimalFormat engFormat = new DecimalFormat("##0.#####E0");
System.out.println(engFormat.format(12345)); // "12.345E3"
System.out.println(engFormat.format(0.0012)); // "1.2E-3"
// 固定尾数整数位为1的科学记数法
DecimalFormat sciFormat = new DecimalFormat("0.#####E0");
System.out.println(sciFormat.format(12345)); // "1.2345E4"
高级用法三:货币与国际货币代码
使用 ¤ 符号,可自动适配货币。Double ¤¤ will use the international currency code (e.g., USD, CNY).
java
DecimalFormat money = new DecimalFormat("¤ #,##0.00");
System.out.println(money.format(1234.5)); // ¥ 1,234.50 (取决于Locale)
// 国际货币代码
money.applyPattern("¤¤ #,##0.00");
System.out.println(money.format(1234.5)); // CNY 1,234.50
高级用法四:千分比、百分比与字符串常量
模式中直接包含任意 Unicode 字符作为前后缀,包括千分比符号 ‰。
java
DecimalFormat percent = new DecimalFormat("#.##%");
System.out.println(percent.format(0.1234)); // "12.34%"
DecimalFormat permille = new DecimalFormat("#.##‰");
System.out.println(permille.format(0.01234)); // "12.34‰"
// 引用特殊字符:显示井号前缀
DecimalFormat hashPrefix = new DecimalFormat("'#'#");
System.out.println(hashPrefix.format(123)); // "#123"
高级用法五:完全定制的本地化符号(DecimalFormatSymbols)
模式本身可以本地化(如用 ,, 等本地化分组符),但更高级的定制是通过 DecimalFormatSymbols 来实现,可以替换任意符号。
java
DecimalFormatSymbols symbols = new DecimalFormatSymbols(Locale.US);
symbols.setDecimalSeparator(',');
symbols.setGroupingSeparator('.');
symbols.setMinusSign('−'); // 真正的减号 U+2212
DecimalFormat df = new DecimalFormat("#,##0.00", symbols);
System.out.println(df.format(-1234567.89)); // "−1.234.567,89"
高级用法六:解析控制与 BigDecimal
DecimalFormat 解析能力强大,可以控制是否只解析整数、是否返回 BigDecimal。
java
DecimalFormat df = new DecimalFormat("#,##0.00");
df.setParseBigDecimal(true);
Number num = df.parse("1,234.56");
System.out.println(num.getClass()); // class java.math.BigDecimal
// 只解析整数部分
df.setParseIntegerOnly(true);
System.out.println(df.parse("1,234.56")); // 1234 (Long)
高级用法七:设置舍入模式
结合 RoundingMode 实现不同的舍入策略。
java
DecimalFormat df = new DecimalFormat("#.##");
df.setRoundingMode(RoundingMode.CEILING);
System.out.println(df.format(1.231)); // 1.24
df.setRoundingMode(RoundingMode.FLOOR);
System.out.println(df.format(1.239)); // 1.23
线程安全与最佳实践
注释明确说明:DecimalFormat 不是线程安全的 。多线程必须各自创建实例或使用 ThreadLocal。结合开头的指导,推荐用法:
java
// 线程安全且能自动本地化
NumberFormat nf = NumberFormat.getNumberInstance(Locale.FRANCE);
if (nf instanceof DecimalFormat df) {
df.setMinimumFractionDigits(3);
}
String result = nf.format(1234.5); // 1 234,500
总结来说,这段注释不仅说明了 DecimalFormat 的基础,更揭示了它作为模式驱动的格式化引擎 的灵活性。真正的高级用法都源于对模式语法、科学记数法规则和符号定制的深度理解,而非简单调用 format() 方法。
readObject方法
这段 readObject 方法是 DecimalFormat 的反序列化专用钩子 ,它的核心使命是:当从流中恢复一个已序列化的对象时,处理不同 JDK 版本之间的字段差异,确保旧版数据能正确适配新类定义,同时做合法性校验并重置瞬时状态。
下面逐段结合注释进行详细解释。
方法签名与整体流程
java
private void readObject(ObjectInputStream stream)
throws IOException, ClassNotFoundException
readObject 是 Java 序列化机制的特殊方法。在反序列化时会自动调用,而不是用默认的构造器。
第一步:默认反序列化 + 初始化内部状态
java
stream.defaultReadObject();
digitList = new DigitList();
fastPathCheckNeeded = true;
isFastPath = false;
fastPathData = null;
stream.defaultReadObject();负责从流中恢复所有可序列化字段 的值(例如pattern、serialVersionOnStream等)。digitList = new DigitList();
digitList被标记为transient,不会被序列化,所以反序列化后必须重新创建。- 之后三行是快速格式化路径 的状态重置。
快速路径是一种性能优化,为常见基本类型(如double)的格式化提供捷径。反序列化后状态未知,必须强制要求重新检查并走慢速路径。
第二步:处理 roundingMode 字段(版本兼容)
java
if (serialVersionOnStream < 4) {
setRoundingMode(RoundingMode.HALF_EVEN);
} else {
setRoundingMode(getRoundingMode());
}
注释对应第 2 点 :"如果 serialVersionOnStream < 4,将 roundingMode 初始化为 HALF_EVEN。该字段是版本 4 新增的。"
- 旧版本(<4)没有
roundingMode字段,反序列化后该字段会是默认值,但为了语义明确,这里主动设为HALF_EVEN(银行家舍入),保持旧版行为。 - 如果版本 >=4,该字段是有效写入的,通过
getRoundingMode()读取流中的值,并用setRoundingMode做一次设置以触发可能的边界检查。 - 注意:这里先
defaultReadObject()已经将roundingMode值读入了对象(如果流中有),所以getRoundingMode()能拿到流中的值。setRoundingMode的调用是为了走 setter 逻辑确保一致性。
第三步:校验父类 NumberFormat 的位数限制
java
if (super.getMaximumIntegerDigits() > DOUBLE_INTEGER_DIGITS ||
super.getMaximumFractionDigits() > DOUBLE_FRACTION_DIGITS) {
throw new InvalidObjectException("Digit count out of range");
}
注释对应第 1 点:验证父类中的数字位数限制。
NumberFormat中存储了对double/float等普通数值进行格式化时的最大位数限制(DOUBLE_INTEGER_DIGITS、DOUBLE_FRACTION_DIGITS是DecimalFormat中的常量,分别为 309 和 340)。- 如果父类保存的值超出这些常量,说明流数据损坏或不一致(因为
NumberFormat.readObject本身已有基本检查,这里做DecimalFormat特有的补充检查),防止后续出现奇怪的格式化行为。 - 注释提到:"我们只需检查最大值,因为
NumberFormat.readObject已确保最大值大于最小值"。因此只检查上限。
第四步:迁移父类字段到子类字段(版本 3 兼容)
java
if (serialVersionOnStream < 3) {
setMaximumIntegerDigits(super.getMaximumIntegerDigits());
setMinimumIntegerDigits(super.getMinimumIntegerDigits());
setMaximumFractionDigits(super.getMaximumFractionDigits());
setMinimumFractionDigits(super.getMinimumFractionDigits());
}
注释对应第 3 点 :"如果 serialVersionOnStream < 3,调用子类 setter 用父类 getter 的值初始化本类的字段。这些字段是版本 3 新增的。"
- 在 Java 版本演进中,
DecimalFormat从某个版本开始将这些位数控制字段从父类NumberFormat移到了自己内部(或同时保留)。为了向后兼容,旧序列化数据中这些信息只存在于父类的序列化字段里,而本类自己的这些字段是新字段,未写入流中(为默认值)。 - 因此这里手动将父类中的值复制到子类字段中,保证对象行为与序列化前一致。
第五步:处理 useExponentialNotation 字段(版本 1 兼容)
java
if (serialVersionOnStream < 1) {
useExponentialNotation = false;
}
注释对应第 4 点 :"如果 serialVersionOnStream < 1(表示流是由 JDK 1.1 写的),则将 useExponentialNotation 初始化为 false,因为它在 JDK 1.1 中不存在。"
- JDK 1.1 的早期版本中,
DecimalFormat没有单独的useExponentialNotation开关(科学记数法是通过模式中的E自动启用的)。 - 反序列化后该字段默认为
false,但为了明确和安全,显式赋值为false。
第六步:修复非法分组大小
java
if (groupingSize < 0) {
groupingSize = 3;
}
- 序列化数据可能由于早期版本或手动修改导致
groupingSize为负数,此时将其重置为默认值 3(千位分组),保证行为合理。
第七步:更新 serialVersionOnStream 到当前版本
java
serialVersionOnStream = currentSerialVersion;
注释对应第 5 点 :"将 serialVersionOnStream 设为允许的最大值,这样如果该对象再次被写出,默认序列化能正常工作。"
serialVersionOnStream记录的是当初写出时的版本号。对象被反序列化后,它在内存中已经适配为最新结构。如果稍后再次序列化这个对象,应该用当前最新版本号写出,所以需要更新。currentSerialVersion是DecimalFormat中定义的一个常量,表示当前类的序列化版本(现在通常是 4)。
注释第 5 点外的补充说明(版本 2 前的 affix 模式)
注释中有一段文字:
"Stream versions older than 2 will not have the affix pattern variables
posPrefixPatternetc. As a result, they will be initialized tonull, which means the affix strings will be taken as literal values. This is exactly what we want, since that corresponds to the pre-version-2 behavior."
- 在版本 2 之前,没有显式的
posPrefixPattern、posSuffixPattern等字段。反序列化后这些字段为null。 - 当这些字段为
null时,DecimalFormat的代码逻辑会直接使用字面量前后缀(positivePrefix等)作为格式化的依据。这正是旧版本的行为,因此无需额外处理,符合向后兼容。
总结
整个 readObject 方法本质上是一个序列化版本适配器 ,它用非常清晰的步骤化解了 DecimalFormat 在多个 JDK 版本演进中字段增删改带来的不兼容问题:
- 将缺失的字段(
roundingMode、位数字段、useExponentialNotation)补上合理的默认值。 - 将位置迁移的字段(从父类搬到子类)重新同步。
- 校验关键数据合法性,防御流损坏。
- 重置瞬时状态(
digitList、快速路径标志),保证对象立即可用。 - 更新版本号,为可能的再序列化做准备。
这保证了你用旧版 JDK 序列化的 DecimalFormat 对象,在新版 JVM 中能无差地反序列化,并且行为与其原始表现一致。