DecimalFormat

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. 工程记数法 :如果 最大整数位数 > 最小整数位数 且 > 1 ,则指数会被强制调整为最大整数位数的倍数。
    典型模式 "##0.#####E0":最大整数位 3,最小整数位 1,指数会被调整为 3 的倍数。
    12345"12.345E3"
    123456"123.456E3"
  2. 普通科学记数法 :其他情况下,通过调整指数来满足最小整数位数。
    模式 "00.###E0" 格式化 0.00123"12.3E-4"

注释还给出了尾数有效位数的计算公式,非常严谨。

舍入与解析
  • 默认舍入模式为 HALF_EVEN(银行家舍入),可通过 setRoundingMode 修改。
  • 解析支持 Unicode 所有十进制数字,并能解析出 BigDecimalLongDouble(通过 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(); 负责从流中恢复所有可序列化字段 的值(例如 patternserialVersionOnStream 等)。
  • 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_DIGITSDOUBLE_FRACTION_DIGITSDecimalFormat 中的常量,分别为 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 记录的是当初写出时的版本号。对象被反序列化后,它在内存中已经适配为最新结构。如果稍后再次序列化这个对象,应该用当前最新版本号写出,所以需要更新。
  • currentSerialVersionDecimalFormat 中定义的一个常量,表示当前类的序列化版本(现在通常是 4)。

注释第 5 点外的补充说明(版本 2 前的 affix 模式)

注释中有一段文字:

"Stream versions older than 2 will not have the affix pattern variables posPrefixPattern etc. As a result, they will be initialized to null, 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 之前,没有显式的 posPrefixPatternposSuffixPattern 等字段。反序列化后这些字段为 null
  • 当这些字段为 null 时,DecimalFormat 的代码逻辑会直接使用字面量前后缀(positivePrefix 等)作为格式化的依据。这正是旧版本的行为,因此无需额外处理,符合向后兼容。

总结

整个 readObject 方法本质上是一个序列化版本适配器 ,它用非常清晰的步骤化解了 DecimalFormat 在多个 JDK 版本演进中字段增删改带来的不兼容问题:

  1. 将缺失的字段(roundingMode、位数字段、useExponentialNotation)补上合理的默认值。
  2. 将位置迁移的字段(从父类搬到子类)重新同步。
  3. 校验关键数据合法性,防御流损坏。
  4. 重置瞬时状态(digitList、快速路径标志),保证对象立即可用。
  5. 更新版本号,为可能的再序列化做准备。

这保证了你用旧版 JDK 序列化的 DecimalFormat 对象,在新版 JVM 中能无差地反序列化,并且行为与其原始表现一致。

相关推荐
2303_821287381 小时前
SQL如何进行分组后字符串拼接_使用GROUP_CONCAT或STRING_AGG
jvm·数据库·python
小哈蒙德1 小时前
基于deepSeekV4Pro(thinking)研究pointPillar的历程
python·算法
Nontee1 小时前
一、Java 基础 面试题解答(72题)
java·开发语言
weixin_459753941 小时前
CSS文本渲染在不同操作系统差异_使用font-smoothing平滑化
jvm·数据库·python
兰令水1 小时前
topcode【随机算法题】【2026.5.16打卡-java版本】
java·数据结构·算法
摇滚侠1 小时前
SpringBoot 面试题 真正的 offer 偏方 Java 基础 Java 高级
java·spring boot·后端
NashSKY1 小时前
关于支持向量机(SVM)的数学原理、参数拟合、嵌入式部署的完整指南
c++·python·机器学习·支持向量机
会开花的二叉树1 小时前
Qt信号槽这套机制
开发语言·qt
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第58题】【JVM篇】第18题:讲一下三色标记
java·开发语言·jvm