阿里妹导读:最新发布的《Java开发手册(嵩山版)》增加了前后端规约,其中有一条:禁止服务端在超大整数下使用Long类型作为返回。这是为何?在实际开发中可能出现什么问题?本文从IEEE754浮点数标准讲起,详细解析背后的原理,帮助大家彻底理解这个问题,提前避坑。
文末福利:下载《Java开发手册(嵩山版)灵魂15问》电子书。
8月3日,这个在我等码农心中具有一定纪念意义的日子里,《Java开发手册》发布了嵩山版。每次发布我都特别期待,因为总能找到一些程序员不得不重视的“血淋淋的巨坑”。比如这次,嵩山版中新增的模块——前后端规约,其中一条禁止服务端在超大整数下使用Long类型作为返回。![bc1eaacc909baea683255ae8a856f805.png](https://i-blog.csdnimg.cn/blog_migrate/4613231d2688936a8fbc75e08a38109b.png)
一问:JS的Number类型能安全表达的最大整型数值是多少?为什么(注意要求更严,是安全表达)?
二问:在Long取值范围内,2的指数次整数转换为JS的Number类型,不会有精度丢失,但能放心使用么?
三问:我们一般都知道十进制数转二进制浮点数有可能会出现精度丢失,但精度丢失具体怎么发生的?
四问:如果不幸中招,服务端正在使用Long类型作为大整数的返回,有哪些办法解决?
![0d9f3f26b5f33ae9fc0692f317976559.png](https://i-blog.csdnimg.cn/blog_migrate/06705a53769ea05cafe5bd5600e7dbb5.png)
(-1)^s*(1+m/2^52)*2^(E-1023)
其中s为符号,m为尾数,E为阶码。
比如上图中的0.7 :
1)符号位:是0,代表正数。
2)指数位:01111111110,转换为十进制,得阶码E为1022,则真值e=1022-1023=-1。
3)有效数字:
0110011001100110011001100110011001100110011001100110
转换为十进制,尾数m为:1801439850948198。
4)计算结果:
(1+1801439850948198/2^52)*(2^-1) =0.6999999999999999555910790149937383830547332763671875
经过显示优化算法后(在后文中详述),为0.7。
2 非规格化 指数位是全零时,有效数字最高位前默认为0。那么,转十进制计算公式:(-1)^s*(0+m/2^52)*2^(-1022)
注意,指数位是-1022,而不是-1023,这是为了平滑有效数字最高位前没有1。比如非规格最小正值为:
0x0.0000000000001*2^-1022=2^-52 * 2^-1022 = 4.9*10^-324
3 特殊值
指数全为1,有效数字全为0时,代表无穷大;有效数字不为0时,代表NaN(不是数字)。
问题解答
1 JS的Number类型能安全表达的最大整型数值是多少?为什么?
规约中已经指出:
在Long类型能表示的最大值是2的63次方-1,在取值范围之内,超过2的53次方(9007199254740992)的数值转化为JS的Number时,有些数值会有精度损失。“2的53次方”这个限制是怎么来的呢?如果看懂上文IEEE754基础回顾,不难得出:在浮点数规格化下,双精度浮点数的有效数字有52位,加上有效数字最高位前默认为1,共53位,所以JS的Number能保障无精度损失表达的最大整数是2的53次方。 而这里的题问是:“能安全表达的最大整型”,安全表达的要求,除了能准确表达,还有正确比较。2^53=9007199254740992,实际上,
9007199254740992+1 == 9007199254740992
的比较结果为true。如下图所示:
![4125b8dd34c0e1fedbda9a9ceead5d6c.png](https://i-blog.csdnimg.cn/blog_migrate/a5cf7ba7dff2b42653996e21244a18dc.png)
在Long取值范围内,任何2的指数次整数都是绝对不会存在精度损失的,所以说精度损失是一个概率问题。若浮点数尾数位与指数位空间不限,则可以精确表示任何整数。后半句,我们就不说了,因为绝对没毛病,空间不限,不仅是任何整数可以精确表示,无理数我们也可以挑战一下。我们重点看前半句,根据本文前面所述基础回顾,双精度浮点数的指数取值范围为[-1022,1023],而指数是以2为底数。另外,双精度浮点数的取值范围,比Long大,所以,理论上Long型变量中2的指数次整数一定可以准确转换为JS的umber类型。但在JS中,实际情况,却是下面这样:
![ab6500dc8476a6341f92b7250c54fae1.png](https://i-blog.csdnimg.cn/blog_migrate/195366961ada568811af9644b3816982.png)
![90ae8c8f3a54b964978d634376f4e270.png](https://i-blog.csdnimg.cn/blog_migrate/59603fcb3d6bb5c678fe798babf63a0c.png)
![4a9ed5be200a9a2cf2c3639b68df38f5.png](https://i-blog.csdnimg.cn/blog_migrate/7b147ada4a23dbcdf237bbf83ad2a7f6.png)
![2bd220347a3cf7e57c688c12c3051bb7.png](https://i-blog.csdnimg.cn/blog_migrate/3c75a7618d27951824bd1641d911eba1.png)
![5addf0501b5efb27c7bce89374710620.png](https://i-blog.csdnimg.cn/blog_migrate/e0d660322701d8bed6ffc7353ee861ad.png)
0.0001 10011001100110011001100110011...
其中0011在循环。将0.1转换为双精度浮点数二进制存储为:
0 01111111011 1001100110011001100110011001100110011001100110011001
按照本文前面所述基础回顾中的计算公式
(-1)^s*(1+m/2^52)*2^(E-1023)计算,可得
转换回十进制为:
0.09999999999999999。
这里可以看出,浮点数有时是无法精确表达一个自然数,这个和十进制中1/3 =0.333333333333333...是一个道理。
2)转换结果长度,超过有效数字位数,超过部分会被舍弃
IEEE754默认是舍入到最近的值,如果“舍”和“入”一样接近,那么取结果为偶数的选择。
另外,在浮点数计算过程中,也可能引起精度丢失。比如,浮点数加减运算执行步骤分为:
零值检测 -> 对阶操作 -> 尾数求和 -> 结果规格化 -> 结果舍入
其中对阶和规格化都有可能造成精度损失:
对阶:是通过尾数右移(左移会导致高位被移出,误差更大,所以只能是右移),将小指数改成大指数,达到指数阶码对齐的效果,而右移出的位,会作为保护位暂存,在结果舍入中处理,这一步有可能导致精度丢失。
规格化:是为了保障计算结果的尾数最高位是1,视情况有可能会出现右规,即将尾数右移,从而导致精度丢失。
String orderDetailString = JSON.toJSONString(orderVO, SerializerFeature.BrowserCompatible);
SerializerFeature.BrowserCompatible
可以自动将数值变成字符串返回,解决精度问题。
方案三:如果,上述两种方式都不适合,那么这种方式就需要后端返回一个新的String类型,前端使用新的,并后续上线后下掉老的Long型(推荐使用该方式,因为可以明确使用String型,防止后续误用Long型)。
2)使用node的方式,直接通过调用后端接口的方式获取
方案一:使用npm的js-2-java的
java.Long(orderId)
方法兼容一下。
方案二:后端接口返回一个新的String类型的订单ID,前端使用新的属性字段(推荐使用,防止后续踩坑)。
引用 [1]http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.52.2247&rank=2 [2]《码出高效》
电子书下载 《 Java开发手册(嵩山版)灵魂15问》
解读《Java开发手册(嵩山版)》电子书来了!深度剖析Java规约背后的原理,从“问题重现”到“原理分析”再到“问题解决”,给你不一样的解读视角,是手册必备的伴读书目。
![a69768c78b57b46a59e72b3a9c40906c.png](https://i-blog.csdnimg.cn/blog_migrate/fa8663af8470be09b95e5df73a81423a.jpeg)
![961d7541fa925dbac014beef05353b77.gif](https://i-blog.csdnimg.cn/blog_migrate/879d61cf126fa7297136a2e0f89249a9.gif)