您好,登錄后才能下訂單哦!
對于Java開發人員來說,只要日常工作中涉及到算術運算,那必然會跟BigDecimal這個類打交道。也許我們可以記住一些使用的注意事項,如使用String的構造函數而不是double的構造函數來避免精度問題。但是對于一個5000行的龐然大物,僅僅了解兩個構造函數還不足以支撐我們大規模應用的信念,好在源代碼對我們是完全開放的,那不妨來一次源代碼的親密接觸。
按照Java的慣例在每個重要類前面都有一篇論文式的注釋,一般情況下把這段理解了應付個面試是沒啥問題的。BigDecimal也不例外的在類注釋上花了近200行,我們做個簡單的摘要:
2 首先給出BigDecimal的定義為任意精度的有符號十進制數。BigDecimal可以表示為一個任意精度的無刻度值和一個32位整型的刻度。
2 BigDecimal提供了一系列的方法,如算術操作、標度控制、舍入、比較等等方法,總之很強大。
2 BigDecimal通過precision、scale、rounding mode和MathContext類來控制標度和進行舍入操作。
2 BigDecimal的equals方法并不是數學意義上的相等,所以在用于Sorted Map和Sorted Set這些和比較有關系的數據結構時需要特別小心。
在論文注釋的指引下,我們可以整理出BigDecimal類的脈絡:
接下來我們就順著脈絡一點點的解剖這個龐然大物了。
從圖中可以看出BigDecimal類主要需要關注5個主要屬性
? intVal和scale
分別表示BigDecimal的無標度值和標度,結合我們在注釋里看到的說法“BigDecimal可以表示為一個任意精度的無刻度值和一個32位整型的刻度”,這兩個屬性可以認為是BigDecimal類的骨架。
? precision
BigDecimal中數字的個數,在確定了precision后就會要求結合Rounding Mode做一些舍入方面的操作。
? stringCache
BigDecimal的字符表示,在toString方法的時候用到。
? intCompact
無標度值的Long表示,方便后續計算。如果intVal在compact的過程發現超過Long.MAX_VALUE則將intCompact記為Long.MIN_VALUE。
我們以三個例子來說明BigDecimal對于以上屬性的定義
BigDecimal b1 = new BigDecimal(“3.1415926”);
從Debug的結果看,intVal為空,因為無標度值可以被壓縮存儲到intCompact中,precision表示有8個數字位,scale表示標度為7
BigDecimal b2 = new BigDecimal(“31415926314159263141592631415926”);
intVal記錄的是無標度值,這時候由于無標度值超過了Long.MAX_VALUE,intCompact存儲了Long.MIN_VALUE,precision表示當前數字位為32個,scale為0表示沒有小數位。
MathContext mc3 = new MathContext(30,RoundingMode.HALF_UP); BigDecimal b2 = new BigDecimal(“31415926314159263141592631415926”);
在這里我們手動設置了precision為30,所以最后兩位被丟棄并執行了舍入操作,同時scale記錄為-2表示無標度值表示到小數點左邊兩位。
通過上面三個例子我們對BigDecimal的5個基本屬性總結如下。
BigDecimal是通過unscaled value和scale來構造,同時使用Long.MAX_VALUE作為我們是否壓縮的閾值。當unscaled value超過閾值時采用intVal字段存儲unscaled value,intCompact字段存儲Long.MIN_VALUE,否則對unscaled value進行壓縮存儲到long型的intCompact字段用于后續計算,intVal為空。
scale字段存儲標度,可以理解為unscaled value最后一位到實際值小數點的距離。如例1中對于3.1415926來說unscaled value為31415926,最后一位6到實際值的小數點距離為7,scale記為7;對于例3中手動設置precision的情況,unscaled value為31415926xxx159的最后一位9到實際值31415926xxx15900的小數點距離為2,由于在小數點左邊scale則記為-2。
precision字段記錄的是unscaled value的數字個數,當手動指定MathContext并且指定的precision小于實際precision的時候,會要求進行rounding操作。
提到如何創建一個BigDecimal,首先想到的肯定是使用String參數的構造函數進行構建。
BigDecimal b = new BigDecimal(“3.14”);
實際上對于對象創建來說,BigDecimal提供了至少三種方式:
1, 構造函數
BigDecimal提供了16個public的構造函數,支持通過char數組,String,double,BigInteger,long和int類型的參數構造。
2, 工廠方法
BigDecimal主要通過valueOf方法提供對象的靜態工廠,支持通過double,BigInteger和long類型的參數構造。具體用法:
BigDecimal f = BigDecimal.valueOf(1000L);
3, 對象緩存
對于常用的BigDecimal對象,內部通過數組進行緩存,并開放了ZERO,ONE和TEN三個對象供使用端復用。具體用法:
BigDecimal c = BigDecimal.ZERO;
接下來具體看看三種創建方式的實現方式。
首先看看BigDecimal類提供的私有構造函數。
/** * 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; }
從這個私有構造函數可以看出BigDecimal對象主要關注的屬性字段,如果可以準確的給這些屬性字段賦值則可以成功構造一個BigDecimal對象。
這里我們可以大膽猜測其他公共的構造函數和工廠方法內部的邏輯都是計算這些屬性字段。
從我們的脈絡圖上看,構造函數分為字符構造和數值構造。
對于字符構造我們只需要關注兩個構造函數即可:
1, public BigDecimal(char[] in, int offset, int len, MathContext mc)
從規模上看這個構造函數是所有字符構造函數中方法體最大的,同時結合其他字符構造函數的邏輯可以發現這個構造函數正是字符構造函數的核心邏輯實現。
2, public BigDecimal(String val)
之所以關注這個構造函數,一方面是實際應用的比較多,再者這個構造函數的100行注釋也表明了官方對于這個構造函數的推薦程度。
接下來我們集中攻克字符構造函數的核心實現,我們結合源代碼以程序流的方式進行說明。
第一步:處理符號位,如果是符號位則設置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) 第一位數字為0,則直接將prec設置為1
2) 非第一位數字為0,則判斷之前的數值是否為0,如果為0則表明前面的數字是0,當前數字不予處理;如果不為0則將數值乘以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開頭的數字的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 如果是字符”.”的情況,主要解決出現了多個小數點的情況。
2 如果是Unicode或者其他格式的字符表示,通過Character.isDigit方法進行判斷,判斷完并完成轉換后將上面0和1-9的邏輯再走一遍,有點重復代碼的嫌疑。
2 如果是字符”e”和”E”,解析出e后面的數字用于后面計算scale
第三步,結合之前字符解析得到的prec和MathContext設置的prec進行rounding操作。主要邏輯是通過相差的prec算出一個drop,然后使用compact value和drop去做除法,比如需要drop 3位,那么就拿compact value和1000去做除法,并結合Rounding Mode判斷結果是否需要加1。
由于rounding之后可能存在進位問題,這里使用while循環來進行檢查。
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數組容器用于構建BigInteger類型的intValue。其他對于字符的處理以及如何設置prec,scale以及如何處理rounding和數值可壓縮的情況基本一致。
至此我們對于字符構造函數的分析已經結束,我們可以發現對于String類型的構造函數,我們其實是首先將String轉換成數組類型char[],然后調用字符數組構造函數。所以出于性能考慮,如果我們的應用場景里面獲取的是char[],可以直接調用字符數組構造函數,沒有必要先轉成String再去調用String構造函數,以至于白白損耗了兩次轉換的性能。
在數值構造函數中,我們重點關注double類型的構造函數,因為這是在日常使用中最容易出問題的地方。
其他構造函數的主要邏輯重點在于rounding和對于四個核心屬性的賦值,這點可以在字符構造函數和后續的重點方法介紹中找到相應的實現解析。
下面就讓我們集中火力攻克double構造函數吧,同樣也是源代碼結合程序流的方式。
第一步,將double轉換成IEEE 754定義的浮點數bit表示方式,并通過位運算獲取到三個部分的值。
其中轉換成bit表示方式的方法是調用的虛擬機的native方法。
獲取sign的值比較好理解,右移63位后判斷值是否為0來確定數值的正負。
int sign = ((valBits >> 63) == 0 ? 1 : -1);
對于exponent和significand的邏輯就比較復雜了,首先明確目標是將這個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在浮點數轉換的幾點約定:
2 小數點左邊隱含一位,通常是1
2 單精度偏移量127,雙精度偏移量是1023
這時候回頭來看這段代碼,在計算significand的時候分成了兩種情況,當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,表明存在小數位,由于二進制數0.1對應的十進制為0.5,所以小數位的轉換是5作為底
exponent>0,表明需要要在右邊補充0,二進制數1.0對應的十進制為2,所以整數位的轉換是2作為底。
第四步,根據MathContext進行rounding操作,獲取precision,intValue和compact value。這一步是通用操作,就不做過多表述。
至此對于數值構造函數的分析已經結束。我們主要分析了double類型的構造函數,從代碼和程序流程可以看出double類型的構造函數首先將double轉換成IEEE標準的二進制表示形式并分離出符號位、指數位和有效位,然后計算出precision、scale、intVal和compactVal來表示一個BigDecimal。由于小數轉二進制存在誤差導致了這個構造函數構造出的BigDecimal對象和實際值之間存在誤差,這也是為什么double類型的構造函數不推薦使用的原因。
BigDecimal的工廠函數是通過靜態的valueOf方法提供的,主要針對long,BigInteger和double類型的參數。
由于long和BigInteger的數據類型和BigDecimal中的intValue和intCompact匹配,所以對于這兩種類型的工廠方法實現相對簡單,主要就是四個屬性的賦值。
而在double類型的工廠方法中,使用了和構造函數完全不同的構造邏輯:
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)); }
這里通過調用Double的toString方法首先將double轉換成字符串然后再調用字符構造函數,從而避免了精度丟失的問題,所以在注釋中也提示了使用者:如果一定要用double來構造BigDecimal對象優先使用工廠方法。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。