<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【項(xiàng)目實(shí)踐】商業(yè)計(jì)算怎樣才能保證精度不丟失

          共 4770字,需瀏覽 10分鐘

           ·

          2021-01-28 23:46

          以項(xiàng)目驅(qū)動(dòng)學(xué)習(xí),以實(shí)踐檢驗(yàn)真知

          前言

          很多系統(tǒng)都有「處理金額」的需求,比如電商系統(tǒng)、財(cái)務(wù)系統(tǒng)、收銀系統(tǒng),等等。只要和錢扯上關(guān)系,就不得不打起十二萬分精神來對(duì)待,一分一毫都不能出錯(cuò),否則對(duì)系統(tǒng)和用戶來說都是災(zāi)難。

          保證金額的準(zhǔn)確性主要有兩個(gè)方面:溢出精度。溢出是指存儲(chǔ)數(shù)據(jù)的空間得充足,不能金額較大就存儲(chǔ)不下了。精度是指計(jì)算金額時(shí)不能有偏差,多一點(diǎn)少一點(diǎn)都不行。

          溢出問題大家都知道如何解決,選擇位數(shù)長的數(shù)值類型即可,即不用 floatdouble 。而精度問題,double 就無法解決了,因?yàn)楦↑c(diǎn)數(shù)會(huì)導(dǎo)致精度丟失。

          我們來直觀感受一下精度丟失:

          double?money?=?1.0?-?0.9;

          這個(gè)運(yùn)算結(jié)果誰都知道該為 0.1,然而實(shí)際結(jié)果卻是 0.09999999999999998。出現(xiàn)這個(gè)現(xiàn)象是因?yàn)橛?jì)算機(jī)底層是二進(jìn)制運(yùn)算,而二進(jìn)制并不能精準(zhǔn)表示十進(jìn)制小數(shù)。所以在商業(yè)計(jì)算等精確計(jì)算中要使用其他數(shù)據(jù)類型來保證精度不丟失,一定不要使用浮點(diǎn)數(shù)。

          本螃蟹接下來會(huì)詳細(xì)講解在實(shí)際開發(fā)中到底該怎樣進(jìn)行商業(yè)計(jì)算,并將所有代碼和 SQL 語句放在了 Github 上(文末有地址),克隆下來即可運(yùn)行。

          解決方案

          有兩種數(shù)據(jù)類型可以滿足商業(yè)計(jì)算的需求,第一個(gè)自然是專為商業(yè)計(jì)算而設(shè)計(jì)的 Decimal 類型,第二個(gè)則是定長整數(shù)

          Decimal

          關(guān)于數(shù)據(jù)類型的選擇,一要考慮數(shù)據(jù)庫,二要考慮編程語言。即數(shù)據(jù)庫中用什么類型來存儲(chǔ)數(shù)據(jù),代碼中用什么類型來處理數(shù)據(jù)

          數(shù)據(jù)庫層面自然是用 decimal 類型,因?yàn)樵擃愋筒淮嬖诰葥p失的情況,用它來進(jìn)行商業(yè)計(jì)算再合適不過。

          將字段定義為 decimal 的語法為 decimal(M,N)M 代表存儲(chǔ)多少位,N 代表小數(shù)存儲(chǔ)多少位。假設(shè) decimal(20,2),則代表一共存儲(chǔ) 20 位數(shù)值,其中小數(shù)占 2 位。

          我們新建一張用戶表,字段很簡單就兩個(gè),主鍵和余額:

          33849b859849c851053904fd8cba754a.webp

          這里小數(shù)位置保留 2 點(diǎn),代表金額只存儲(chǔ)到,實(shí)際項(xiàng)目中存儲(chǔ)到什么單位得根據(jù)業(yè)務(wù)需求來定,都是可以的。

          數(shù)據(jù)庫層面搞定了咱們來看代碼層面,在 Java 中對(duì)應(yīng)數(shù)據(jù)庫 decimal 的是 java.math.BigDecimal類型,它自然也能保證精度完全準(zhǔn)確。

          要?jiǎng)?chuàng)建BigDecimal主要有三種方法:

          BigDecimal?d1?=?new?BigDecimal(0.1);?//?BigDecimal(double?val)
          BigDecimal?d2?=?new?BigDecimal("0.1");?//?BigDecimal(String?val)
          BigDecimal?d3?=?BigDecimal.valueOf(0.1);?//?static?BigDecimal?valueOf(double?val)

          前面兩個(gè)是構(gòu)造函數(shù),后面一個(gè)是靜態(tài)方法。這三種方法都非常方便,但第一種方法禁止使用!看一下這三個(gè)對(duì)象各自的打印結(jié)果就知道為什么了:

          d1:?0.1000000000000000055511151231257827021181583404541015625
          d2:?0.1
          d3:?0.1

          第一種方法通過構(gòu)造函數(shù)傳入 double 類型的參數(shù)并不能精確地獲取到值,若想正確的創(chuàng)建 BigDecimal,要么將 double 轉(zhuǎn)換為字符串然后調(diào)用構(gòu)造方法,要么直接調(diào)用靜態(tài)方法。事實(shí)上,靜態(tài)方法內(nèi)部也是將 double 轉(zhuǎn)換為字符串然后調(diào)用的構(gòu)造方法:

          5c8d8535f7490512cb59a7fa82f031f1.webp

          如果是從數(shù)據(jù)庫中查詢出小數(shù)值,或者前端傳遞過來小數(shù)值,數(shù)據(jù)會(huì)準(zhǔn)確映射成 BigDecimal 對(duì)象,這一點(diǎn)我們不用操心。

          說完創(chuàng)建,接下來就要說最重要的數(shù)值運(yùn)算。運(yùn)算無非就是加減乘除,這些 BigDecimal 都提供了對(duì)應(yīng)的方法:

          BigDecimal?add(BigDecimal);?//?加
          BigDecimal?subtract(BigDecimal);?//?減
          BigDecimal?multiply(BigDecimal);?//?乘
          BigDecimal?divide(BigDecimal);?//?除

          BigDecimal 是不可變對(duì)象,意思就是這些操作都不會(huì)改變?cè)袑?duì)象的值,方法執(zhí)行完畢只會(huì)返回一個(gè)新的對(duì)象。若要運(yùn)算后更新原有值,只能重新賦值:

          d1?=?d1.subtract(d2);

          口說無憑,我們來驗(yàn)證一下精度是否會(huì)丟失 :

          BigDecimal?d1?=?new?BigDecimal("1.0");
          BigDecimal?d2?=?new?BigDecimal("0.9");
          System.out.println(d1.subtract(d2));

          輸出結(jié)果毫無疑問為 ?0.1

          代碼方面已經(jīng)能保證精度不會(huì)丟失,但數(shù)學(xué)方面除法可能會(huì)出現(xiàn)除不盡的情況。比如我們運(yùn)算 10 除以 3,會(huì)拋出如下異常:

          9c0b4f7564e2372512c62e9beaca5b6c.webp
          為了解決除不盡后導(dǎo)致的無窮小數(shù)問題,我們需要人為去控制小數(shù)的精度。除法運(yùn)算還有一個(gè)方法就是用來控制精度的:
          BigDecimal?divide(BigDecimal?divisor,?int?scale,?int?roundingMode)

          scale 參數(shù)表示運(yùn)算后保留幾位小數(shù),roundingMode 參數(shù)表示計(jì)算小數(shù)的方式。

          BigDecimal?d1?=?new?BigDecimal("1.0");
          BigDecimal?d2?=?new?BigDecimal("3");
          System.out.println(d1.divide(d2,?2,?RoundingMode.DOWN));?//?小數(shù)精度為2,多余小數(shù)直接舍去。輸出結(jié)果為0.33

          RoundingMode 枚舉能夠方便地指定小數(shù)運(yùn)算方式,除了直接舍去,還有四舍五入、向上取整等多種方式,根據(jù)具體業(yè)務(wù)需求指定即可。

          注意,小數(shù)精度盡量在代碼中控制,不要通過數(shù)據(jù)庫來控制。數(shù)據(jù)庫中默認(rèn)采用四舍五入的方式保留小數(shù)精度。

          比如數(shù)據(jù)庫中設(shè)置的小數(shù)精度為 2,我存入 0.335,那么最終存儲(chǔ)的值就會(huì)變?yōu)?0.34

          我們已經(jīng)知道如何創(chuàng)建和運(yùn)算 BigDecimal 對(duì)象,只剩下最后一個(gè)操作:比較。因?yàn)槠洳皇腔緮?shù)據(jù)類型,用雙等號(hào) == 肯定是不行的,那我們來試試用 equals比較:

          BigDecimal?d1?=?new?BigDecimal("0.33");
          BigDecimal?d2?=?new?BigDecimal("0.3300");
          System.out.println(d1.equals(d2));?//?false

          輸出結(jié)果為 false,因?yàn)?BigDecimalequals 方法不光會(huì)比較值,還會(huì)比較精度,就算值一樣但精度不一樣結(jié)果也是 false。若想判斷值是否一樣,需要使用int compareTo(BigDecimal val)方法:

          BigDecimal?d1?=?new?BigDecimal("0.33");
          BigDecimal?d2?=?new?BigDecimal("0.3300");
          System.out.println(d1.compareTo(d2)?==?0);?//?true

          d1 大于 d2,返回 1

          d1 小于 d2,返回 -1

          兩值相等,返回 0

          BigDecimal 的用法就介紹到這,我們接下來看第二種解決方案。

          定長整數(shù)

          定長整數(shù),顧名思義就是固定(小數(shù))長度的整數(shù)。它只是一個(gè)概念,并不是新的數(shù)據(jù)類型,我們使用的還是普通的整數(shù)。

          金額好像理所應(yīng)當(dāng)有小數(shù),但稍加思考便會(huì)發(fā)覺小數(shù)并非是必須的。之前我們演示的金額單位是1.55 就是一元五角五分。那如果我們單位是,一元五角五分的值就會(huì)變成 15.5。如果再將單位縮小到,值就為 155。沒錯(cuò),只要達(dá)到最小單位,小數(shù)完全可以省略!這個(gè)最小單位根據(jù)業(yè)務(wù)需求來定,比如系統(tǒng)要求精確到,那么值就是1550。當(dāng)然,一般精確到分就可以了,咱們接下來演示單位都是分。

          咱們現(xiàn)在新建一個(gè)字段,類型為 bigint,單位為分:

          66f5fe0ec61e94e8cef4a592169c4add.webp

          代碼中對(duì)應(yīng)的數(shù)據(jù)類型自然是 Long。基本類型的數(shù)值運(yùn)算我們是再熟悉不過的了,直接使用運(yùn)算操作符即可:

          long?d1?=?10000L;?//?100元
          d1?+=?500L;?//?加五元
          d1?-=?500L;?//?減五元

          加和減沒什么好說的,乘和除可能會(huì)出現(xiàn)小數(shù)的情況,比如某個(gè)商品打八折,運(yùn)算就是乘以 0.8

          long?d1?=?2366L;?//?23.66元
          double?result?=?d1?*?0.8;?//?打八折,運(yùn)算后結(jié)果為1892.8
          d1?=?(long)result;?//?轉(zhuǎn)換為整數(shù),舍去所有小數(shù),值為1892。即18.92元

          進(jìn)行小數(shù)運(yùn)算,類型自然而然就會(huì)變?yōu)楦↑c(diǎn)數(shù),所以我們還要將浮點(diǎn)數(shù)轉(zhuǎn)換為整數(shù)。

          強(qiáng)轉(zhuǎn)會(huì)將所有小數(shù)舍去,這個(gè)舍去并不代表精度丟失。業(yè)務(wù)要求最小單位是什么,就只保留什么,低于分的單位我們壓根沒必要保存。這一點(diǎn)和 BigDecimal 是一致的,如果系統(tǒng)中只需要到分,那小數(shù)精度就為 2, 剩余的小數(shù)都舍去。

          不過有些業(yè)務(wù)計(jì)算可能要求四舍五入等其他操作,這一點(diǎn)我們可以通過 Math類來完成:

          long?d1?=?2366L;?//?23.66元
          double?result?=?d1?*?0.8;?//?運(yùn)算后結(jié)果為1892.8
          d1?=?(long)result;?//?強(qiáng)轉(zhuǎn)舍去所有小數(shù),值為1892
          d1?=?(long)Math.ceil(result);?//?向上取整,值為1893
          d1?=?(long)Math.round(result);?//?四舍五入,值為1893
          ...

          再來看除法運(yùn)算。當(dāng)整數(shù)除以整數(shù)時(shí),會(huì)自動(dòng)舍去所有小數(shù):

          long?d1?=?2366L;
          long?result?=?d1?/?3;?//?正確的值本應(yīng)該為788.6666666666666,舍去所有小數(shù),最終值為788

          如果要進(jìn)行四舍五入等其他小數(shù)操作,則運(yùn)算時(shí)先進(jìn)行浮點(diǎn)數(shù)運(yùn)算,然后再轉(zhuǎn)換成整數(shù):

          long?d1?=?2366L;
          double?result?=?d1?/?3.0;?//?注意,這里除以不是?3,而是?3.0?浮點(diǎn)數(shù)
          d1?=?(long)Math.round(result);?//?四射勿入,最終值為789,即7.89元

          雖說數(shù)據(jù)庫存儲(chǔ)和代碼運(yùn)算都是整數(shù),但前端顯示時(shí)若還是以為單位就對(duì)用戶不太友好了。所以后端將值傳遞給前端后,前端需要自行將值除以 100,以為單位展示給用戶。然后前端傳值給后端時(shí),還是以約定好的整數(shù)傳遞。

          0c980c145114b31c07c4eb2c17a16c23.webp收尾

          關(guān)于金額處理就講解完畢了。我們學(xué)會(huì)了兩個(gè)商業(yè)計(jì)算方案:

          • Decimal 類型
          • 定長整數(shù)

          其實(shí)商業(yè)計(jì)算并沒有什么技術(shù)難度,但如果沒有正確處理則會(huì)導(dǎo)致難以估量的損失,畢竟和錢相關(guān)的事都不是小事。

          本文為了方便大家理解,所以省略了前后端聯(lián)調(diào)以及數(shù)據(jù)庫操作的內(nèi)容。但既然是項(xiàng)目實(shí)踐,那就得有一個(gè)完整項(xiàng)目,所以本螃蟹基于 Spring Boot 搭建了一個(gè)完整的 Web 項(xiàng)目,數(shù)據(jù)庫操作和接口都已寫好,SQL 語句也有,Github 倉庫地址:

          https://github.com/RudeCrab/rude-java

          克隆下來即可感受在真實(shí)項(xiàng)目中如何運(yùn)用的本文知識(shí)。倉庫中還有許多其他項(xiàng)目實(shí)踐,涵蓋各個(gè)業(yè)務(wù)各個(gè)功能,其中一些模塊的質(zhì)量甚至可以單開一個(gè)倉庫,讓你再也不用尋找各個(gè)框架 Demo 和腳手架。歡迎 star,螃蟹會(huì)更新更多項(xiàng)目實(shí)踐的!

          最近掘金弄了一個(gè)投票活動(dòng),希望大家能為我投上幾票。掃碼或者點(diǎn)擊原文即可參與投票,可以投多次哦~謝謝大家了

          左下角點(diǎn)擊原文也可參與投票

          瀏覽 36
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久爱视颊在线观看 | 无码人妻AV一 | 在线观看黄片网站 | 黄片叉蛋的视频在线播放免费看 | 中文字幕在线观看第一页 |