当前位置: 首页 > 科技观察

解读:为什么Long类型禁止大整数传输?

时间:2023-03-22 15:55:12 科技观察

《Java开发手册(嵩山版)》的最新版本增加了前后端规范,其中一项是:禁止服务器使用Long类型作为非常大整数的返回。为什么是这样?实际开发中会出现哪些问题?本文从IEEE754浮点数标准入手,详细分析其背后的原理,帮助大家全面了解这个问题,提前避坑。8月3日,一个在我和其他码农心目中具有一定纪念意义的日子,《Java开发手册》发布了嵩山版。我很期待每一次发布,因为我总能找到一些程序员不得不注意的“血坑”。比如这次嵩山版本新增的模块——前后端协议,其中一个是禁止服务端使用Long类型作为一个非常大的整数的返回值。这个问题我在实际开发中遇到过,印象特别深刻。如果业务前期不评估这一点,订单ID等关键信息以Long类型返回前端,可能会在业务高速发展的中后期突然暴雷业务,导致严重的业务失败。希望大家多多关注。这条法规对于避坑的指导是直接明确的,但是要充分理解其背后的原理并知其所以然,还是有很多值得思考的地方。首先,让我们来看几个问题。如果你能说出题的所有细节,可以直接跳过,否则下面值得一读:一题:JS的Number类型可以安全表达的最大整数值是多少?为什么(注意是要求更严格,这是一个安全的表达)?第二个问题:在Long的取值范围内,如果将指数次为2的整数转换为JS的Number类型,不会有精度损失,但是可以放心使用吗?第三个问题:我们一般都知道十进制数转换为二进制浮点数可能会造成精度损失,但是精度损失是怎么发生的呢?四问:如果不幸中招,服务器使用Long类型作为大整数返回,有什么解决办法?基础回顾在回答以上问题之前,先介绍一下本文涉及的重要基础:IEEE754浮点数标准。如果熟悉IEEE754的细节,可以跳过这一段,直接进入下一段,问答部分。目前业界的浮点数标准是IEEE754,规定了四种浮点数:单精度、双精度、扩展单精度和扩展双精度。前两种类型是最常用的。简单介绍一下双精度,掌握了双精度,自然就理解了单精度(而且上面的问题场景也涉及到双精度)。双精度分配8个字节,共64位,从左到右依次为1位符号、11位指数、52位尾数。如下图,以0.7为例,说明了双精度浮点数的存储方式。存储位分配1)符号位:在最高二进制位上分配1位表示浮点数的符号,0表示正数,1表示负数。2)索引:也叫序码位。在符号位的右边分配11位来存储指数。IEEE754标准规定阶码位存放的是指数对应的移位码,而不是指数的原码或补码。根据计算机组成原理中frameshift的定义,frameshift是将一个真值在数轴上向前移动一个偏移量,即[x]shift=x+2^(n-1)(n是x的二进制数字,包括符号位)。移框的几何意义是将真值映射到一个正数域,其特点是可以直观地反映两个真值的大小,即真值越大,框越大转变群岛基于这个特点,计算机使用移码来比较两个真值的大小是非常简单的。只要高位对齐,一位一位比较,就不用考虑负号的问题了。这就是指数代码用移码表示的原因。由于指数实际存储的是指数的移码,所以指数与指数的换算关系就是指数与其移码的换算关系。假设指数的真实值为e,阶码为E,则E=e+(2^(n-1)-1),其中2^(n-1)-1为指定的偏移量IEEE754标准。在双精度中,偏移量是1023,11位二进制值的范围是[0,2047]。因为全0都是机器零,全1都是无穷大,被当作特殊值处理,所以E的取值范围是[1,2046],减去偏移量,e的取值范围可以是[-1022,1023]。3)有效数字:也称尾数。最右边分配的连续52位用来存放有效数字,IEEE754标准规定尾数用原码表示。浮点数与小数的转换在实际实现中,浮点数与小数的转换规则有以下三种情况:1.当归一化指数的位数不全为零也不全为1时,加1默认在有效数字的最高位之前,不占用任何位。那么,十进制转换的计算公式为:(-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为:1890194394)计算结果:(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^-3243特殊值指数全为1,当有效数字都是0,表示无穷大;当有效数字不为0时,表示NaN(不是数字)。答1JS的Number类型可以安全表达的最大整数值是多少?为什么?规范中已经指出,Long类型可以表示的最大值为2的63次方-1,取值范围内,超过2的53次方(9007199254740992)的值转换时变成一个JSNumber,有些值会失去精度。“2的53次方”的限制是怎么来的?如果看懂了上面的IEEE754基础回顾,就不难得出结论:在浮点数的标准化下,双精度浮点数的有效位有52位,加上有效位最高位为1默认,一共53位,所以JSNumber能保证不失精度的最大整数是2的53次方。这里的问题是:“能安全表达的最大整数”,安全表达的要求,除了表达准确,还要有正确的比较。2^53=9007199254740992,其实9007199254740992+1==9007199254740992的比较结果为真。如下图所示:这个测试结果足以说明2^53不是一个安全整数,因为它不能唯一确定一个自然整数。其实9007199254740992和9007199254740993都对应这个值。因此,这道题的答案是:2^53-1。2在Long的取值范围内,如果将2的指数整数转换为JS的Number类型,不会有精度损失,但是可以放心使用吗?规范中说:在Long的取值范围内,任何指数为2的整数都不会出现精度损失,所以精度损失是概率问题。如果一个浮点数的尾数位和指数位是无限的,那么任何整数都可以精确表示。后半句我们就不说了,因为绝对没有错,篇幅不限。不仅可以准确表示任何整数,我们还可以挑战无理数。让我们关注句子的前半部分。根据本文前面提到的基本回顾,双精度浮点数的指数范围是[-1022,1023],指数以2为基础。另外,双精度浮点数的取值范围-点数比龙大。因此,理论上Long变量中的指数整数2必须准确转换为JS的umber类型。但是在JS中,实际情况是这样的:2的55次方的准确计算结果是:?28797018963968,而从上图来看,JS的计算结果是:36028797018963970。并直接输入?28797018963968,控制台显示结果为?28797018963970。测试结果已经给出了这个问题的答案。为了保证程序的准确性,本文建议在整数场景下,JS的Number类型的使用严格限制在2^53-1以内,并且最好相信规则,使用String类型直接地。为什么会出现上述测试现象?实际上,我们在程序中输入一个浮点数a,在输出中得到a',会经过以下过程:1)输入时:按照IEEE754规则存储a。在此过程中很可能会丢失精度。2)输出时:根据IEEE754规则计算a对应的值。根据计算结果,找出最短的十进制数a',并保证a'不会与a的相邻浮点数范围冲突。隔壁的浮点数是什么意思?由于存储位数有限,所以浮点数实际上是一个离散集。两个相邻的浮点数之间有无数个自然数,无法表示。假设有三个递增的浮点数f1、f2、f3,它们之间的距离不能缩短。然后,在这三个浮点数之间,按照范围划分自然数。浮点数输出的过程就是在自己的范围内寻找最合适的自然数作为输出。如何找到最合适的自然数是一个比较复杂的浮点数输出算法。有兴趣的可以参考相关论文[1]。所以,?28797018963968和?28797018963970这两个自然数对于计算机浮点数来说其实是同一个存储结果,双精度浮点数是无法区分的。最终显示哪个十进制数取决于浮点数输出算法。下面的例子表明这两个数字在浮点数上是相等的。另外可以想一下输入为0.7,输出为0.7的问题。浮点数不能精确存储0.7,但输出可以精确。这也是因为浮点数输出算法的控制。等于输出,它只是试图确保输出符合正常认知)。扩展JS的Number类型既用于整数计算,也用于浮点数计算。将其转换成String输出的规则也会影响我们的使用。具体规则如下:以上是典型的臭臭长篇大论但逻辑严密的描述。我总结了一个说法,虽然不是很严谨,但是通俗易懂。大家可以参考一下:除小数点前位数(不包括首位0)小于22,绝对值大于等于1e-6外,其余格式化输出以科学计数法。例子:3我们一般都知道十进制数转换为二进制浮点数可能会造成精度损失。精度损失是如何发生的?通过前面对IEEE754的分析,我们知道十进制数在计算机中存储需要转换为二进制。有两种情况会导致转换后精度丢失:1)转换结果是无限循环数或无理数如0.1转换为二进制:0.000110011001100110011001100110011...其中0011在循环中。将0.1转换为双精度浮点数,用二进制存储为:0011111110111001100110011001100110011001100110011001100110011001根据计算公式(-1)^s*(1+m/2^52)*2^(E-1023)本文前面提到的基本复习,可以折算回十进制为:0.099999999999999999。从这里可以看出,浮点数有时不能准确表达一个自然数,在十进制中相当于1/3=0.333333333333333...。2)转换结果长度超过有效位数,超出部分将被丢弃。IEEE754默认四舍五入到最接近的值。如果“四舍五入”与“四舍五入”接近,则结果为偶数选择。另外,在浮点计算的过程中,也可能造成精度损失。例如,浮点数加减运算的执行步骤分为:零值检测->顺序运算->尾数求和->结果归一化->结果舍入。orderorder和normalization都可能造成精度损失:orderorder:通过将尾数右移(左移会导致高位移出,误差较大,所以只能右移),将小指数变为大指数,达到索引码对齐的效果,右移的位会被暂存为保护位,在结果取整时进行处理,这一步可能会导致精度损失。归一化:就是保证计算结果的尾数最高位为1。根据不同的情况,可能有右手法则,即尾数右移,导致精度损失。4如果不幸中招,服务器正在使用Long类型作为大整数的返回值,有什么解决办法?这取决于实际情况。1)通过Web的ajax异步接口,以Json字符串的形式返回给前端。方案一:如果返回Long类型的POJO对象没有在其他地方使用,那么可以直接将后端的Long类型修改为String类型。方案二:如果返回给前端的Json字符串是一个Json序列化后的POJO对象,而这个POJO对象还在其他地方使用,不能直接将Long类型的属性改成String,那么可以使用下面的方法:StringorderDetailString=JSON.toJSONString(orderVO,SerializerFeature.BrowserCompatible);SerializerFeature.BrowserCompatible可以自动将值转换成字符串返回,解决精度问题。方案三:如果以上两种方式都不合适,那么这种方式需要后端返回一个新的String类型,前端使用新的类型,上线后再drop旧的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]《码出高效》