1. 程式人生 > >Java之道系列:BigDecimal如何解決浮點數精度問題

Java之道系列:BigDecimal如何解決浮點數精度問題

IEEE 754

IEEE二進位制浮點數算術標準(IEEE 754)是20世紀80年代以來最廣泛使用的浮點數運算標準,為許多CPU與浮點運算器所採用。這個標準定義了表示浮點數的格式(包括負零-0)與反常值(denormal number)),一些特殊數值(無窮(Inf)與非數值(NaN)),以及這些數值的“浮點數運算子”;它也指明瞭四種數值舍入規則和五種異常狀況(包括異常發生的時機與處理方式)。

下面我們就以雙精度,也就是double型別,為例來看看浮點數的格式。

sign exponent fraction
1位 11位 52位
63 62-52
實際的指數大小+1023
51-0

下面看個栗子,直接輸出double型別的二進位制表示,

    public static void main(String[] args) {
        printBits(3.5);
    }

    private static void printBits(double d) {
        System.out.println("##"+d);
        long l = Double.doubleToLongBits(d);
        String bits = Long.toBinaryString(l);
        int
len = bits.length(); System.out.println(bits+"#"+len); if(len == 64) { System.out.println("[63]"+bits.charAt(0)); System.out.println("[62-52]"+bits.substring(1,12)); System.out.println("[51-0]"+bits.substring(12, 64)); } else { System.out.println("[63]0"
); System.out.println("[62-52]"+ pad(bits.substring(0, len - 52))); System.out.println("[51-0]"+bits.substring(len-52, len)); } } private static String pad(String exp) { int len = exp.length(); if(len == 11) { return exp; } else { StringBuilder sb = new StringBuilder(); for (int i = 11-len; i > 0; i--) { sb.append("0"); } sb.append(exp); return sb.toString(); } }
##3.5
100000000001100000000000000000000000000000000000000000000000000#63
[63]0
[62-52]10000000000
[51-0]1100000000000000000000000000000000000000000000000000

指數大小為10000000000B-1023=1,尾數為1.11B,所以實際數值大小為11.1B=3.5,妥妥的。
有一點需要注意的是上述格式為歸約形式,所以尾數的整數部分為1,而當非歸約形式時,尾數的整數部分是為0的。

0.1 Orz

上面我們使用的浮點數3.5剛好可以準確的用二進位制來表示,21+20+21,但並不是所有的小數都可以用二進位制來表示,例如,0.1

    public static void main(String[] args) {
        printBits(0.1);
    }
##0.1
11111110111001100110011001100110011001100110011001100110011010#62
[63]0
[62-52]01111111011
[51-0]1001100110011001100110011001100110011001100110011010

0.1無法表示成2x+2y+… 這樣的形式,尾數部分後面應該是1100一直迴圈下去(純屬猜測,不過這個應該也是可以證明的),但是由於計算機無法表示這樣的無限迴圈,所以就需要截斷,這就是浮點數的精度問題。精度問題會帶來一些unexpected的問題,例如0.1 + 0.1 + 0.1 == 0.3將會返回false,

    public static void main(String[] args) {
        System.out.println(0.1 + 0.1 == 0.2); // true
        System.out.println(0.1 + 0.1 + 0.1 == 0.3); // false
    }

那麼BigDecimal又是如何解決這個問題的?

BigDecimal的解決方案就是,不使用二進位制,而是使用十進位制(BigInteger)+小數點位置(scale)來表示小數,

    public static void main(String[] args) {
        BigDecimal bd = new BigDecimal("100.001");
        System.out.println(bd.scale());
        System.out.println(bd.unscaledValue());
    }

輸出,

3
100001

也就是100.001 = 100001 * 0.1^3。這種表示方式下,避免了小數的出現,當然也就不會有精度問題了。十進位制,也就是整數部分使用了BigInteger來表示,小數點位置只需要一個整數scale來表示就OK了。
當使用BigDecimal來進行運算時,也就可以分解成兩部分,BigInteger間的運算,以及小數點位置scale的更新,下面先看下運算過程中scale的更新。

scale

加法運算時,根據下面的公式scale更新為兩個BigDecimal中較大的那個scale即可。

