1. 概述

本文将深入探讨 DecimalFormat 类及其实际应用场景。作为 NumberFormat 的子类,它允许我们通过预定义模式格式化数字的字符串表示,也能反向将字符串解析为数字。

2. 工作原理

格式化数字的核心是定义模式——由特殊字符和文本组成的序列。关键特殊字符包括:

字符 作用
0 有数字则显示,无则补零
# 有数字则显示,无则留空
. 指定小数点位置
, 指定分组分隔符位置

模式应用时,会根据 JVM 的 Locale 默认规则执行格式化(除非显式指定 Locale)。以下示例均基于英文 Locale 环境。

3. 基础格式化

3.1 简单小数

double d = 1234567.89;    
assertThat(
  new DecimalFormat("#.##").format(d)).isEqualTo("1234567.89");
assertThat(
  new DecimalFormat("0.00").format(d)).isEqualTo("1234567.89");

关键点:整数部分永远不会被丢弃,即使模式比数字短:

assertThat(new DecimalFormat("#########.###").format(d))
  .isEqualTo("1234567.89");
assertThat(new DecimalFormat("000000000.000").format(d))
  .isEqualTo("001234567.890");

当模式比数字长时:

  • 0 会强制补零
  • # 会自动舍弃

3.2 舍入处理

当模式的小数位不足时,会触发舍入:

assertThat(new DecimalFormat("#.#").format(d))
  .isEqualTo("1234567.9"); // 0.89 → 0.9
assertThat(new DecimalFormat("#").format(d))
  .isEqualTo("1234568");   // 0.89 → 1.0 → 整数+1

⚠️ 默认使用 HALF_EVEN 舍入模式,可通过 setRoundingMode() 自定义。

3.3 分组分隔

分组分隔符会自动重复应用:

assertThat(new DecimalFormat("#,###.#").format(d))
  .isEqualTo("1,234,567.9");
assertThat(new DecimalFormat("#,###").format(d))
  .isEqualTo("1,234,568");

3.4 多重分组模式

某些国家(如印度)使用变长分组模式(如 #,##,###.##)。但 DecimalFormat 不支持这种复杂规则——它只保留最右侧的分组模式并全局应用。

❌ 踩坑:尝试使用 #,##,##,##,### 会被简化为 #,###,###,###。解决方案:

  • 自定义字符串处理逻辑
  • 使用 ICU4J 的 DecimalFormat

3.5 混合文本

可在模式中嵌入文本:

assertThat(new DecimalFormat("The # number")
  .format(d))
  .isEqualTo("The 1234568 number");

特殊字符需转义:

assertThat(new DecimalFormat("The '#' # number")
  .format(d))
  .isEqualTo("The # 1234568 number");

4. 本地化格式

不同地区的数字符号差异显著(如意大利用 . 作分组符,, 作小数点)。强制指定 Locale 可避免 JVM 默认设置干扰:

assertThat(new DecimalFormat("#,###.##", 
  new DecimalFormatSymbols(Locale.ENGLISH)).format(d))
  .isEqualTo("1,234,567.89");
assertThat(new DecimalFormat("#,###.##", 
  new DecimalFormatSymbols(Locale.ITALIAN)).format(d))
  .isEqualTo("1.234.567,89");

自定义 Locale 示例:

Locale customLocale = new Locale("it", "IT");
assertThat(new DecimalFormat(
  "#,###.##", 
   DecimalFormatSymbols.getInstance(customLocale)).format(d))
  .isEqualTo("1.234.567,89");

5. 科学计数法

5.1 E 计数法

使用 E 表示十的指数:

assertThat(new DecimalFormat("00.#######E0").format(d))
  .isEqualTo("12.3456789E5");
assertThat(new DecimalFormat("000.000000E0").format(d))
  .isEqualTo("123.456789E4");

⚠️ 指数位数很重要:10^12 需用 E00 而非 E0

5.2 工程计数法

工程计数法要求指数是 3 的倍数(如 Kilo/Mega/Giga)。关键技巧:设置整数部分最大位数 > 最小位数且 > 1:

assertThat(new DecimalFormat("##0.######E0")
  .format(d)).isEqualTo("1.23456789E6");        
assertThat(new DecimalFormat("###.000000E0")
  .format(d)).isEqualTo("1.23456789E6");

6. 字符串解析

通过 parse() 将字符串转为数字:

assertThat(new DecimalFormat("", new DecimalFormatSymbols(Locale.ENGLISH))
  .parse("1234567.89"))
  .isEqualTo(1234567.89);
assertThat(new DecimalFormat("", new DecimalFormatSymbols(Locale.ITALIAN))
  .parse("1.234.567,89"))
  .isEqualTo(1234567.89);

强制输出特定类型:

Number result = nf.parse("123.45");
double d = result.doubleValue(); // 转为 double

获取 BigDecimal

NumberFormat nf = new DecimalFormat(
  "", 
  new DecimalFormatSymbols(Locale.ENGLISH));
((DecimalFormat) nf).setParseBigDecimal(true);
 
assertThat(nf.parse("1234567.89"))
  .isEqualTo(BigDecimal.valueOf(1234567.89));

7. 线程安全

⚠️ 重要DecimalFormat 非线程安全!多线程共享实例时需同步处理或使用 ThreadLocal

8. 总结

本文系统梳理了 DecimalFormat 的核心用法、优势与局限。掌握这些技巧,能让你在数字格式化场景中游刃有余。完整代码示例请参考 GitHub 仓库


原始标题:A Practical Guide to DecimalFormat