bigywater.tech
Java · 351

DecimalFormat:数字格式化的 Locale 与线程安全

数字格式化看起来简单,却同时牵扯 Locale、舍入、解析宽松度和线程安全。这篇逐项实测,尤其是共用一个 DecimalFormat 时哪些操作出错、哪些没有。

1.005 → 1.00 / 1.01
double 与 BigDecimal 输入,HALF_UP 下两位小数的结果
292186 / 800000
8 线程共用一个 DecimalFormat 做 parse 时结果错误的次数(format 为 0)
"12abc" → 12
NumberFormat.parse 不会因为尾部有多余字符而报错

模式语法

实测 · 本机执行输出
  [pattern]
    "#"                    1234568 | 0 | -42 | 0 | 1234 |
    "0"                    1234568 | 0 | -42 | 0 | 1234 |
    "#.##"                 1234567.89 | 0.5 | -42.1 | 0 | 1234.5 |
    "0.00"                 1234567.89 | 0.50 | -42.10 | 0.00 | 1234.50 |
    "#,##0.00"             1,234,567.89 | 0.50 | -42.10 | 0.00 | 1,234.50 |
    "000000"               1234568 | 000000 | -000042 | 000000 | 001234 |
    "#,###"                1,234,568 | 0 | -42 | 0 | 1,234 |
    "00.0"                 1234567.9 | 00.5 | -42.1 | 00.0 | 1234.5 |
    "#.00"                 1234567.89 | .50 | -42.10 | .00 | 1234.50 |
    "0.###E0"              1.235E6 | 5E-1 | -4.21E1 | 0E0 | 1.234E3 |
    "##0.#####E0"          1.2345679E6 | 500E-3 | -42.1E0 | 0E0 | 1.2345E3 |
    "0.00%"                123456789.10% | 50.00% | -4210.00% | 0.00% | 123450.00% |
    "0.0‰"                 1234567891.0‰ | 500.0‰ | -42100.0‰ | 0.0‰ | 1234500.0‰ |
    "#,##0.00;(#,##0.00)"  1,234,567.89 | 0.50 | (42.10) | 0.00 | 1,234.50 |
    "'编号'0000"             编号1234568 | 编号0000 | -编号0042 | 编号0000 | 编号1234 |
    "$#,##0.00"            $1,234,567.89 | $0.50 | -$42.10 | $0.00 | $1,234.50 |
    "#,##,##0.00"          1,234,567.89 | 0.50 | -42.10 | 0.00 | 1,234.50 |
    (五列依次是 1234567.891、0.5、-42.1、0、1234.5)
    NumberFormat.getInstance(Locale.US) 的默认:最多 3 位小数,分组 true;toPattern = #,##0.###
    NumberFormat.getIntegerInstance 的舍入:2.5 → 2,3.5 → 4;getPercentInstance 0.256 → 26%;getCurrencyInstance 1234.5 → $1,234.50
    # 与 0 的区别:#.## 对 5 → 5,0.## 对 5 → 5;#.00 对 0.5 → .50,0.00 对 0.5 → 0.50

舍入