X*0.1n + Y*0.1m == X*0.1n + (Y*0.1mn) * 0.1n == (X+Y*0.1mn) * 0.1n,其中n>m

相應的程式碼如下,

    /**
     * Returns a {@code BigDecimal} whose value is {@code (this +
     * augend)}, and whose scale is {@code max(this.scale(),
     * augend.scale())}.
     *
     * @param  augend value to be added to this {@code BigDecimal}.
     * @return {@code this + augend}
     */
    public BigDecimal add(BigDecimal augend) {
        long xs = this.intCompact;
        long ys = augend.intCompact;
        BigInteger fst = (xs != INFLATED) ? null : this.intVal;
        BigInteger snd = (ys != INFLATED) ? null : augend.intVal;
        int rscale = this.scale;

        long sdiff = (long)rscale - augend.scale;
        if (sdiff != 0) {
            if (sdiff < 0) {
                int raise = checkScale(-sdiff);
                rscale = augend.scale;
                if (xs == INFLATED ||
                    (xs = longMultiplyPowerTen(xs, raise)) == INFLATED)
                    fst = bigMultiplyPowerTen(raise);
            } else {
                int raise = augend.checkScale(sdiff);
                if (ys == INFLATED ||
                    (ys = longMultiplyPowerTen(ys, raise)) == INFLATED)
                    snd = augend.bigMultiplyPowerTen(raise);
            }
        }
        if (xs != INFLATED && ys != INFLATED) {
            long sum = xs + ys;
            // See "Hacker's Delight" section 2-12 for explanation of
            // the overflow test.
            if ( (((sum ^ xs) & (sum ^ ys))) >= 0L) // not overflowed
                return BigDecimal.valueOf(sum, rscale);
        }
        if (fst == null)
            fst = BigInteger.valueOf(xs);
        if (snd == null)
            snd = BigInteger.valueOf(ys);
        BigInteger sum = fst.add(snd);
        return (fst.signum == snd.signum) ?
            new BigDecimal(sum, INFLATED, rscale, 0) :
            new BigDecimal(sum, rscale);
    }

乘法運算根據下面的公式也可以確定scale更新為兩個scale之和。

X*0.1n * Y*0.1m == (X*Y)*0.1n+m

相應的程式碼,

    /**
     * Returns a {@code BigDecimal} whose value is <tt>(this &times;
     * multiplicand)</tt>, and whose scale is {@code (this.scale() +
     * multiplicand.scale())}.
     *
     * @param  multiplicand value to be multiplied by this {@code BigDecimal}.
     * @return {@code this * multiplicand}
     */
    public BigDecimal multiply(BigDecimal multiplicand) {
        long x = this.intCompact;
        long y = multiplicand.intCompact;
        int productScale = checkScale((long)scale + multiplicand.scale);

        // Might be able to do a more clever check incorporating the
        // inflated check into the overflow computation.
        if (x != INFLATED && y != INFLATED) {
            /*
             * If the product is not an overflowed value, continue
             * to use the compact representation.  if either of x or y
             * is INFLATED, the product should also be regarded as
             * an overflow. Before using the overflow test suggested in
             * "Hacker's Delight" section 2-12, we perform quick checks
             * using the precision information to see whether the overflow
             * would occur since division is expensive on most CPUs.
             */
            long product = x * y;
            long prec = this.precision() + multiplicand.precision();
            if (prec < 19 || (prec < 21 && (y == 0 || product / y == x)))
                return BigDecimal.valueOf(product, productScale);
            return new BigDecimal(BigInteger.valueOf(x).multiply(y), INFLATED,
                                  productScale, 0);
        }
        BigInteger rb;
        if (x == INFLATED && y == INFLATED)
            rb = this.intVal.multiply(multiplicand.intVal);
        else if (x != INFLATED)
            rb = multiplicand.intVal.multiply(x);
        else
            rb = this.intVal.multiply(y);
        return new BigDecimal(rb, INFLATED, productScale, 0);
    }

