對于Java開發(fā)人員來說,只要日常工作中涉及到算術(shù)運算,那必然會跟BigDecimal這個類打交道。也許我們可以記住一些使用的注意事項,如使用String的構(gòu)造函數(shù)而不是double的構(gòu)造函數(shù)來避免精度問題。但是對于一個5000行的龐然大物,僅僅了解兩個構(gòu)造函數(shù)還不足以支撐我們大規(guī)模應(yīng)用的信念,好在源代碼對我們是完全開放的,那不妨來一次源代碼的親密接觸。
站在用戶的角度思考問題,與客戶深入溝通,找到裕華網(wǎng)站設(shè)計與裕華網(wǎng)站推廣的解決方案,憑借多年的經(jīng)驗,讓設(shè)計與互聯(lián)網(wǎng)技術(shù)結(jié)合,創(chuàng)造個性化、用戶體驗好的作品,建站類型包括:做網(wǎng)站、成都網(wǎng)站設(shè)計、企業(yè)官網(wǎng)、英文網(wǎng)站、手機端網(wǎng)站、網(wǎng)站推廣、域名注冊、網(wǎng)頁空間、企業(yè)郵箱。業(yè)務(wù)覆蓋裕華地區(qū)。
按照J(rèn)ava的慣例在每個重要類前面都有一篇論文式的注釋,一般情況下把這段理解了應(yīng)付個面試是沒啥問題的。BigDecimal也不例外的在類注釋上花了近200行,我們做個簡單的摘要:
2 首先給出BigDecimal的定義為任意精度的有符號十進制數(shù)。BigDecimal可以表示為一個任意精度的無刻度值和一個32位整型的刻度。
2 BigDecimal提供了一系列的方法,如算術(shù)操作、標(biāo)度控制、舍入、比較等等方法,總之很強大。
2 BigDecimal通過precision、scale、rounding mode和MathContext類來控制標(biāo)度和進行舍入操作。
2 BigDecimal的equals方法并不是數(shù)學(xué)意義上的相等,所以在用于Sorted Map和Sorted Set這些和比較有關(guān)系的數(shù)據(jù)結(jié)構(gòu)時需要特別小心。
在論文注釋的指引下,我們可以整理出BigDecimal類的脈絡(luò):
接下來我們就順著脈絡(luò)一點點的解剖這個龐然大物了。
從圖中可以看出BigDecimal類主要需要關(guān)注5個主要屬性
? intVal和scale
分別表示BigDecimal的無標(biāo)度值和標(biāo)度,結(jié)合我們在注釋里看到的說法“BigDecimal可以表示為一個任意精度的無刻度值和一個32位整型的刻度”,這兩個屬性可以認(rèn)為是BigDecimal類的骨架。
? precision
BigDecimal中數(shù)字的個數(shù),在確定了precision后就會要求結(jié)合Rounding Mode做一些舍入方面的操作。
? stringCache
BigDecimal的字符表示,在toString方法的時候用到。
? intCompact
無標(biāo)度值的Long表示,方便后續(xù)計算。如果intVal在compact的過程發(fā)現(xiàn)超過Long.MAX_VALUE則將intCompact記為Long.MIN_VALUE。
我們以三個例子來說明BigDecimal對于以上屬性的定義
BigDecimal b1 = new BigDecimal(“3.1415926”);
從Debug的結(jié)果看,intVal為空,因為無標(biāo)度值可以被壓縮存儲到intCompact中,precision表示有8個數(shù)字位,scale表示標(biāo)度為7
BigDecimal b2 = new BigDecimal(“31415926314159263141592631415926”);
intVal記錄的是無標(biāo)度值,這時候由于無標(biāo)度值超過了Long.MAX_VALUE,intCompact存儲了Long.MIN_VALUE,precision表示當(dāng)前數(shù)字位為32個,scale為0表示沒有小數(shù)位。
MathContext mc3 = new MathContext(30,RoundingMode.HALF_UP); BigDecimal b2 = new BigDecimal(“31415926314159263141592631415926”);
在這里我們手動設(shè)置了precision為30,所以最后兩位被丟棄并執(zhí)行了舍入操作,同時scale記錄為-2表示無標(biāo)度值表示到小數(shù)點左邊兩位。
通過上面三個例子我們對BigDecimal的5個基本屬性總結(jié)如下。
BigDecimal是通過unscaled value和scale來構(gòu)造,同時使用Long.MAX_VALUE作為我們是否壓縮的閾值。當(dāng)unscaled value超過閾值時采用intVal字段存儲unscaled value,intCompact字段存儲Long.MIN_VALUE,否則對unscaled value進行壓縮存儲到long型的intCompact字段用于后續(xù)計算,intVal為空。
scale字段存儲標(biāo)度,可以理解為unscaled value最后一位到實際值小數(shù)點的距離。如例1中對于3.1415926來說unscaled value為31415926,最后一位6到實際值的小數(shù)點距離為7,scale記為7;對于例3中手動設(shè)置precision的情況,unscaled value為31415926xxx159的最后一位9到實際值31415926xxx15900的小數(shù)點距離為2,由于在小數(shù)點左邊scale則記為-2。
precision字段記錄的是unscaled value的數(shù)字個數(shù),當(dāng)手動指定MathContext并且指定的precision小于實際precision的時候,會要求進行rounding操作。
提到如何創(chuàng)建一個BigDecimal,首先想到的肯定是使用String參數(shù)的構(gòu)造函數(shù)進行構(gòu)建。
BigDecimal b = new BigDecimal(“3.14”);
實際上對于對象創(chuàng)建來說,BigDecimal提供了至少三種方式:
1, 構(gòu)造函數(shù)
BigDecimal提供了16個public的構(gòu)造函數(shù),支持通過char數(shù)組,String,double,BigInteger,long和int類型的參數(shù)構(gòu)造。
2, 工廠方法
BigDecimal主要通過valueOf方法提供對象的靜態(tài)工廠,支持通過double,BigInteger和long類型的參數(shù)構(gòu)造。具體用法:
BigDecimal f = BigDecimal.valueOf(1000L);
3, 對象緩存
對于常用的BigDecimal對象,內(nèi)部通過數(shù)組進行緩存,并開放了ZERO,ONE和TEN三個對象供使用端復(fù)用。具體用法:
BigDecimal c = BigDecimal.ZERO;
接下來具體看看三種創(chuàng)建方式的實現(xiàn)方式。
首先看看BigDecimal類提供的私有構(gòu)造函數(shù)。
/** * Trusted package private constructor. * Trusted simply means if val is INFLATED, intVal could not be null and * if intVal is null, val could not be INFLATED. */ BigDecimal(BigInteger intVal, long val, int scale, int prec) { this.scale = scale; this.precision = prec; this.intCompact = val; this.intVal = intVal; }
從這個私有構(gòu)造函數(shù)可以看出BigDecimal對象主要關(guān)注的屬性字段,如果可以準(zhǔn)確的給這些屬性字段賦值則可以成功構(gòu)造一個BigDecimal對象。
這里我們可以大膽猜測其他公共的構(gòu)造函數(shù)和工廠方法內(nèi)部的邏輯都是計算這些屬性字段。
從我們的脈絡(luò)圖上看,構(gòu)造函數(shù)分為字符構(gòu)造和數(shù)值構(gòu)造。
對于字符構(gòu)造我們只需要關(guān)注兩個構(gòu)造函數(shù)即可:
1, public BigDecimal(char[] in, int offset, int len, MathContext mc)
從規(guī)模上看這個構(gòu)造函數(shù)是所有字符構(gòu)造函數(shù)中方法體最大的,同時結(jié)合其他字符構(gòu)造函數(shù)的邏輯可以發(fā)現(xiàn)這個構(gòu)造函數(shù)正是字符構(gòu)造函數(shù)的核心邏輯實現(xiàn)。
2, public BigDecimal(String val)
之所以關(guān)注這個構(gòu)造函數(shù),一方面是實際應(yīng)用的比較多,再者這個構(gòu)造函數(shù)的100行注釋也表明了官方對于這個構(gòu)造函數(shù)的推薦程度。
接下來我們集中攻克字符構(gòu)造函數(shù)的核心實現(xiàn),我們結(jié)合源代碼以程序流的方式進行說明。
第一步:處理符號位,如果是符號位則設(shè)置isneg字段并將offset往后移動一位
// handle the sign boolean isneg = false; // assume positive if (in[offset] == '-') { isneg = true; // leading minus means negative offset++; len--; } else if (in[offset] == '+') { // leading + allowed offset++; len--; }
第二步,針對可壓縮的情況,遍歷字符進行分別處理。
2 如果是字符0判斷了兩種情況來處理prec和compact value的賦值,主要解決”00”這種多個0的無意義輸入。
1) 第一位數(shù)字為0,則直接將prec設(shè)置為1
2) 非第一位數(shù)字為0,則判斷之前的數(shù)值是否為0,如果為0則表明前面的數(shù)字是0,當(dāng)前數(shù)字不予處理;如果不為0則將數(shù)值乘以10,prec加1
if ((c == '0')) { // have zero if (prec == 0) prec = 1; else if (rs != 0) { rs *= 10; ++prec; } // else digit is a redundant leading zero if (dot) ++scl; }
2 如果是字符1-9的情況,同樣處理了prec和compact value的賦值,主要考慮解決”01”這種以0開頭的數(shù)字的prec問題。
else if ((c >= '1' && c <= '9')) { // have digit int digit = c - '0'; if (prec != 1 || rs != 0) ++prec; // prec unchanged if preceded by 0s rs = rs * 10 + digit; if (dot) ++scl; }
2 如果是字符”.”的情況,主要解決出現(xiàn)了多個小數(shù)點的情況。
2 如果是Unicode或者其他格式的字符表示,通過Character.isDigit方法進行判斷,判斷完并完成轉(zhuǎn)換后將上面0和1-9的邏輯再走一遍,有點重復(fù)代碼的嫌疑。
2 如果是字符”e”和”E”,解析出e后面的數(shù)字用于后面計算scale
第三步,結(jié)合之前字符解析得到的prec和MathContext設(shè)置的prec進行rounding操作。主要邏輯是通過相差的prec算出一個drop,然后使用compact value和drop去做除法,比如需要drop 3位,那么就拿compact value和1000去做除法,并結(jié)合Rounding Mode判斷結(jié)果是否需要加1。
由于rounding之后可能存在進位問題,這里使用while循環(huán)來進行檢查。
int mcp = mc.precision; int drop = prec - mcp; if (mcp > 0 && drop > 0) { // do rounding while (drop > 0) { scl = checkScaleNonZero((long) scl - drop); rs = divideAndRound(rs, LONG_TEN_POWERS_TABLE[drop], mc.roundingMode.oldMode); prec = longDigitLength(rs); drop = prec - mcp; } }
第四步,針對不可壓縮的情況,引入一個char數(shù)組容器用于構(gòu)建BigInteger類型的intValue。其他對于字符的處理以及如何設(shè)置prec,scale以及如何處理rounding和數(shù)值可壓縮的情況基本一致。
至此我們對于字符構(gòu)造函數(shù)的分析已經(jīng)結(jié)束,我們可以發(fā)現(xiàn)對于String類型的構(gòu)造函數(shù),我們其實是首先將String轉(zhuǎn)換成數(shù)組類型char[],然后調(diào)用字符數(shù)組構(gòu)造函數(shù)。所以出于性能考慮,如果我們的應(yīng)用場景里面獲取的是char[],可以直接調(diào)用字符數(shù)組構(gòu)造函數(shù),沒有必要先轉(zhuǎn)成String再去調(diào)用String構(gòu)造函數(shù),以至于白白損耗了兩次轉(zhuǎn)換的性能。
在數(shù)值構(gòu)造函數(shù)中,我們重點關(guān)注double類型的構(gòu)造函數(shù),因為這是在日常使用中最容易出問題的地方。
其他構(gòu)造函數(shù)的主要邏輯重點在于rounding和對于四個核心屬性的賦值,這點可以在字符構(gòu)造函數(shù)和后續(xù)的重點方法介紹中找到相應(yīng)的實現(xiàn)解析。
下面就讓我們集中火力攻克double構(gòu)造函數(shù)吧,同樣也是源代碼結(jié)合程序流的方式。
第一步,將double轉(zhuǎn)換成IEEE 754定義的浮點數(shù)bit表示方式,并通過位運算獲取到三個部分的值。
其中轉(zhuǎn)換成bit表示方式的方法是調(diào)用的虛擬機的native方法。
獲取sign的值比較好理解,右移63位后判斷值是否為0來確定數(shù)值的正負(fù)。
int sign = ((valBits >> 63) == 0 ? 1 : -1);
對于exponent和significand的邏輯就比較復(fù)雜了,首先明確目標(biāo)是將這個double表示為以下格式val == sign * significand * 2^exponent,再來看代碼:
int exponent = (int) ((valBits >> 52) & 0x7ffL); long significand = (exponent == 0 ? (valBits & ((1L << 52) - 1)) << 1 : (valBits & ((1L << 52) - 1)) | (1L << 52)); exponent -= 1075;
要看懂這段代碼我們首先需要了解IEE754在浮點數(shù)轉(zhuǎn)換的幾點約定:
2 小數(shù)點左邊隱含一位,通常是1
2 單精度偏移量127,雙精度偏移量是1023
這時候回頭來看這段代碼,在計算significand的時候分成了兩種情況,當(dāng)exponent為0的時候直接進行左移右邊補0否則在左邊補1,都是為了補齊52個有效位和一個隱含位。
exponent需要偏移1075 = 1023 + 52,來源于自身的1023偏移量加上52位的有效位偏移。
第二步,將significand進行格式化,去除低位的0
while ((significand & 1) == 0) { // i.e., significand is even significand >>= 1; exponent++; }
第三步,計算intVal和scale
BigInteger intVal; long compactVal = sign * significand; if (exponent == 0) { intVal = (compactVal == INFLATED) ? INFLATED_BIGINT : null; } else { if (exponent < 0) { intVal = BigInteger.valueOf(5).pow(-exponent).multiply(compactVal); scale = -exponent; } else { // (exponent > 0) intVal = BigInteger.valueOf(2).pow(exponent).multiply(compactVal); } compactVal = compactValFor(intVal); }
計算的時候按照exponent分成三種情況,
exponent==0,直接計算intVal
exponent<0,表明存在小數(shù)位,由于二進制數(shù)0.1對應(yīng)的十進制為0.5,所以小數(shù)位的轉(zhuǎn)換是5作為底
exponent>0,表明需要要在右邊補充0,二進制數(shù)1.0對應(yīng)的十進制為2,所以整數(shù)位的轉(zhuǎn)換是2作為底。
第四步,根據(jù)MathContext進行rounding操作,獲取precision,intValue和compact value。這一步是通用操作,就不做過多表述。
至此對于數(shù)值構(gòu)造函數(shù)的分析已經(jīng)結(jié)束。我們主要分析了double類型的構(gòu)造函數(shù),從代碼和程序流程可以看出double類型的構(gòu)造函數(shù)首先將double轉(zhuǎn)換成IEEE標(biāo)準(zhǔn)的二進制表示形式并分離出符號位、指數(shù)位和有效位,然后計算出precision、scale、intVal和compactVal來表示一個BigDecimal。由于小數(shù)轉(zhuǎn)二進制存在誤差導(dǎo)致了這個構(gòu)造函數(shù)構(gòu)造出的BigDecimal對象和實際值之間存在誤差,這也是為什么double類型的構(gòu)造函數(shù)不推薦使用的原因。
BigDecimal的工廠函數(shù)是通過靜態(tài)的valueOf方法提供的,主要針對long,BigInteger和double類型的參數(shù)。
由于long和BigInteger的數(shù)據(jù)類型和BigDecimal中的intValue和intCompact匹配,所以對于這兩種類型的工廠方法實現(xiàn)相對簡單,主要就是四個屬性的賦值。
而在double類型的工廠方法中,使用了和構(gòu)造函數(shù)完全不同的構(gòu)造邏輯:
public static BigDecimal valueOf(double val) { // Reminder: a zero double returns '0.0', so we cannot fastpath // to use the constant ZERO. This might be important enough to // justify a factory approach, a cache, or a few private // constants, later. return new BigDecimal(Double.toString(val)); }
這里通過調(diào)用Double的toString方法首先將double轉(zhuǎn)換成字符串然后再調(diào)用字符構(gòu)造函數(shù),從而避免了精度丟失的問題,所以在注釋中也提示了使用者:如果一定要用double來構(gòu)造BigDecimal對象優(yōu)先使用工廠方法。