实测 · 本机执行输出
  [round]
    默认舍入模式:HALF_EVEN
        0 位小数   0.5 → HALF_EVEN 0 | HALF_UP 1 | HALF_DOWN 0 | UP 1 | DOWN 0 | CEILING 1 | FLOOR 0
        0 位小数   1.5 → HALF_EVEN 2 | HALF_UP 2 | HALF_DOWN 1 | UP 2 | DOWN 1 | CEILING 2 | FLOOR 1
        0 位小数   2.5 → HALF_EVEN 2 | HALF_UP 3 | HALF_DOWN 2 | UP 3 | DOWN 2 | CEILING 3 | FLOOR 2
        0 位小数   3.5 → HALF_EVEN 4 | HALF_UP 4 | HALF_DOWN 3 | UP 4 | DOWN 3 | CEILING 4 | FLOOR 3
        0 位小数  -2.5 → HALF_EVEN -2 | HALF_UP -3 | HALF_DOWN -2 | UP -3 | DOWN -2 | CEILING -2 | FLOOR -3
        两位小数 0.125  精确值 0.125                      → HALF_EVEN 0.12 | HALF_UP 0.13
        两位小数 0.135  精确值 0.135000000000000008881784 → HALF_EVEN 0.14 | HALF_UP 0.14
        两位小数 1.005  精确值 1.004999999999999893418589 → HALF_EVEN 1.00 | HALF_UP 1.00
        两位小数 2.675  精确值 2.674999999999999822364316 → HALF_EVEN 2.67 | HALF_UP 2.67
        两位小数 0.285  精确值 0.284999999999999975575093 → HALF_EVEN 0.28 | HALF_UP 0.28
        两位小数 1.115  精确值 1.114999999999999991118215 → HALF_EVEN 1.11 | HALF_UP 1.11
    BigDecimal 输入(HALF_UP):1.005 → 1.01,2.675 → 2.68;double 输入:1.005 → 1.00,2.675 → 2.67
    0.1 + 0.2 = 0.30000000000000004;format("0.################") → 0.3;BigDecimal 版 → 0.3
    format(Long.MAX_VALUE) = 9,223,372,036,854,775,807;format(1e20 double,0.###) = 100,000,000,000,000,000,000;format(123456789.123456789 double) = 123,456,789.123
    -0.0001 用 0.00 → -0.00(负零);0.00 对 -0.0 → -0.00

parse 的宽松

实测 · 本机执行输出
  [parse]
        parse("12"    ) → 12       Long     消耗 2 / 2 个字符
        parse("12abc" ) → 12       Long     消耗 2 / 5 个字符
        parse(" 12"   ) → null     -        消耗 0 / 3 个字符(errorIndex=0)
        parse("12 "   ) → 12       Long     消耗 2 / 3 个字符
        parse("1,234" ) → 1234     Long     消耗 5 / 5 个字符
        parse("1,2,3" ) → 123      Long     消耗 5 / 5 个字符
        parse("1.5.5" ) → 1.5      Double   消耗 3 / 5 个字符
        parse("1e3"   ) → 1        Long     消耗 1 / 3 个字符
        parse("-5"    ) → -5       Long     消耗 2 / 2 个字符
        parse("+5"    ) → null     -        消耗 0 / 2 个字符(errorIndex=0)
        parse("(5)"   ) → null     -        消耗 0 / 3 个字符(errorIndex=0)
        parse(""      ) → null     -        消耗 0 / 0 个字符(errorIndex=0)
        parse("abc"   ) → null     -        消耗 0 / 3 个字符(errorIndex=0)
        parse("12,"   ) → 12       Long     消耗 2 / 3 个字符
        parse(".5"    ) → 0.5      Double   消耗 2 / 2 个字符
        parse("٣٤"    ) → 34       Long     消耗 2 / 2 个字符
        parse("123"   ) → 123      Long     消耗 3 / 3 个字符
        parse("1_000" ) → 1        Long     消耗 1 / 5 个字符
        parse("NaN"   ) → NaN      Double   消耗 3 / 3 个字符
        parse("∞"     ) → Infinity Double   消耗 1 / 1 个字符
    nf.parse("abc") 抛 ParseException: Unparseable number: "abc",offset=0
    nf.parse("12abc") 不抛异常,返回 12——要整串匹配必须自己检查 ParsePosition.getIndex() == length
    默认 parse 返回:Double;parse("1234") 返回 Long;parse("12345678901234567890") 返回 1.2345678901234567E19(Double)
    setParseBigDecimal(true):parse("12345678901234567890.123456789") → 12345678901234567890.123456789(BigDecimal)
    负数子模式 (…):format(-1234.5) = (1,234.50);parse("(1,234.50)") = -1234.5;parse("-1,234.50") = ParseException(offset 0)

Locale

实测 · 本机执行输出
  [locale]
        en_US  1,234,567.891            货币 $1,234,567.89      小数点 .(U+002E) 分组 ,(U+002C)
        de_DE  1.234.567,891            货币 1.234.567,89 €     小数点 ,(U+002C) 分组 .(U+002E)
        fr_FR  1 234 567,891            货币 1 234 567,89 €     小数点 ,(U+002C) 分组  (U+202F)
        de_CH  1’234’567.891            货币 CHF 1’234’567.89   小数点 .(U+002E) 分组 ’(U+2019)
        en_IN  1,234,567.891            货币 ₹1,234,567.89      小数点 .(U+002E) 分组 ,(U+002C)
        hi_IN  1,234,567.891            货币 ₹1,234,567.89      小数点 .(U+002E) 分组 ,(U+002C)
        zh_CN  1,234,567.891            货币 ¥1,234,567.89      小数点 .(U+002E) 分组 ,(U+002C)
        ja_JP  1,234,567.891            货币 ¥1,234,568         小数点 .(U+002E) 分组 ,(U+002C)
        ar_EG  ١٬٢٣٤٬٥٦٧٫٨٩١            货币 ‏١٬٢٣٤٬٥٦٧٫٨٩ ج.م.‏ 小数点 ٫(U+066B) 分组 ٬(U+066C)
        fa_IR  ۱٬۲۳۴٬۵۶۷٫۸۹۱            货币 ‎ریال۱٬۲۳۴٬۵۶۷٫۸۹  小数点 ٫(U+066B) 分组 ٬(U+066C)
    解析的 Locale 错位:
        de_DE 解析 "1.234"      → 1234    (消耗 5 / 5)
        de_DE 解析 "1,234"      → 1.234   (消耗 5 / 5)
        en_US 解析 "1.234,56"   → 1.234   (消耗 5 / 8)
        en_US 解析 "1,234.56"   → 1234.56 (消耗 8 / 8)
        de_DE 解析 "1.234,56"   → 1234.56 (消耗 8 / 8)
        fr_FR 解析 "1 234,56"   → 1       (消耗 1 / 8)
        fr_FR 解析 "1 234,56"   → 1234.56 (消耗 8 / 8)
    NaN 与无穷的符号:NaN / ∞;format(NaN) = NaN;format(+∞) = ∞;德语 NaN = NaN
    货币 Currency.getInstance(Locale.JAPAN) 小数位 = 0;USD = 2;JOD(约旦第纳尔)= 3;CLF = 4

紧凑数字格式

实测 · 本机执行输出
  [compact]
        en_US  SHORT 999 | 1K | 1K | 12K | 1M | 1M | 2M | 123M | 1T |
        en_US  LONG  999 | 1 thousand | 1 thousand | 12 thousand | 1 million | 1 million | 2 million | 123 million | 1 trillion |
        zh_CN  SHORT 999 | 1,000 | 1,234 | 1万 | 100万 | 100万 | 150万 | 1亿 | 1万亿 |
        zh_CN  LONG  999 | 1,000 | 1,234 | 1万 | 100万 | 100万 | 150万 | 1亿 | 1万亿 |
        de_DE  SHORT 999 | 1.000 | 1.234 | 12.345 | 999.999 | 1 Mio. | 2 Mio. | 123 Mio. | 1 Bio. |
        de_DE  LONG  999 | 1 Tausend | 1 Tausend | 12 Tausend | 1 Million | 1 Million | 2 Million | 123 Millionen | 1 Billion |
        ja_JP  SHORT 999 | 1,000 | 1,234 | 1万 | 100万 | 100万 | 150万 | 1億 | 1兆 |
        ja_JP  LONG  999 | 1,000 | 1,234 | 1万 | 100万 | 100万 | 150万 | 1億 | 1兆 |
    (依次是 999、1000、1234、12345、999999、1000000、1500000、123456789、1234567890123)
    US SHORT,setMaximumFractionDigits(1):1234 → 1.2K,1500000 → 1.5M,999999 → 1M,1050 → 1.1K,1950 → 1.9K
    setRoundingMode(DOWN):999999 → 999.9K,1950 → 1.9K
    parse:1500000 / 12000 / 15000

线程安全

实测 · 本机执行输出
  [thread]
    8 个线程共用同一个 DecimalFormat 各 100000 次(共 800000 次):结果不对 0 次,抛异常 0 次;例:null
    共用同一个 DecimalFormat,输入类型 BigDecimal,8 线程各 100000 次:结果不对 0 次,抛异常 0 次;例:null
    共用同一个 DecimalFormat,输入类型 long,8 线程各 100000 次:结果不对 0 次,抛异常 0 次;例:null
    共用同一个 DecimalFormat,输入类型 double(带负数子模式),8 线程各 100000 次:结果不对 0 次,抛异常 0 次;例:null
    共用同一个 DecimalFormat 做 parse(setParseBigDecimal=false),8 线程各 100000 次:结果不对 292186 次,抛异常 12541 次;例:7204.25 (期望 7004.25)
    共用同一个 DecimalFormat 做 parse(setParseBigDecimal=true),8 线程各 100000 次:结果不对 274181 次,抛异常 0 次;例:1014.25 (期望 1022.25)
    一个线程不断 setMinimum/MaximumFractionDigits(0..3),4 个线程格式化 1234.5678:出现的不同结果 [, ., .568, 1,234.568, 1,234.57, 1,234.6, 1,235, 1,235.];异常 1318 次(java.lang.NullPointerException: Cannot assign field "fastPathContainer" beca
    每线程一个 ThreadLocal<DecimalFormat>:结果不对 0 次
    第 4 轮:复用实例 226 ns;每次 new DecimalFormat 573 ns;每次 NumberFormat.getInstance 318 ns
    第 5 轮:复用实例 237 ns;每次 new DecimalFormat 637 ns;每次 NumberFormat.getInstance 353 ns
    (自写计时循环,不是 JMH;sink=true)

分步演示

DecimalFormat
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12,CLDR 区域数据;最后一节的计时是自写循环,不是 JMH
    • 没有验证:format 在共用实例上 0 次出错的原因(没有读源码,只在 4 种输入各 80 万次、一次实验里偶发 1 次的 NullPointerException 之后没有再复现——该偶发异常的触发条件没有查明,所以不能据此说 format 线程安全,官方文档说它不同步)、JDK 17 / 27 上的相同行为、BigDecimal 大数格式化的性能、DecimalFormat 的 setGroupingSize 与印度式分组的实际写法(这里只发现 “#,##,##0.00” 分组仍是三位)、Locale 的 -u-nu 扩展对数字的影响
    京ICP备2026067502号-1京公网安备11010502063487号