BigInteger可以表示任意精度的整數。當你使用long型別進行運算,可能會產生溢位時就要考慮使用BigInteger了。BigDecimal就使用了BigInteger作為backend。
那麼BigInteger是如何做到可以表示任意精度的整數的?答案是使用陣列來表示,看下面這個栗子就很直觀了,

    public static void main(String[] args) {
        byte[] mag = {
                2, 1 // 10 00000001 == 513
        };
        System.out.println(new BigInteger(mag));
    }

通過byte[]來當作底層的二進位制表示,例如栗子中的[2, 1],也就是[00000010B, 00000001B],就是表示二進位制的10 00000001B這個數,也就是513了。
BigInteger內部會將這個byte[]轉換成int[]儲存,程式碼在stripLeadingZeroBytes方法,

    /**
     * Translates a byte array containing the two's-complement binary
     * representation of a BigInteger into a BigInteger.  The input array is
     * assumed to be in <i>big-endian</i> byte-order: the most significant
     * byte is in the zeroth element.
     *
     * @param  val big-endian two's-complement binary representation of
     *         BigInteger.
     * @throws NumberFormatException {@code val} is zero bytes long.
     */
    public BigInteger(byte[] val) {
        if (val.length == 0)
            throw new NumberFormatException("Zero length BigInteger");

        if (val[0] < 0) {
            mag = makePositive(val);
            signum = -1;
        } else {
            mag = stripLeadingZeroBytes(val);
            signum = (mag.length == 0 ? 0 : 1);
        }
    }
    /**
     * Returns a copy of the input array stripped of any leading zero bytes.
     */
    private static int[] stripLeadingZeroBytes(byte a[]) {
        int byteLength = a.length;
        int keep;

        // Find first nonzero byte
        for (keep = 0; keep < byteLength && a[keep]==0; keep++)
            ;

        // Allocate new array and copy relevant part of input array
        int intLength = ((byteLength - keep) + 3) >>> 2;
        int[] result = new int[intLength];
        int b = byteLength - 1;
        for (int i = intLength-1; i >= 0; i--) {
            result[i] = a[b--] & 0xff;
            int bytesRemaining = b - keep + 1;
            int bytesToTransfer = Math.min(3, bytesRemaining);
            for (int j=8; j <= (bytesToTransfer << 3); j += 8)
                result[i] |= ((a[b--] & 0xff) << j);
        }
        return result;
    }

上面也可以看到這個byte[]應該是big-endian two's-complement binary representation
那麼為什麼建構函式不直接讓我們扔一個int[]進去就得了呢,還要這麼轉換一下?答案是因為Java的整數都是有符號整數,舉個栗子,int型別沒辦法表示2321,也就是32位上全都是1這個數的,這時候用byte[]得這麼寫,(byte)255,(byte)255,(byte)255,(byte)255,這樣才能表示32個1。

最後來看看BigInteger間的加法與乘法運算。

add

程式碼如下,

    private static int[] add(int[] x, int[] y) {
        // If x is shorter, swap the two arrays
        if (x.length < y.length) {
            int[] tmp = x;
            x = y;
            y = tmp;
        }

        int xIndex = x.length;
        int yIndex = y.length;
        int result[] = new int[xIndex];
        long sum = 0;

        // Add common parts of both numbers
        while(yIndex > 0) {
            // 最低位對齊再開始加
            sum = (x[--xIndex] & LONG_MASK) +
                  (y[--yIndex] & LONG_MASK) + (sum >>> 32); // sum>>>32 是高32位,也就是進位
            result[xIndex] = (int)sum; // 低32位直接儲存
        }

        // Copy remainder of longer number while carry propagation is required
        boolean carry = (sum >>> 32 != 0);
        while (xIndex > 0 && carry) // x比y長,且最後還有進位
            carry = ((result[--xIndex] = x[xIndex] + 1) == 0); // 一位一位往前進位,直到沒有產生進位

        // Copy remainder of longer number
        while (xIndex > 0)
            result[--xIndex] = x[xIndex];

        // Grow result if necessary
        if (carry) {
            int bigger[] = new int[result.length + 1];
            System.arraycopy(result, 0, bigger, 1, result.length);
            bigger[0] = 0x01;
            return bigger;
        }
        return result;
    }

加法運算比較簡單,就是模擬十進位制加法運算的過程,從兩個加數的最低位開始加,如果有進位就進位。

multiply

