0.1+0.2的经典结果0.30000000000000004,很多人以为这是"完整展开的二进制误差"——这篇真实用BigDecimal对比double的精确二进制值和toString()结果,验证这套算法到底在做什么。
上一篇:EnumSet内部原理真实验证
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值的最短边界。
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
每一行都是真实运行这段代码得到的结果。