新聞中心
最新發(fā)布的《Java開發(fā)手冊(嵩山版)》增加了前后端規(guī)約,其中有一條:禁止服務(wù)端在超大整數(shù)下使用Long類型作為返回。這是為何?在實(shí)際開發(fā)中可能出現(xiàn)什么問題?本文從IEEE754浮點(diǎn)數(shù)標(biāo)準(zhǔn)講起,詳細(xì)解析背后的原理,幫助大家徹底理解這個問題,提前避坑。

成都創(chuàng)新互聯(lián)公司是一家專注網(wǎng)站建設(shè)、網(wǎng)絡(luò)營銷策劃、微信平臺小程序開發(fā)、電子商務(wù)建設(shè)、網(wǎng)絡(luò)推廣、移動互聯(lián)開發(fā)、研究、服務(wù)為一體的技術(shù)型公司。公司成立十多年以來,已經(jīng)為1000+酒店設(shè)計各業(yè)的企業(yè)公司提供互聯(lián)網(wǎng)服務(wù)?,F(xiàn)在,服務(wù)的1000+客戶與我們一路同行,見證我們的成長;未來,我們一起分享成功的喜悅。
8月3日,這個在我等碼農(nóng)心中具有一定紀(jì)念意義的日子里,《Java開發(fā)手冊》發(fā)布了嵩山版。每次發(fā)布我都特別期待,因為總能找到一些程序員不得不重視的“血淋淋的巨坑”。比如這次,嵩山版中新增的模塊——前后端規(guī)約,其中一條禁止服務(wù)端在超大整數(shù)下使用Long類型作為返回。
這個問題,我在實(shí)際開發(fā)中遇到過,所以印象也特別深。如果在業(yè)務(wù)初期沒有評估到這一點(diǎn),將訂單ID這類關(guān)鍵信息,按照Long類型返回給前端,可能會在業(yè)務(wù)中后期高速發(fā)展階段,突然暴雷,導(dǎo)致嚴(yán)重的業(yè)務(wù)故障。期望大家能夠重視。
這條規(guī)約給出了直接明確的避坑指導(dǎo),但要充分理解背后的原理,知其所以然,還有很多點(diǎn)要思考。首先,我們來看幾個問題,如果能說出所有問題的細(xì)節(jié),就可直接跳過了,否則下文還是值得一看的:
- 一問:JS的Number類型能安全表達(dá)的最大整型數(shù)值是多少?為什么(注意要求更嚴(yán),是安全表達(dá))?
- 二問:在Long取值范圍內(nèi),2的指數(shù)次整數(shù)轉(zhuǎn)換為JS的Number類型,不會有精度丟失,但能放心使用么?
- 三問:我們一般都知道十進(jìn)制數(shù)轉(zhuǎn)二進(jìn)制浮點(diǎn)數(shù)有可能會出現(xiàn)精度丟失,但精度丟失具體怎么發(fā)生的?
- 四問:如果不幸中招,服務(wù)端正在使用Long類型作為大整數(shù)的返回,有哪些辦法解決?
基礎(chǔ)回顧
在解答上面這些問題前,先介紹本文涉及到的重要基礎(chǔ):IEEE754浮點(diǎn)數(shù)標(biāo)準(zhǔn)。如果大家對IEEE754的細(xì)節(jié)爛熟于心的話,可以跳過本段內(nèi)容,直接看下一段,問題解答部分。
當(dāng)前業(yè)界流行的浮點(diǎn)數(shù)標(biāo)準(zhǔn)是IEEE754,該標(biāo)準(zhǔn)規(guī)定了4種浮點(diǎn)數(shù)類型:單精度、雙精度、延伸單精度、延伸雙精度。前兩種類型是最常用的。我們單介紹一下雙精度,掌握雙精度,自然就了解了單精度(而且上述問題場景也是涉及雙精度)。
雙精度分配了8個字節(jié),總共64位,從左至右劃分是1位符號、11位指數(shù)、52位有效數(shù)字。如下圖所示,以0.7為例,展示了雙精度浮點(diǎn)數(shù)的存儲方式。
存儲位分配
1)符號位:在最高二進(jìn)制位上分配1位表示浮點(diǎn)數(shù)的符號,0表示正數(shù),1表示負(fù)數(shù)。
2)指數(shù):也叫階碼位。
在符號位右側(cè)分配11位用來存儲指數(shù),IEEE754標(biāo)準(zhǔn)規(guī)定階碼位存儲的是指數(shù)對應(yīng)的移碼,而不是指數(shù)的原碼或補(bǔ)碼。根據(jù)計算機(jī)組成原理中對移碼的定義可知,移碼是將一個真值在數(shù)軸上正向平移一個偏移量之后得到的,即[x]移=x+2^(n-1)(n為x的二進(jìn)制位數(shù),含符號位)。移碼的幾何意義是把真值映射到一個正數(shù)域,其特點(diǎn)是可以直觀地反映兩個真值的大小,即移碼大的真值也大?;谶@個特點(diǎn),對計算機(jī)來說用移碼比較兩個真值的大小非常簡單,只要高位對齊后逐個比較即可,不用考慮負(fù)號的問題,這也是階碼會采用移碼表示的原因所在。
由于階碼實(shí)際存儲的是指數(shù)的移碼,所以指數(shù)與階碼之間的換算關(guān)系就是指數(shù)與它的移碼之間的換算關(guān)系。假設(shè)指數(shù)的真值為e,階碼為E ,則有 E = e + (2 ^ (n-1) - 1),其中 2 ^ (n-1) - 1 是IEEE754 標(biāo)準(zhǔn)規(guī)定的偏移量。則雙精度下,偏移量為1023,11位二進(jìn)制取值范圍為[0,2047],因為全0是機(jī)器零、全1是無窮大都被當(dāng)做特殊值處理,所以E的取值范圍為[1,2046],減去偏移量,可得e的取值范圍為[-1022,1023] 。
3)有效數(shù)字:也叫尾數(shù)位。最右側(cè)分配連續(xù)的52位用來存儲有效數(shù)字,IEEE754標(biāo)準(zhǔn)規(guī)定尾數(shù)以原碼表示。
浮點(diǎn)數(shù)和十進(jìn)制之間的轉(zhuǎn)換
在實(shí)際實(shí)現(xiàn)中,浮點(diǎn)數(shù)和十進(jìn)制之間的轉(zhuǎn)換規(guī)則有3種情況:
1 規(guī)格化
指數(shù)位不是全零,且不是全1時,有效數(shù)字最高位前默認(rèn)增加1,不占用任何比特位。那么,轉(zhuǎn)十進(jìn)制計算公式為:
- (-1)^s*(1+m/2^52)*2^(E-1023)
其中s為符號,m為尾數(shù),E為階碼。比如上圖中的0.7 :
1)符號位:是0,代表正數(shù)。
2)指數(shù)位:01111111110,轉(zhuǎn)換為十進(jìn)制,得階碼E為1022,則真值e=1022-1023=-1。
3)有效數(shù)字:
- 0110011001100110011001100110011001100110011001100110
轉(zhuǎn)換為十進(jìn)制,尾數(shù)m為:1801439850948198。
4)計算結(jié)果:
(1+1801439850948198/2^52)*(2^-1) =0.6999999999999999555910790149937383830547332763671875
經(jīng)過顯示優(yōu)化算法后(在后文中詳述),為0.7。
2 非規(guī)格化
指數(shù)位是全零時,有效數(shù)字最高位前默認(rèn)為0。那么,轉(zhuǎn)十進(jìn)制計算公式:
- (-1)^s*(0+m/2^52)*2^(-1022)
注意,指數(shù)位是-1022,而不是-1023,這是為了平滑有效數(shù)字最高位前沒有1。比如非規(guī)格最小正值為:
- 0x0.0000000000001*2^-1022=2^-52 * 2^-1022 = 4.9*10^-324
3 特殊值
指數(shù)全為1,有效數(shù)字全為0時,代表無窮大;有效數(shù)字不為0時,代表NaN(不是數(shù)字)。
問題解答
1 JS的Number類型能安全表達(dá)的最大整型數(shù)值是多少?為什么?
規(guī)約中已經(jīng)指出:
在Long類型能表示的最大值是2的63次方-1,在取值范圍之內(nèi),超過2的53次方(9007199254740992)的數(shù)值轉(zhuǎn)化為JS的Number時,有些數(shù)值會有精度損失。
“2的53次方”這個限制是怎么來的呢?如果看懂上文IEEE754基礎(chǔ)回顧,不難得出:在浮點(diǎn)數(shù)規(guī)格化下,雙精度浮點(diǎn)數(shù)的有效數(shù)字有52位,加上有效數(shù)字最高位前默認(rèn)為1,共53位,所以JS的Number能保障無精度損失表達(dá)的最大整數(shù)是2的53次方。
而這里的題問是:“能安全表達(dá)的最大整型”,安全表達(dá)的要求,除了能準(zhǔn)確表達(dá),還有正確比較。2^53=9007199254740992,實(shí)際上,
- 9007199254740992+1 == 9007199254740992
的比較結(jié)果為true。如下圖所示:
這個測試結(jié)果足以說明2^53不是一個安全整數(shù),因為它不能唯一確定一個自然整數(shù),實(shí)際上9007199254740992、9007199254740993,都對應(yīng)這個值。因此這個問題的答案是:2^53-1。
2 在Long取值范圍內(nèi),2的指數(shù)次整數(shù)轉(zhuǎn)換為JS的Number類型,不會有精度丟失,但能放心使用么?
規(guī)約中指出:
在Long取值范圍內(nèi),任何2的指數(shù)次整數(shù)都是絕對不會存在精度損失的,所以說精度損失是一個概率問題。若浮點(diǎn)數(shù)尾數(shù)位與指數(shù)位空間不限,則可以精確表示任何整數(shù)。
后半句,我們就不說了,因為絕對沒毛病,空間不限,不僅是任何整數(shù)可以精確表示,無理數(shù)我們也可以挑戰(zhàn)一下。我們重點(diǎn)看前半句,根據(jù)本文前面所述基礎(chǔ)回顧,雙精度浮點(diǎn)數(shù)的指數(shù)取值范圍為[-1022,1023],而指數(shù)是以2為底數(shù)。另外,雙精度浮點(diǎn)數(shù)的取值范圍,比Long大,所以,理論上Long型變量中2的指數(shù)次整數(shù)一定可以準(zhǔn)確轉(zhuǎn)換為JS的umber類型。但在JS中,實(shí)際情況,卻是下面這樣:
2的55次方的準(zhǔn)確計算結(jié)果是:36028797018963968,而從上圖可看到,JS的計算結(jié)果是:36028797018963970。而且直接輸入36028797018963968,控制臺顯示結(jié)果是36028797018963970。
這個測試結(jié)果,已經(jīng)對本問題給出答案。為了確保程序準(zhǔn)確,本文建議,在整數(shù)場景下,對于JS的Number類型使用,嚴(yán)格限制在2^53-1以內(nèi),最好還是信規(guī)約的,直接使用String類型。
為什么會出現(xiàn)上面的測試現(xiàn)象呢?
實(shí)際上,我們在程序中輸入一個浮點(diǎn)數(shù)a,在輸出得到a',會經(jīng)歷以下過程:
1)輸入時:按照IEEE754規(guī)則,將a存儲。這個過程很有可能會發(fā)生精度損失。
2)輸出時:按照IEEE754規(guī)則,計算a對應(yīng)的值。根據(jù)計算結(jié)果,尋找一個最短的十進(jìn)制數(shù)a',且要保障a'不會和a隔壁浮點(diǎn)數(shù)的范圍沖突。a隔壁浮點(diǎn)數(shù)是什么意思呢?由于存儲位數(shù)是限定的,浮點(diǎn)數(shù)其實(shí)是一個離散的集合,兩個緊鄰的浮點(diǎn)數(shù)之間,還存在著無數(shù)的自然數(shù)字,無法表達(dá)。假設(shè)有f1、f2、f3三個升序浮點(diǎn)數(shù),且它們之間的距離,不可能在拉近。則在這三個浮點(diǎn)數(shù)之間,按照范圍來劃分自然數(shù)。而浮點(diǎn)數(shù)輸出的過程,就是在自己范圍中找一個最適合的自然數(shù),作為輸出。如何找到最合適的自然數(shù),這是一個比較復(fù)雜的浮點(diǎn)數(shù)輸出算法,大家感興趣的,可參考相關(guān)論文[1]。
所以,36028797018963968和36028797018963970這兩個自然數(shù),對應(yīng)到計算機(jī)浮點(diǎn)數(shù)來說,其實(shí)是同一個存儲結(jié)果,雙精度浮點(diǎn)數(shù)無法區(qū)分它們,最終呈現(xiàn)哪一個十進(jìn)制數(shù),就看浮點(diǎn)數(shù)的輸出算法了。下圖這個例子可以說明這兩個數(shù)字在浮點(diǎn)數(shù)中是相等的。另外,大家可以想想輸入0.7,輸出是0.7的問題,浮點(diǎn)數(shù)是無法精確存儲0.7,輸出卻能夠精確,也是因為有浮點(diǎn)數(shù)輸出算法控制(特別注意,這個輸出算法無法保證所有情況下,輸入等于輸出,它只是盡力確保輸出符合正常的認(rèn)知)。
擴(kuò)展
JS的Number類型既用來做整數(shù)計算、也用來做浮點(diǎn)數(shù)計算。其轉(zhuǎn)換為String輸出的規(guī)則也會影響我們使用,具體規(guī)則如下:
上面是一段典型的又臭又長但邏輯很嚴(yán)謹(jǐn)?shù)拿枋?,我總結(jié)了一個不是很嚴(yán)謹(jǐn),但好理解的說法,大家可以參考一下:
除了小數(shù)點(diǎn)前的數(shù)字位數(shù)(不算開始的0)少于22位,且絕對值大于等于1e-6的情況,其余都用科學(xué)計數(shù)法格式化輸出。舉例:
3 我們一般都知道十進(jìn)制數(shù)轉(zhuǎn)二進(jìn)制浮點(diǎn)數(shù)有可能會出現(xiàn)精度丟失,精度丟失怎么發(fā)生的?
通過前面IEEE754分析,我們知道十進(jìn)制數(shù)存儲到計算機(jī),需要轉(zhuǎn)換為二進(jìn)制。有兩種情況,會導(dǎo)致轉(zhuǎn)換后精度損失:
1)轉(zhuǎn)換結(jié)果是無限循環(huán)數(shù)或無理數(shù)
比如0.1轉(zhuǎn)換成二進(jìn)制為:
- 0.0001 10011001100110011001100110011...
其中0011在循環(huán)。將0.1轉(zhuǎn)換為雙精度浮點(diǎn)數(shù)二進(jìn)制存儲為:
- 0 01111111011 1001100110011001100110011001100110011001100110011001
按照本文前面所述基礎(chǔ)回顧中的計算公式 (-1)^s*(1+m/2^52)*2^(E-1023)計算,可得轉(zhuǎn)換回十進(jìn)制為:0.09999999999999999。這里可以看出,浮點(diǎn)數(shù)有時是無法精確表達(dá)一個自然數(shù),這個和十進(jìn)制中1/3 =0.333333333333333...是一個道理。
2)轉(zhuǎn)換結(jié)果長度,超過有效數(shù)字位數(shù),超過部分會被舍棄
IEEE754默認(rèn)是舍入到最近的值,如果“舍”和“入”一樣接近,那么取結(jié)果為偶數(shù)的選擇。
另外,在浮點(diǎn)數(shù)計算過程中,也可能引起精度丟失。比如,浮點(diǎn)數(shù)加減運(yùn)算執(zhí)行步驟分為:
零值檢測 -> 對階操作 -> 尾數(shù)求和 -> 結(jié)果規(guī)格化 -> 結(jié)果舍入
其中對階和規(guī)格化都有可能造成精度損失:
- 對階:是通過尾數(shù)右移(左移會導(dǎo)致高位被移出,誤差更大,所以只能是右移),將小指數(shù)改成大指數(shù),達(dá)到指數(shù)階碼對齊的效果,而右移出的位,會作為保護(hù)位暫存,在結(jié)果舍入中處理,這一步有可能導(dǎo)致精度丟失。
- 規(guī)格化:是為了保障計算結(jié)果的尾數(shù)最高位是1,視情況有可能會出現(xiàn)右規(guī),即將尾數(shù)右移,從而導(dǎo)致精度丟失。
4 如果不幸中招,服務(wù)端正在使用Long類型作為大整數(shù)的返回,有哪些辦法解決?
需要分情況。
1)通過Web的ajax異步接口,以Json串的形式返回給前端
方案一:如果,返回Long型所在的POJO對象在其他地方無使用,那么可以將后端的Long型直接修改成String型。
方案二:如果,返回給前端的Json串是將一個POJO對象Json序列化而來,并且這個POJO對象還在其他地方使用,而無法直接將其中的Long型屬性直接改為String,那么可以采用以下方式:
- String orderDetailString = JSON.toJSONString(orderVO, SerializerFeature.BrowserCompatible);
SerializerFeature.BrowserCompatible 可以自動將數(shù)值變成字符串返回,解決精度問題。
方案三:如果,上述兩種方式都不適合,那么這種方式就需要后端返回一個新的String類型,前端使用新的,并后續(xù)上線后下掉老的Long型(推薦使用該方式,因為可以明確使用String型,防止后續(xù)誤用Long型)。
2)使用node的方式,直接通過調(diào)用后端接口的方式獲取
方案一:使用npm的js-2-java的 java.Long(orderId) 方法兼容一下。
方案二:后端接口返回一個新的String類型的訂單ID,前端使用新的屬性字段(推薦使用,防止后續(xù)踩坑)。
引用
[1]http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.52.2247&rank=2
[2]《碼出高效》
文章題目:解讀:大整數(shù)傳輸為何禁用Long類型?
本文路徑:http://fisionsoft.com.cn/article/cosgipg.html


咨詢
建站咨詢
