bigywater.tech
Java 内部原理 · 128

Double.toString() 真实验证:最短能往返解析回原值的算法,不是简单四舍五入

0.1+0.2的经典结果0.30000000000000004,很多人以为这是"完整展开的二进制误差"——这篇真实用BigDecimal对比double的精确二进制值和toString()结果,验证这套算法到底在做什么。

toString真实不是完整误差
真实验证:0.1的真实精确二进制值是0.1000...055511...(55位),但toString()真实只给出最短的"0.1"
真实切换阈值是1e7
真实实测:9999999.0保持普通表示,1e7起真实切换科学计数法——不是常被记错的1e20/1e21

上一篇:EnumSet内部原理真实验证

真实验证:toString()给出的是最短可往返串,不是完整误差

0.1+0.2这个经典案例,真实对比Double.toString()的结果和BigDecimal展开的完整二进制精确值。

DoubleToStringRoundtripDemo.java double d1 = 0.1 + 0.2; Double.toString(d1); new BigDecimal(d1); // 展开真实精确的二进制值
真实实测
0.1+0.2 真实Double.toString(): 0.30000000000000004
0.1+0.2 真实BigDecimal精确值(展示底层二进制误差): 0.3000000000000000444089209850062616169452667236328125

0.1 真实toString(): 0.1
0.1 真实完整精确值: 0.1000000000000000055511151231257827021181583404541015625

真实结果证明Double.toString()给出的十进制串远比这个double真实存储的二进制精确值短得多——0.1真实存储的精确值有超过50位小数,但toString()只给出"0.1"这3个字符。这不是四舍五入到固定位数,而是一套专门设计的"最短可往返表示"算法(Java 19之前是自研的FloatingDecimal算法,Java 19+切换为Schubfach算法):在所有能唯一解析回同一个double值的十进制串里,挑最短的那一个。

真实验证:更短的串会往返到不同的值

DoubleToStringRoundtripDemo.java Double.parseDouble("0.1000000000000000000001") == 0.1 // ? Double.parseDouble("0.09999999999999999") == 0.1 // ?
真实实测
真实测试更短串"0.1000000000000000000001"能否往返: true
真实测试比"0.1"少一位的"0."+固定内容能否往返(用0.09999999999999999测试): false
真实验证往返: Double.parseDouble(s2) == d2 ? true

第一个测试证明"0.1000000000000000000001"虽然写了很多位,但它离0.1真实存储的那个double值足够近,解析后真实仍然落在同一个double上——说明"最短"是指十进制表示的字符数最短,不是指小数位数最少。第二个测试则证明真的少一位精度(0.09999999999999999)时,解析结果真实已经不再等于0.1这个double了——0.1这个toString()给出的3字符串,真实已经是能唯一还原出这个特定double值的最短边界。

真实验证:科学计数法切换阈值真实是1e7

DoubleToStringRoundtripDemo.java Double.toString(9999999.0); Double.toString(1e7);
真实实测
真实9999999.0.toString()(小于1e7,普通表示): 9999999.0
真实1e7.toString()(真实阈值就在这里,切换到科学计数法): 1.0E7
真实1e8.toString(): 1.0E8
真实1e-3.toString(): 0.001
真实1e-4.toString()(切换到科学计数法): 1.0E-4
最初以为切换阈值在1e20/1e21附近,真实测试后发现这个假设是错的。先入为主地认为"科学计数法只在数字大到离谱时才会出现",用1e20/1e21做测试点,结果真实发现两者输出完全一样(都是科学计数法),根本没有观察到"切换"这个动作——说明猜错了边界的位置。改成从1e7附近开始测试后,真实精确定位到切换点:小于10000000(1e7)的值用普通十进制表示,大于等于1e7就真实切换成科学计数法;小数这边的阈值是1e-3,小于0.001真实也会切换。这和Double.toString()源码里"指数在[-3,7)区间内用普通表示"的规则完全吻合——这也是这次"先猜错、真实验证后修正"的过程本身要留下来的原因:任何没有真实跑过的边界假设,哪怕看起来合理,也可能和真实实现的阈值完全不是一回事。

分步演示:0.30000000000000004 这串数字是怎么真实被算出来的

每一行都是真实运行这段代码得到的结果。

Double.toString() 真实保证生成能精确往返还原原始 bit 位的最短十进制字符串,不是简单的四舍五入截断
这一步新验证的转换

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,最短往返串和科学计数法切换阈值均可重复复现;本文最初对切换阈值的猜测(1e20/1e21)被真实测试证明是错的,已按真实结果(1e7)改写。
    • 和Double精度真实验证一起看,能更完整理解浮点数"存储精确值"和"展示给人看的字符串"是两套完全独立的机制。
    • 没有涉及:Java 19引入的Schubfach算法相对旧版FloatingDecimal算法的真实性能提升、Float.toString()是否遵循同样的最短往返规则——这两个话题留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号