程式碼如下,

    private int[] multiplyToLen(int[] x, int xlen, int[] y, int ylen, int[] z) {
        int xstart = xlen - 1;
        int ystart = ylen - 1;

        if (z == null || z.length < (xlen+ ylen))
            z = new int[xlen+ylen];

        long carry = 0;
        for (int j=ystart, k=ystart+1+xstart; j>=0; j--, k--) {
            long product = (y[j] & LONG_MASK) *
                           (x[xstart] & LONG_MASK) + carry;
            z[k] = (int)product;
            carry = product >>> 32;
        }
        z[xstart] = (int)carry;

        for (int i = xstart-1; i >= 0; i--) {
            carry = 0;
            for (int j=ystart, k=ystart+1+i; j>=0; j--, k--) {
                long product = (y[j] & LONG_MASK) *
                               (x[i] & LONG_MASK) +
                               (z[k] & LONG_MASK) + carry;
                z[k] = (int)product;
                carry = product >>> 32;
            }
            z[i] = (int)carry;
        }
        return z;
    }

乘法運算要複雜一點,不過也一樣是模擬十進位制乘法運算,也就是一個乘數的每一位與另一個乘數的每一位相乘再相加(乘法運算可以拆成加法運算),所以才有那個雙重的for迴圈。
最後的最後,想說的一點是,其實BigInteger可以看成是232進位制的計數表示,這樣就比較容易理解上面的加法跟乘法運算了。至於為什麼是232進位制?自己再想想哈^_^

參考資料

相關推薦

Java系列BigDecimal如何解決點數精度問題

IEEE 754 IEEE二進位制浮點數算術標準(IEEE 754)是20世紀80年代以來最廣泛使用的浮點數運算標準,為許多CPU與浮點運算器所採用。這個標準定義了表示浮點數的格式(包括負零-0)與反常值(denormal number)),一些

BigDecimal解決精度丟失

一、問題:(1)在電商網站計算中,價錢計算是重要一環,那麼價錢是如何計算的,先來看:可以看到如果用Long型別計算,會導致問題。比如我買一件0.01元和0.05元的商品,我手裡一共0.06元,是可以購買的,但是付款的時候卻會發現錢不夠,因為Long型別,0.01+0.05等於

Java 安全技術探索系列J2SE安全架構】安全管理器

一 安全管理器的功能 安全管理器是一個允許程式實現安全策略的類,它會在執行階段檢查需要保護的資源的訪問許可權及其它規定的操作許可權,保護系統免受惡意操作攻擊,以達到系統的安全策略。 安全管理器負責檢查的操作主要包括以下幾個: 建立一個新的類載入器

Java安全技術探索系列Java可擴充套件安全架構】之一Java可擴充套件安全架構開篇

【Java安全技術探索之路系列:Java可擴充套件安全架構】章節目錄 Java平臺使用基於標準的安全的API技術提供可擴充套件的安全架構模型,這些API技術提供了平臺獨立性,是不同廠商之間能夠進行互操作。這些API技術通過技術整合來支援加密演算法、

Java 安全技術探索系列J2SE安全架構】類載入器

