亚洲激情专区-91九色丨porny丨老师-久久久久久久女国产乱让韩-国产精品午夜小视频观看

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

BigDecimal的介紹和使用

發布時間:2020-05-26 15:38:11 來源:億速云 閱讀:623 作者:鴿子 欄目:編程語言

對于Java開發人員來說,只要日常工作中涉及到算術運算,那必然會跟BigDecimal這個類打交道。也許我們可以記住一些使用的注意事項,如使用String的構造函數而不是double的構造函數來避免精度問題。但是對于一個5000行的龐然大物,僅僅了解兩個構造函數還不足以支撐我們大規模應用的信念,好在源代碼對我們是完全開放的,那不妨來一次源代碼的親密接觸。

 

按照Java的慣例在每個重要類前面都有一篇論文式的注釋,一般情況下把這段理解了應付個面試是沒啥問題的。BigDecimal也不例外的在類注釋上花了近200行,我們做個簡單的摘要:

2  首先給出BigDecimal的定義為任意精度的有符號十進制數。BigDecimal可以表示為一個任意精度的無刻度值和一個32位整型的刻度。

2  BigDecimal提供了一系列的方法,如算術操作、標度控制、舍入、比較等等方法,總之很強大。

2  BigDecimal通過precisionscalerounding modeMathContext類來控制標度和進行舍入操作。

2  BigDecimalequals方法并不是數學意義上的相等,所以在用于Sorted MapSorted Set這些和比較有關系的數據結構時需要特別小心。

 

在論文注釋的指引下,我們可以整理出BigDecimal類的脈絡:

BigDecimal的介紹和使用

 

接下來我們就順著脈絡一點點的解剖這個龐然大物了。


基本屬性

從圖中可以看出BigDecimal類主要需要關注5個主要屬性

?  intValscale

分別表示BigDecimal的無標度值和標度,結合我們在注釋里看到的說法“BigDecimal可以表示為一個任意精度的無刻度值和一個32位整型的刻度”,這兩個屬性可以認為是BigDecimal類的骨架。

?  precision

BigDecimal中數字的個數,在確定了precision后就會要求結合Rounding Mode做一些舍入方面的操作。

?  stringCache

BigDecimal的字符表示,在toString方法的時候用到。

?  intCompact

無標度值的Long表示,方便后續計算。如果intValcompact的過程發現超過Long.MAX_VALUE則將intCompact記為Long.MIN_VALUE

我們以三個例子來說明BigDecimal對于以上屬性的定義

BigDecimal b1 = new BigDecimal(“3.1415926”);

BigDecimal的介紹和使用

Debug的結果看,intVal為空,因為無標度值可以被壓縮存儲到intCompact中,precision表示有8個數字位,scale表示標度為7

BigDecimal b2 = new BigDecimal(“31415926314159263141592631415926”);

BigDecimal的介紹和使用

intVal記錄的是無標度值,這時候由于無標度值超過了Long.MAX_VALUEintCompact存儲了Long.MIN_VALUEprecision表示當前數字位為32個,scale0表示沒有小數位。

MathContext mc3 = new MathContext(30,RoundingMode.HALF_UP);
BigDecimal b2 = new BigDecimal(“31415926314159263141592631415926”);

BigDecimal的介紹和使用 

在這里我們手動設置了precision30,所以最后兩位被丟棄并執行了舍入操作,同時scale記錄為-2表示無標度值表示到小數點左邊兩位。

         

通過上面三個例子我們對BigDecimal5個基本屬性總結如下。

BigDecimal是通過unscaled valuescale來構造,同時使用Long.MAX_VALUE作為我們是否壓縮的閾值。當unscaled value超過閾值時采用intVal字段存儲unscaled valueintCompact字段存儲Long.MIN_VALUE,否則對unscaled value進行壓縮存儲到long型的intCompact字段用于后續計算,intVal為空。

scale字段存儲標度,可以理解為unscaled value最后一位到實際值小數點的距離。如例1中對于3.1415926來說unscaled value31415926,最后一位6到實際值的小數點距離為7scale記為7;對于例3中手動設置precision的情況,unscaled value31415926xxx159的最后一位9到實際值31415926xxx15900的小數點距離為2,由于在小數點左邊scale則記為-2

precision字段記錄的是unscaled value的數字個數,當手動指定MathContext并且指定的precision小于實際precision的時候,會要求進行rounding操作。

 

創建函數

提到如何創建一個BigDecimal,首先想到的肯定是使用String參數的構造函數進行構建。

BigDecimal b = new BigDecimal(“3.14”);

實際上對于對象創建來說,BigDecimal提供了至少三種方式:

1, 構造函數

BigDecimal提供了16public的構造函數,支持通過char數組,StringdoubleBigIntegerlongint類型的參數構造。

2, 工廠方法

