一、背景:大促備戰(zhàn)中的異常數(shù)據(jù)
大促備戰(zhàn)期間,接到客戶反饋我司上傳到客戶服務(wù)器上的文件存在科學(xué)計(jì)數(shù)法表示的情況(下圖的4.55058496E7),與約定不符。

??
查看轉(zhuǎn)換前的數(shù)據(jù)是:455058496,轉(zhuǎn)換后(除以10:進(jìn)行毫米到厘米的轉(zhuǎn)換)就變成了科學(xué)計(jì)數(shù)法形式了。

??
問題代碼:
說明:
這個(gè)是個(gè)EL表達(dá)式,含義是使用expr的值作為計(jì)算邏輯,計(jì)算結(jié)果賦值給var指向的變量temp.b,類型是java.lang.String。
?_item代表當(dāng)前上下文里的一個(gè)對(duì)象。
?boxLength是_item對(duì)象所具備的屬性。
?該表達(dá)式先對(duì)boxLength執(zhí)行除以 10 的運(yùn)算,再把運(yùn)算結(jié)果轉(zhuǎn)換為字符串(由clazz定義的)。
業(yè)務(wù)上,boxLength是個(gè)長度的概念,單位是毫米,除以10是轉(zhuǎn)換成厘米的含義。為了保證精度,系統(tǒng)(基于JAVA)會(huì)先將boxLength先轉(zhuǎn)成java.lang.Double類型,再除以10,最后調(diào)用Double.toString()方法轉(zhuǎn)成字符串。
二、問題定位:字符串轉(zhuǎn)換的科學(xué)計(jì)數(shù)法陷阱
2.1 問題復(fù)現(xiàn)
代碼:
Double depthInDouble = 455058496d/10;
log.info("depthInDouble={}", depthInDouble);
結(jié)果:

??
2.2 原因分析
問題就出在了最后一行,日志輸出的時(shí)候Double會(huì)被轉(zhuǎn)成String,調(diào)用Double.toString()方法,而對(duì)于Double對(duì)象的值在一定的范圍內(nèi),會(huì)使用科學(xué)計(jì)數(shù)法表示。
log.info的調(diào)用鏈(為什么會(huì)調(diào)用到Double.toStirng()):
log.info("depthInDouble={}", depthInDouble);
↓
Log4jLogger.info(String format, Object arg)
↓
AbstractLogger.logIfEnabled(...)
↓
AbstractLogger.logMessage(...)
↓
ParameterizedMessageFactory.newMessage(...)
↓
ParameterizedMessage 構(gòu)造函數(shù)(參數(shù)被暫存為 Object[])
↓
// 此時(shí)尚未調(diào)用 Double.toString()
↓
// 當(dāng) Appender 執(zhí)行輸出時(shí)...
Appender.append(LogEvent)
↓
LogEvent.getMessage().getFormattedMessage() // 觸發(fā)消息格式化
↓
ParameterizedMessage.getFormattedMessage()
↓
ParameterizedMessage.formatMessage(...)
↓
ParameterizedMessage.argToString(Object)
↓
Double.toString() // 終于在這里被調(diào)用!
查看Double.toString()的源碼,可以看到相關(guān)解釋:

??
也就是說對(duì)于極?。ㄐ∮?0^-3)或者極大(大于10^7)值的浮點(diǎn)數(shù),轉(zhuǎn)成String的時(shí)候會(huì)使用科學(xué)計(jì)數(shù)法表示,驗(yàn)證如下。
代碼:
public static void main(String args[]) {
String depth = "455058496"; // 單位:毫米
Double depthInDouble = Double.parseDouble(depth)/10;
String doubleInString = String.valueOf(depthInDouble);
log.info("depthInDouble={}", depthInDouble);
log.info("doubleInString={}", doubleInString);
depthInDouble = 1e-3;
log.info("10^-3 = {}", depthInDouble);
depthInDouble = 1e7;
log.info("10^7 = {}", depthInDouble);
Double aVerySmallNumber = 1e-9;
depthInDouble = 1e-3 - aVerySmallNumber;
log.info("10^-3 - delta = {}", depthInDouble);
depthInDouble = 1e7 - aVerySmallNumber;
log.info("10^7 - delta = {}", depthInDouble);
}
運(yùn)行結(jié)果:

??
說明,10^-3不會(huì)使用科學(xué)記計(jì)數(shù)法,但是小于它就會(huì)使用科學(xué)計(jì)數(shù)法,10^7就會(huì)使用科學(xué)計(jì)數(shù)法,小于它就會(huì)不會(huì),大于它會(huì)。
2.3 為什么要使用科學(xué)計(jì)數(shù)法
2.3.1 小數(shù)在計(jì)算機(jī)內(nèi)是如何表示的
先不急于討論為什么使用科學(xué)計(jì)數(shù)法,我們先看看小數(shù)在計(jì)算機(jī)內(nèi)是如何表示的。
從存儲(chǔ)角度來看,計(jì)算機(jī)的存儲(chǔ)是有限資源,能存儲(chǔ)的數(shù)據(jù)是有范圍的,不是無限大,也就是說有限的硬件資源限制了計(jì)算機(jī)可以表示的數(shù)值的大小。對(duì)于一個(gè)浮點(diǎn)數(shù),我們可以用10個(gè)bit存儲(chǔ),也可以用100個(gè),為了實(shí)現(xiàn)跨設(shè)備、跨平臺(tái)的數(shù)據(jù)統(tǒng)一表示和交換,IEEE 754 規(guī)范定義了標(biāo)準(zhǔn)格式,規(guī)定了Double類型使用64比特。

當(dāng)64個(gè)比特確定了,那么它可以表示的數(shù)字的范圍就確定了,接下來考慮怎么表示小數(shù),可以表示什么范圍內(nèi)的小數(shù),進(jìn)而再討論威懾么定義超過10^7或者小于10^-3使用科學(xué)計(jì)數(shù)法,而不用普通的方式(定點(diǎn)數(shù)表示法)。
類似整數(shù)可以利用除以2取余獲得其二級(jí)制的表示形式,例如:123(10進(jìn)制)= 1111011(二進(jìn)制)

??
小數(shù)則進(jìn)行乘2取整,如0.123(10進(jìn)制)= 0. 0001111101(二進(jìn)制,位數(shù)會(huì)一直循環(huán)無法精確表示,只能近似,這里取了10位)

??
?
因此最簡單的一種設(shè)計(jì)(不考慮正負(fù))就是將64位中的一部分劃分為整數(shù)位,一部分劃分為小數(shù)位,比如32位整數(shù),32位小數(shù)(定點(diǎn)數(shù)表示法)。
那么這樣設(shè)計(jì)的Double最大數(shù)可以表示2^32-1,
如果要以米為單位表示銀河系直徑,約1光年≈299792458米/秒*1年 = 299792458米/秒*365天*86400秒/天 ≈ 9.45 * 10^15 ,而2^32-1≈4.29 * 10^9 (遠(yuǎn)小于1光年),因此無法使用Double表示銀河系直徑,無法支撐天文學(xué)科的計(jì)算了。