【Java 安全技術探索之路系列:J2SE安全架構】章節列表 一 類載入器的作用 1.1 名字空間的隔離(Name Space Separation) 把名字空間隔離以防止有意或無意的名字衝突問題。 1.2 包邊界保護(Packa

Java安全技術探索系列Java可擴充套件安全架構】十四JAAS(一)JAAS架構介紹

【Java安全技術探索之路系列:Java可擴充套件安全架構】章節目錄 JAAS,即Java認證和授權服務。 認證是通過驗證使用者或裝置的身份來判斷真實性和可信賴性的過程。 授權是根據提出請求的身份被授予的許可權來提供訪問資源或執行功能的許可權。

Java 安全技術探索系列J2SE安全架構】安全管理工具

【Java 安全技術探索之路系列:J2SE安全架構】章節列表 作為J2SE複合包的一部分,Java2平臺提供了一組工具來管理安全策略、建立金鑰、管理金鑰和證書、簽署JAR檔案、驗證簽名以及支援金鑰管理相關的其他功能。 一 keystore 金鑰

Java程式碼保護探索系列程式碼加密】之一程式碼加密開篇

程式碼加密也是對Java程式碼進行保護的一種重要方式,作為Java程式碼加密開篇的文章,本文先舉例介紹,如何利用加密演算法實現對.class檔案進行加密。注意為說明基本原理,本文程式採用命令列進行操作,後續會給出具有UI介面的Java類加密軟體。 一

深入Java集合學習系列HashSet的實現原理

是否 abstract arc html 源代碼 cat param body static 0.參考文獻 深入Java集合學習系列:HashSet的實現原理 1.HashSet概述:   HashSet實現Set接口,由哈希表(實際上是一個HashMap實例)支持。它

Java總結篇系列Java多線程(二)

文章 睡眠 blog setdeamon java多線程 cep public pan level Java總結篇系列:Java多線程(二) 本文承接上一篇文章《Java總結篇系列:Java多線程(一)》。 四.Java多線程的阻塞狀態與線程控制 上文已經提到Jav

Java總結篇系列Java多線程(一)

常見 而是 同時 private 狀態 過程 運行時 不同的 bstr Java總結篇系列:Java多線程(一) 多線程作為Java中很重要的一個知識點,在此還是有必要總結一下的。 一.線程的生命周期及五種基本狀態 關於Java中線程的生命周期,首先看一下下面這張較

Java工具類判斷對象是否為空或null

sar 判斷 ins == span urn lean color style 1 import java.lang.reflect.Array; 2 import java.util.Collection; 3 import java.util.Map; 4

Java總結篇系列Java泛型

ech internal clone 傳遞 sta 是什麽 dom bar 依然 一. 泛型概念的提出(為什麽需要泛型)? 首先,我們看下下面這段簡短的代碼: 1 public class GenericTest { 2 3 public static

3星|《實戰復盤第四季·商業巨頭們的變革GE、TCL、力拓集團、英美資源集團等企業總裁的變更經驗

tar 表現 哈佛商業評論 運動 選擇 方法 -c 團隊 文章 實戰復盤第四季·商業巨頭們的變革之道(《哈佛商業評論》增刊) 本期是《哈佛商業評論》“實戰復盤”欄目的10篇文章,講的是GE、TCL、力拓集團、英美資源集團等企業如何

java點數精度損失原理和解決

我所在的公司近期要做一個打賞的功能,比如說發一張照片其他人可以對這張照片進行打賞,給些小錢。我的工作是負責給客戶端下發打賞訊息。工作完工之後客戶端同學說有個問題,我下發的打賞金額是string型別的,他們覺得double才對。於是我就去找老大問這個能不能改成double型別,老大說這個應該

程式碼整潔1反轉“if”語句減少巢狀

程式碼片段1:("if"巢狀) void PrintName(Person p) { if (p != null) { if (p.Name != null) { Console.WriteLine(p.Name); } } } 程式碼片段2:

Javahttp請求亂碼問題解決

連接 tco get div gbk readline url prop safari 這周由於項目需要請求一個接口,獲取數據,反復嘗試,請求的數據始終亂碼。這裏簡單的總結一下解決亂碼的幾個方法。 首先,需要註意的是編碼方式的一致,其次對方怎麽編碼,接收方怎麽解碼即可。 先

java(二) 從HelloWorld開始

老生常談的開始 對程式有些許瞭解的人大概都知道“HelloWorld”,這幾乎是所有語言的第一個程式,大部分人也是從這個簡單的執行結果揭開程式的面紗。而對於行業“老鳥”而言,可以從這一個程式的寫法看出不同語言的些許共同點,從而可以更快的入手另外一門語

java(一)學習的方法

行java之道(一)學習的心得 自序 我是一名普通的JAVA開發從業者,接下來一段時間我會更新一些自己的心得體會,之所以想要這麼做,一是因為自己早有將自己的心得體會記錄下來的願景;二是因為自己在近來招聘中所遇見的應聘者誇誇其談框架,卻對基礎答非所問

Java總結篇系列Java String

1 public static main([Ljava/lang/String;)V 2 L0 3 LINENUMBER 5 L0 4 LDC "aa" 5 ASTORE 1 6 L1 7 LINENUMBER 6 L1 8 LDC "bb" 9