BigDecimal主要通過valueOf方法提供對象的靜態工廠,支持通過doubleBigIntegerlong類型的參數構造。具體用法:

BigDecimal f = BigDecimal.valueOf(1000L);

3, 對象緩存

對于常用的BigDecimal對象,內部通過數組進行緩存,并開放了ZEROONETEN三個對象供使用端復用。具體用法:

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判斷了兩種情況來處理preccompact value的賦值,主要解決”00”這種多個0的無意義輸入。

1) 第一位數字為0,則直接將prec設置為1

2) 非第一位數字為0,則判斷之前的數值是否為0,如果為0則表明前面的數字是0,當前數字不予處理;如果不為0則將數值乘以10prec1

                    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的情況,同樣處理了preccompact 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方法進行判斷,判斷完并完成轉換后將上面01-9的邏輯再走一遍,有點重復代碼的嫌疑。

2  如果是字符”e””E”,解析出e后面的數字用于后面計算scale

 

第三步,結合之前字符解析得到的precMathContext設置的prec進行rounding操作。主要邏輯是通過相差的prec算出一個drop,然后使用compact valuedrop去做除法,比如需要drop 3位,那么就拿compact value1000去做除法,并結合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。其他對于字符的處理以及如何設置precscale以及如何處理rounding和數值可壓縮的情況基本一致。

 

至此我們對于字符構造函數的分析已經結束,我們可以發現對于String類型的構造函數,我們其實是首先將String轉換成數組類型char[],然后調用字符數組構造函數。所以出于性能考慮,如果我們的應用場景里面獲取的是char[],可以直接調用字符數組構造函數,沒有必要先轉成String再去調用String構造函數,以至于白白損耗了兩次轉換的性能。

數值構造函數

在數值構造函數中,我們重點關注double類型的構造函數,因為這是在日常使用中最容易出問題的地方。

其他構造函數的主要邏輯重點在于rounding和對于四個核心屬性的賦值,這點可以在字符構造函數和后續的重點方法介紹中找到相應的實現解析。

 

下面就讓我們集中火力攻克double構造函數吧,同樣也是源代碼結合程序流的方式。

 

第一步,將double轉換成IEEE 754定義的浮點數bit表示方式,并通過位運算獲取到三個部分的值。

BigDecimal的介紹和使用

其中轉換成bit表示方式的方法是調用的虛擬機的native方法。

 

獲取sign的值比較好理解,右移63位后判斷值是否為0來確定數值的正負。

int sign = ((valBits >> 63) == 0 ? 1 : -1);

 

對于exponentsignificand的邏輯就比較復雜了,首先明確目標是將這個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的時候分成了兩種情況,當exponent0的時候直接進行左移右邊補0否則在左邊補1,都是為了補齊52個有效位和一個隱含位。

exponent需要偏移1075 = 1023 + 52,來源于自身的1023偏移量加上52位的有效位偏移。

 

第二步,將significand進行格式化,去除低位的0

        while ((significand & 1) == 0) { // i.e., significand is even
            significand >>= 1;
            exponent++;
        }

 

第三步,計算intValscale

        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操作,獲取precisionintValuecompact value。這一步是通用操作,就不做過多表述。

 

至此對于數值構造函數的分析已經結束。我們主要分析了double類型的構造函數,從代碼和程序流程可以看出double類型的構造函數首先將double轉換成IEEE標準的二進制表示形式并分離出符號位、指數位和有效位,然后計算出precisionscaleintValcompactVal來表示一個BigDecimal。由于小數轉二進制存在誤差導致了這個構造函數構造出的BigDecimal對象和實際值之間存在誤差,這也是為什么double類型的構造函數不推薦使用的原因。

 

工廠函數

BigDecimal的工廠函數是通過靜態的valueOf方法提供的,主要針對longBigIntegerdouble類型的參數。

由于longBigInteger的數據類型和BigDecimal中的intValueintCompact匹配,所以對于這兩種類型的工廠方法實現相對簡單,主要就是四個屬性的賦值。

而在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));
    }

這里通過調用DoubletoString方法首先將double轉換成字符串然后再調用字符構造函數,從而避免了精度丟失的問題,所以在注釋中也提示了使用者:如果一定要用double來構造BigDecimal對象優先使用工廠方法。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

北安市| 友谊县| 乐业县| 如东县| 枣庄市| 荆门市| 靖远县| 东乌珠穆沁旗| 陕西省| 开阳县| 陆川县| 太仆寺旗| 木兰县| 泸水县| 肥西县| 屏边| 罗定市| 通化县| 常德市| 虎林市| 西峡县| 志丹县| 鹤岗市| 杭州市| 北辰区| 晋江市| 台州市| 宁化县| 庆安县| 桐城市| 遂昌县| 张家港市| 泉州市| 石楼县| 东乡族自治县| 荥阳市| 陈巴尔虎旗| 五寨县| 大荔县| 宁津县| 收藏|