??
這樣設(shè)計(jì)的Double最小可以表示2^-32=2.38*10^-10 ,一個(gè)質(zhì)子的大小是0.84飛米=8.4*10^-16,因此也無法支持物理學(xué)的計(jì)算。
所以,矛盾在于增加整數(shù)部分的位數(shù),就會(huì)壓縮小數(shù)部分的位數(shù),不同的領(lǐng)域中,既有要求數(shù)字很大可表示的(在乎量級(jí),如天文學(xué)、金融學(xué)),也有要求數(shù)值很小能表示的(在乎精度,如物理學(xué)、生物學(xué))。
可以看到,上面的很多數(shù)字表達(dá),我們也使用了科學(xué)計(jì)數(shù)法的表示形式來簡化表達(dá),對(duì)于上面這個(gè)數(shù)字(9.454,254,955,488,000)寫起來麻煩還很占地方,而且我們也不需要那么精確,只是看個(gè)量級(jí),因此會(huì)寫成9.45 * 10^15 ,不影響理解。
即表示一個(gè)極大或者極小的數(shù)可以使用:【數(shù)值*底數(shù)^指數(shù)】的形式,對(duì)于大數(shù)來講指數(shù)就是正的,小數(shù)就是負(fù)的,計(jì)算機(jī)使用二進(jìn)制,因此底數(shù)就是2,所以小數(shù)可以表示成:【數(shù)值*2^指數(shù)】的形式,這個(gè)數(shù)值,其實(shí)就是尾數(shù)。
計(jì)算機(jī)專家們經(jīng)過多種研究,最終經(jīng)過IEEE確定了IEEE 754標(biāo)準(zhǔn),即不確定整數(shù)和小數(shù)的位數(shù)(固定小數(shù)點(diǎn),即定點(diǎn)數(shù)),而使用變化的位數(shù),也就是小數(shù)點(diǎn)可以浮動(dòng),即浮點(diǎn)數(shù)表示法。浮點(diǎn)數(shù)表示法定義了小數(shù)由符號(hào)位+指數(shù)位+尾數(shù)位三部分組成。
符號(hào)位是1bit,0代表整數(shù),1代表負(fù)數(shù),指數(shù)位決定數(shù)值的量級(jí),尾數(shù)位決定數(shù)值精度。
64位的說明如下:

??
?
其中11和52的設(shè)計(jì)是在平衡了很多需求后得到的最佳實(shí)踐。
Double (64位) = 符號(hào)位(1位) + 指數(shù)位(11位) + 尾數(shù)位(52位) 示例:455058496.0 的IEEE 754表示 原始值:455058496.0 二進(jìn)制科學(xué)計(jì)數(shù)法:1.0101100001110000000000000000000 × 2^28 符號(hào)位:0 (正數(shù)) 指數(shù)位:28 + 1023(偏移量) = 1051 = 10000011011? 尾數(shù)位:0101100001110000000000000000000... (52位) 完整64位表示: 0 10000011011 0101100001110000000000000000000000000000000000000000
2.3.2 數(shù)值超過10^7或者小于10^-3會(huì)發(fā)生什么
其實(shí)什么也不會(huì)發(fā)生,只是基于如下原因綜合權(quán)衡的結(jié)果。
1、認(rèn)知科學(xué)依據(jù)
?人類短期記憶的數(shù)字處理能力約為7±2位
?超過7位的整數(shù)部分難以快速理解
?科學(xué)計(jì)數(shù)法提供更好的可讀性
2、精度保持考慮
?10^7 = 10,000,000 (8位數(shù)字)
?超過此值,普通格式會(huì)顯得冗長
?10^-3 = 0.001,更小的數(shù)用科學(xué)計(jì)數(shù)法更清晰
3、歷史兼容性
?這個(gè)標(biāo)準(zhǔn)在多種編程語言中被采用
?保持了與C語言printf的兼容性
?符合IEEE 754標(biāo)準(zhǔn)的建議
這也就是為什么這個(gè)這個(gè)范圍內(nèi)的數(shù)要表示成科學(xué)計(jì)數(shù)法了。
2.3.3 源碼探究
1、調(diào)用鏈路
根據(jù)源碼,可以看到Double.toString()方法的調(diào)用鏈?zhǔn)牵?/p>

??
分流是否使用科學(xué)計(jì)數(shù)法的核心代碼toChars的代碼如下:
/*
* Formats the decimal f 10^e.
*/
private int toChars(byte[] str, int index, long f, int e, FormattedFPDecimal fd) {
/*
* For details not discussed here see section 10 of [1].
*
* Determine len such that
* 10^(len-1) <= f < 10^len
*/
int len = flog10pow2(Long.SIZE - numberOfLeadingZeros(f));
if (f >= pow10(len)) {
len += 1;
}
if (fd != null) {
fd.set(f, e, len);
return index;
}
/*
* Let fp and ep be the original f and e, respectively.
* Transform f and e to ensure
* 10^(H-1) <= f < 10^H
* fp 10^ep = f 10^(e-H) = 0.f 10^e
*/
f *= pow10(H - len);
e += len;
/*
* The toChars?() methods perform left-to-right digits extraction
* using ints, provided that the arguments are limited to 8 digits.
* Therefore, split the H = 17 digits of f into:
* h = the most significant digit of f
* m = the next 8 most significant digits of f
* l = the last 8, least significant digits of f
*
* For n = 17, m = 8 the table in section 10 of [1] shows
* floor(f / 10^8) = floor(193_428_131_138_340_668 f / 2^84) =
* floor(floor(193_428_131_138_340_668 f / 2^64) / 2^20)
* and for n = 9, m = 8
* floor(hm / 10^8) = floor(1_441_151_881 hm / 2^57)
*/
long hm = multiplyHigh(f, 193_428_131_138_340_668L) >>> 20;
int l = (int) (f - 100_000_000L * hm);
int h = (int) (hm * 1_441_151_881L >>> 57);
int m = (int) (hm - 100_000_000 * h);
if (0 < e && e <= 7) {
return toChars1(str, index, h, m, l, e);
}
if (-3 < e && e <= 0) {
return toChars2(str, index, h, m, l, e);
}
return toChars3(str, index, h, m, l, e);
}
代碼地址: https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/jdk/internal/math/DoubleToDecimal.java
可以看到使用科學(xué)計(jì)數(shù)法處理的核心代碼是toChars3,代碼如下:
private int toChars3(byte[] str, int index, int h, int m, int l, int e) {
/* -3 >= e | e > 7: computerized scientific notation */
index = putDigit(str, index, h);
index = putChar(str, index, '.');
index = put8Digits(str, index, m);
index = lowDigits(str, index, l);
return exponent(str, index, e - 1);
}
2、toChars3()的參數(shù)含義
?byte[] str: 輸出字符串的字節(jié)數(shù)組
?int index: 當(dāng)前寫入位置的索引
?int h: 最高位數(shù)字 (0-9)
?int m: 中間8位數(shù)字 (00000000-99999999)
?int l: 低位數(shù)字 (用于精度控制)
?int e: 調(diào)整后的十進(jìn)制指數(shù)值
3、 toChars3()的數(shù)據(jù)流處理步驟
1.putDigit(str, index, h) → 寫入最高位數(shù)字
2.putChar(str, index, '.') → 寫入小數(shù)點(diǎn)
3.put8Digits(str, index, m) → 寫入中間8位數(shù)字
4.lowDigits(str, index, l) → 寫入低位數(shù)字(去除尾隨零)
5.exponent(str, index, e-1) → 寫入指數(shù)部分
為什么使用 e-1?
原因:已經(jīng)放置了一位數(shù)字在小數(shù)點(diǎn)前 目的:調(diào)整指數(shù)以保持?jǐn)?shù)值不變 示例:4.55058496E7 表示 4.55058496 × 10^7
4、exponent()分析
標(biāo)準(zhǔn)科學(xué)計(jì)數(shù)法:a.bcd × 10^n 約束條件:1 ≤ a < 10(小數(shù)點(diǎn)前只有一位非零數(shù)字)
private int exponent(byte[] str, int index, int exp) {
str[index++] = (byte) 'E'; // 寫入字符 'E'
if (exp < 0) {
str[index++] = (byte) '-'; // 負(fù)指數(shù)寫入 '-'
exp = -exp; // 轉(zhuǎn)為正數(shù)處理
}
if (exp >= 100) {
str[index++] = (byte) ('0' + exp / 100); // 百位
exp %= 100;
}
if (exp >= 10) {
str[index++] = (byte) ('0' + exp / 10); // 十位
exp %= 10;
}
str[index++] = (byte) ('0' + exp); // 個(gè)位
return index;
}
?輸入?yún)?shù): byte[] str(輸出緩沖區(qū))、int index(寫入位置)、int exp(指數(shù)值)
?核心功能: 將指數(shù)值格式化為字符串并寫入字節(jié)數(shù)組
?處理邏輯: 優(yōu)化處理1位、2位、3位數(shù)的指數(shù)
1. 寫入 'E' 2. 處理負(fù)號(hào)(如果 exp < 0) 3. 處理百位(如果 exp >= 100) 4. 處理十位(如果 exp >= 10) 5. 處理個(gè)位(必須)
?返回值: 更新后的索引位置
例子:
1. 原始數(shù)值: 45505849.6 2. 精確指數(shù): 7.658067227112319 3. 調(diào)整后指數(shù): 7.658 - 1 = 6.658 4. 四舍五入: 7 5. exponent方法輸入: exp = 7 6. 執(zhí)行步驟: - 寫入 'E' → index = 1 - exp = 7 < 10,跳過百位和十位 - 寫入個(gè)位 '7' → index = 2 7. 輸出: "E7" 8. 完整結(jié)果: "4.55058496E7"
根據(jù)源代碼的邏輯簡化了一版如下:
https://coding.jd.com/newJavaEngineerOrientation/Double2String.git
三、解決方案
3.1 BigDecimal 精準(zhǔn)控制
new BigDecimal(doubleValue).setScale(2, RoundingMode.HALF_UP).toPlainString()
3.2 DecimalFormat 格式化
new DecimalFormat("#0.00").format(doubleValue) // 強(qiáng)制保留兩位小數(shù)
四、總結(jié)
Double 數(shù)值的字符串格式化規(guī)則(如 Double.toString())遵循:
?普通格式(Plain):當(dāng)數(shù)值的指數(shù)范圍在 [-3, 7) 時(shí)(即絕對(duì)值在 [10^-3, 10^7) 之間),直接顯示小數(shù)形式(如 0.001 或 123456.0)。
?科學(xué)計(jì)數(shù)法(Scientific):當(dāng)指數(shù)范圍超出 [-3, 7)(如 0.000999 或 10000000.0),顯示為科學(xué)計(jì)數(shù)法(如 9.99e-4 或 1.0e7)。
審核編輯 黃宇
-
Doubler
+關(guān)注
關(guān)注
0文章
8瀏覽量
7353 -
string
+關(guān)注
關(guān)注
0文章
41瀏覽量
5059
發(fā)布評(píng)論請(qǐng)先 登錄
C陷阱與缺陷
科學(xué)計(jì)數(shù)如何轉(zhuǎn)化為數(shù)字。
科學(xué)計(jì)數(shù)法轉(zhuǎn)換
long double to string函數(shù)找不到本地支持
IAR debug查看浮點(diǎn)類型變量怎么不用科學(xué)計(jì)數(shù)法顯示呢?
氣相色譜法在環(huán)境科學(xué)中的應(yīng)用
采用歸零法的N進(jìn)制計(jì)數(shù)器原理
java中string不可變的原因
基于隱蔽信息存儲(chǔ)分布的隱蔽信道構(gòu)造方法
如何使用C語言實(shí)現(xiàn)動(dòng)態(tài)擴(kuò)容的string
UTF8String是如何編碼的?
大促備戰(zhàn)中的隱蔽陷阱:Double轉(zhuǎn)String會(huì)使用科學(xué)計(jì)數(shù)法展示?
評(píng)論