數(shù)誤差到實(shí)戰(zhàn)應(yīng)用)
開頭要寫得像有經(jīng)驗(yàn)的Java開發(fā)者在分享經(jīng)驗(yàn)。我做過(guò)多個(gè)金融相關(guān)的項(xiàng)目每次接手和金額有關(guān)的模塊都要先看一眼代碼里用的是 double 還是 BigDecimal。這個(gè)習(xí)慣的由來(lái)是一次線上事故——一個(gè)賬務(wù)系統(tǒng)用 double 算利息季度結(jié)算時(shí)對(duì)不上賬差了幾分錢排查了一整天最后定位到是浮點(diǎn)數(shù)精度導(dǎo)致的問(wèn)題。也正是從那次之后我對(duì) BigDecimal 的態(tài)度從會(huì)用變成了研究透。作為 Java 基礎(chǔ)篇里幾乎必考、也幾乎必用的一塊內(nèi)容BigDecimal 是真正考驗(yàn)基本功的知識(shí)點(diǎn)它不復(fù)雜但坑很多。這篇內(nèi)容我從原理講到實(shí)戰(zhàn)把用到過(guò)的細(xì)節(jié)、踩過(guò)的坑、面試?yán)锍1粏?wèn)到的點(diǎn)一次性梳理清楚。1. 為什么非用 BigDecimal 不可從 0.10.2 說(shuō)起1.1 浮點(diǎn)數(shù)精度丟失的根源先做一個(gè)最簡(jiǎn)單、也最經(jīng)典的實(shí)驗(yàn)在你的 IDE 里跑一下這段代碼System.out.println(0.1 0.2); System.out.println(1.0 - 0.9); System.out.println(0.5 * 3);輸出結(jié)果會(huì)讓你懷疑人生0.30000000000000004 0.09999999999999998 1.5前兩行都出現(xiàn)了詭異的尾巴第三行卻正常。原因在于計(jì)算機(jī)底層用二進(jìn)制存儲(chǔ)數(shù)據(jù)而二進(jìn)制并不能精確表示所有十進(jìn)制小數(shù)。0.1 轉(zhuǎn)換成二進(jìn)制后是一個(gè)無(wú)限循環(huán)小數(shù)類似于十進(jìn)制里 1/3 無(wú)法被有限表示double 只能取其近似值。0.1 0.2時(shí)兩個(gè)近似值相加誤差就疊加暴露出來(lái)了。1.5為什么沒(méi)問(wèn)題因?yàn)?0.5 正好等于 2 的負(fù)一次方二進(jìn)制可以精確表示。這也是浮點(diǎn)數(shù)精度問(wèn)題的核心規(guī)律只有能用 2 的冪次和表示的小數(shù)才是精確的其他都是近似值。1.2 float/double 在業(yè)務(wù)場(chǎng)景中為什么失控如果你只把 float/double 用在科學(xué)計(jì)算或者游戲物理引擎里誤差通常是可接受的。但一旦進(jìn)入業(yè)務(wù)系統(tǒng)情況完全不同金額計(jì)算單價(jià)×數(shù)量得到的價(jià)格用戶會(huì)拿著計(jì)算器和你對(duì)賬任何一分錢的偏差都會(huì)觸發(fā)客訴。統(tǒng)計(jì)報(bào)表百分比、平均值、增長(zhǎng)率連續(xù)運(yùn)算后誤差會(huì)不斷累積放大最后報(bào)表數(shù)據(jù)和數(shù)據(jù)庫(kù)明細(xì)對(duì)不上。計(jì)費(fèi)系統(tǒng)按分鐘計(jì)費(fèi)、按流量計(jì)費(fèi)每筆誤差看起來(lái)微小乘以海量用戶就是巨大的資金差異。數(shù)值比較if (balance targetBalance)這種直接等值判斷在浮點(diǎn)數(shù)面前是不可靠的誤差將導(dǎo)致正確業(yè)務(wù)被誤判。在這些場(chǎng)景里需要的是一個(gè)能精確表示小數(shù)并且可控舍入的類型。Java 給出的標(biāo)準(zhǔn)答案就是 BigDecimal。它的思路很直接不采用二進(jìn)制近似表示而是用一個(gè)大整數(shù)unscaled value加上一個(gè)小數(shù)位刻度scale完整還原出十進(jìn)制數(shù)值。0.1在 BigDecimal 里被存成整數(shù)1和刻度1本質(zhì)含義是1 × 10?1這是精確的。一句話總結(jié)凡是和錢、比例、精確計(jì)算有關(guān)的場(chǎng)景放棄原生浮點(diǎn)類型統(tǒng)一上 BigDecimal。2. 構(gòu)造 BigDecimal一半的坑集中在這里2.1 三種構(gòu)造方式對(duì)比BigDecimal 提供了多種構(gòu)造方式選擇哪一種是最容易踩坑的地方。先看這段真實(shí)的坑BigDecimal a new BigDecimal(0.1); System.out.println(a); // 輸出0.1000000000000000055511151231257827021181583404541015625有沒(méi)有發(fā)現(xiàn)問(wèn)題明明寫的是0.1得到的卻不是 0.1。原因在于new BigDecimal(double)接收的是 double 的二進(jìn)制近似值然后把這個(gè)近似值如實(shí)轉(zhuǎn)換為 BigDecimal。double 表示的 0.1 本身就是那個(gè)一長(zhǎng)串尾巴的近似值于是結(jié)果就污染了。而換成字符串構(gòu)造結(jié)果立刻清爽BigDecimal b new BigDecimal(0.1); System.out.println(b); // 輸出0.1字符串構(gòu)造會(huì)嚴(yán)格按照字符串的內(nèi)容解析得到精確的十進(jìn)制結(jié)果。所以業(yè)界有一條鐵律不要用 new BigDecimal(double構(gòu)造優(yōu)先使用 new BigDecimal(String或 BigDecimal.valueOf()。那BigDecimal.valueOf(0.1)為什么也是安全的看一下源碼就不難理解public static BigDecimal valueOf(double val) { return new BigDecimal(Double.toString(val)); }valueOf內(nèi)部先把 double 轉(zhuǎn)成字符串再走字符串構(gòu)造路徑所以繞開了二進(jìn)制近似值的陷阱。這實(shí)際上就是官方幫你封了一層安全轉(zhuǎn)換。我把三種方式整理成一個(gè)對(duì)比表方便記憶構(gòu)造方式示例結(jié)果安全性new BigDecimal(double)new BigDecimal(0.1)0.1000000000000000055511151231257827021181583404541015625不安全new BigDecimal(String)new BigDecimal(0.1)0.1安全BigDecimal.valueOf(double)BigDecimal.valueOf(0.1)0.1安全2.2 從源碼理解 BigDecimal 的內(nèi)部結(jié)構(gòu)理解 BigDecimal 的精度機(jī)制關(guān)鍵在于它內(nèi)部的兩個(gè)核心字段intVal或者 Java 高版本中直接使用BigInteger表示未縮放值和scale。用公式表示就是BigDecimal 數(shù)值 unscaledValue × 10^(-scale)舉個(gè)例子new BigDecimal(123.45)中unscaledValue 是整數(shù)12345scale 是2也就是12345 × 10?2。new BigDecimal(0.1)則是1 × 10?1。這套設(shè)計(jì)讓任意十進(jìn)制小數(shù)都能被精確表示不再依賴二進(jìn)制近似。這也解釋了為什么equals方法會(huì)比較 scale兩個(gè)數(shù)值相同但 scale 不同的 BigDecimal在你眼里可能是同一個(gè)數(shù)在它自己看來(lái)身份不同。比如1.0是10 × 10?11.00是100 × 10?2數(shù)值相等但內(nèi)部表示不同。這個(gè)細(xì)節(jié)極其重要后面我會(huì)單獨(dú)展開講。從 JDK 9 開始 BigDecimal 的內(nèi)部實(shí)現(xiàn)做過(guò)升級(jí)核心存儲(chǔ)換成了BigInteger但對(duì)外行為和概念模型不變。理解unscaledValue scale這套模型就理解了 BigDecimal 的一切行為。3. 加減乘除與舍入規(guī)則業(yè)務(wù)里 90% 的操作在這里3.1 add/subtract/multiply 基本用法與鏈?zhǔn)秸{(diào)用BigDecimal 的加減乘操作接口非常直觀分別是add、subtract、multiply。關(guān)鍵要養(yǎng)成一個(gè)習(xí)慣每次運(yùn)算都要接收返回值因?yàn)樗且粋€(gè)不可變對(duì)象運(yùn)算結(jié)果不會(huì)修改調(diào)用者本身而是返回一個(gè)新對(duì)象。BigDecimal price new BigDecimal(19.9); BigDecimal count new BigDecimal(3); BigDecimal total price.multiply(count); System.out.println(total); // 輸出59.7 BigDecimal taxRate new BigDecimal(0.06); BigDecimal tax total.multiply(taxRate).setScale(2, RoundingMode.HALF_UP); System.out.println(tax); // 輸出3.58第二行里我用了鏈?zhǔn)秸{(diào)用multiply(taxRate)返回一個(gè)新 BigDecimal再繼續(xù)調(diào)用setScale。這種寫法在工作中很常見(jiàn)要注意順序別亂。好多初學(xué)者寫鏈?zhǔn)秸{(diào)用時(shí)會(huì)忘掉中間結(jié)果沒(méi)有接收導(dǎo)致后面取到的還是舊對(duì)象排查半天才發(fā)現(xiàn)是這回事。對(duì)于加法減法用法完全對(duì)稱。特別提醒一點(diǎn)如果要對(duì)金額做累加比如統(tǒng)計(jì)一批訂單的總金額推薦先初始化一個(gè)BigDecimal.ZERO然后循環(huán)往里面加BigDecimal sum BigDecimal.ZERO; for (Order order : orderList) { sum sum.add(order.getAmount()); }注意每一輪都要重新賦值給sum這是不可變對(duì)象的使用常識(shí)。有人會(huì)寫sum.add(order.getAmount())然后不接收循環(huán)結(jié)束后 sum 仍然是零這個(gè) bug 非常隱蔽。3.2 divide 的兩種形態(tài)與除不盡的異常除法是整個(gè) BigDecimal 中最容易出問(wèn)題的操作尤其是不指定精度時(shí)。看這個(gè)例子BigDecimal one new BigDecimal(1); BigDecimal three new BigDecimal(3); System.out.println(one.divide(three));運(yùn)行后直接拋出ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.因?yàn)?1 ÷ 3 的結(jié)果是無(wú)限循環(huán)小數(shù)BigDecimal 不知道你想保留多少位也不知道怎么舍入只能拋異常結(jié)束。解決方式是指定精度和舍入模式兩個(gè)參數(shù)幾乎是綁定出現(xiàn)的BigDecimal result one.divide(three, 4, RoundingMode.HALF_UP); System.out.println(result); // 輸出0.3333這個(gè)例子中4表示保留 4 位小數(shù)RoundingMode.HALF_UP表示四舍五入。Java 里除法的一個(gè)常見(jiàn)報(bào)錯(cuò)就是除不盡拋異常凡是代碼里出現(xiàn)divide的地方都應(yīng)該習(xí)慣性帶上 scale 和 RoundingMode。3.3 setScale 與七種舍入模式setScale是控制 BigDecimal 小數(shù)位數(shù)的核心方法。它有兩種常見(jiàn)形態(tài)bigDecimal.setScale(2); // 不指定舍入模式遇到無(wú)法精確表示時(shí)可能拋異常 bigDecimal.setScale(2, RoundingMode.HALF_UP); // 推薦指定舍入模式Java 8 開始RoundingMode取代了老的常量提供了 7 種模式。我把每種模式結(jié)合實(shí)際數(shù)值說(shuō)明白這樣你以后選型心里有數(shù)。舍入模式對(duì)正數(shù)的行為對(duì)負(fù)數(shù)的行為典型使用場(chǎng)景UP遠(yuǎn)離零方向舍入1.1→2遠(yuǎn)離零-1.1→-2極少用向上取整的場(chǎng)景DOWN趨向零舍入1.9→1趨向零-1.9→-1直接截?cái)嘈?shù)位CEILING向正無(wú)窮大方向1.1→2向正無(wú)窮大-1.1→-1結(jié)果不允許小于真實(shí)值FLOOR向負(fù)無(wú)窮大方向1.9→1向負(fù)無(wú)窮大-1.1→-2結(jié)果不允許大于真實(shí)值HALF_UP四舍五入2.5→3四舍五入-2.5→-3業(yè)務(wù)中最常用HALF_DOWN五舍六入2.5→2五舍六入-2.5→-2少見(jiàn)HALF_EVEN銀行家舍入2.5→23.5→4同左統(tǒng)計(jì)、金融算法中有時(shí)會(huì)用到業(yè)務(wù)系統(tǒng)里最常用的是HALF_UP理解起來(lái)就是小學(xué)學(xué)的四舍五入。HALF_EVEN銀行家舍入在金融統(tǒng)計(jì)中會(huì)用到它的原則是五入成雙當(dāng)數(shù)字正好處于中間值比如 2.5時(shí)舍入到相鄰的偶數(shù)目的是讓多次舍入產(chǎn)生的統(tǒng)計(jì)偏差趨于平衡。但普通計(jì)費(fèi)場(chǎng)景不需要這種高級(jí)策略HALF_UP是最穩(wěn)的選擇。注意setScale不止能縮小小數(shù)位數(shù)還能擴(kuò)大。比如new BigDecimal(3.1).setScale(3, RoundingMode.HALF_UP)的結(jié)果是3.100這個(gè)特性在需要對(duì)齊小數(shù)位做比較時(shí)很實(shí)用。4. 比較大小用 compareTo別用 equals4.1 equals 的 scale 陷阱接前面提到的內(nèi)部結(jié)構(gòu)來(lái)看一個(gè)讓人猝不及防的比較問(wèn)題BigDecimal a new BigDecimal(1.0); BigDecimal b new BigDecimal(1.00); System.out.println(a.equals(b)); // false System.out.println(a.compareTo(b) 0); // true數(shù)學(xué)上完全相等的兩個(gè)數(shù)equals卻返回 false。因?yàn)閑quals不僅比較數(shù)值還比較 scale。1.0的 scale 是 11.00的 scale 是 2內(nèi)部結(jié)構(gòu)不同結(jié)果就是 false。這是 BigDecimal 里最經(jīng)典的隱形坑之一。如果代碼里用equals判斷金額或者價(jià)格相等一旦兩個(gè)值的精度位數(shù)不一致即使數(shù)值相同也會(huì)被判為不同。我見(jiàn)過(guò)有人用數(shù)據(jù)庫(kù)查出一個(gè)1.00代碼里寫死1.0equals永遠(yuǎn)返回 false導(dǎo)致業(yè)務(wù)分支走錯(cuò)。排查了很久才發(fā)現(xiàn)問(wèn)題出在 scale 上。4.2 正確比較姿勢(shì)與排序應(yīng)用正確判斷數(shù)值大小的方式是compareTo它只比較數(shù)值本身忽略 scale 差異System.out.println(a.compareTo(b)); // 0表示相等 System.out.println(a.compareTo(new BigDecimal(2))); // -1表示小于 System.out.println(a.compareTo(new BigDecimal(0.5))); // 1表示大于compareTo的返回值語(yǔ)義和 Comparable 接口一致負(fù)數(shù)表示小于0 表示等于正數(shù)表示大于。這也讓 BigDecimal 天然支持排序。實(shí)際工作中常見(jiàn)的場(chǎng)景是列表按金額排序ListBigDecimal amounts Arrays.asList( new BigDecimal(39.90), new BigDecimal(129.00), new BigDecimal(9.90) ); amounts.sort(BigDecimal::compareTo); System.out.println(amounts); // 輸出[9.9, 39.90, 129.00]如果要在HashMap或HashSet里用 BigDecimal 當(dāng) key同樣要注意equals和hashCode是配套的。既然equals會(huì)對(duì)1.0和1.00返回 false那么它們?cè)?HashMap 中也會(huì)被當(dāng)作兩個(gè) key。如果業(yè)務(wù)上希望數(shù)值相等就視為相同 key最好統(tǒng)一精度后再入 Map或者用TreeMap配合compareTo來(lái)規(guī)避。4.3 判斷零值的正確姿勢(shì)判斷一個(gè) BigDecimal 是否為零很多人順手寫bigDecimal.equals(BigDecimal.ZERO)這又會(huì)踩到 scale 的坑。0.00在 equals 眼里和0不是同一個(gè)對(duì)象但業(yè)務(wù)上它們就是零。推薦兩種寫法bigDecimal.compareTo(BigDecimal.ZERO) 0; // 或者 bigDecimal.signum() 0;signum()方法返回 -1、0、1 三種狀態(tài)表示負(fù)數(shù)、零、正數(shù)效率高且沒(méi)有 scale 問(wèn)題。在判斷大于 0 時(shí)signum() 0也是一個(gè)很清晰的寫法。5. 格式化輸出與類型轉(zhuǎn)換toString 也可能出問(wèn)題5.1 科學(xué)計(jì)數(shù)法帶來(lái)的顯示問(wèn)題BigDecimal 在數(shù)值特別大或特別小時(shí)toString()會(huì)采用科學(xué)計(jì)數(shù)法展示??催@個(gè)例子BigDecimal number new BigDecimal(1E10); System.out.println(number.toString()); // 輸出1E10如果直接把這個(gè)結(jié)果拼進(jìn)短信、郵件、報(bào)表或者傳給前端展示會(huì)出現(xiàn)1E10這種讓人摸不著頭腦的顯示。正確做法是使用toPlainString()System.out.println(number.toPlainString()); // 輸出10000000000toPlainString()返回的永遠(yuǎn)是不帶科學(xué)計(jì)數(shù)法的普通十進(jìn)制字符串。凡是要把 BigDecimal 展示給用戶或?qū)懭霕I(yè)務(wù)單據(jù)我都統(tǒng)一走toPlainString()這是一個(gè)成本極低但收益很大的習(xí)慣。5.2 金額格式化與小數(shù)位對(duì)齊業(yè)務(wù)中最常見(jiàn)的格式化需求是保留兩位小數(shù)并加千分位。通常會(huì)配合DecimalFormat完成需要注意先設(shè)置好 BigDecimal 的精度再做格式化避免格式化階段引入意外BigDecimal amount new BigDecimal(12345678.9); DecimalFormat df new DecimalFormat(#,##0.00); System.out.println(df.format(amount)); // 輸出12,345,678.90有一個(gè)細(xì)節(jié)容易被忽視DecimalFormat默認(rèn)使用的舍入模式是HALF_EVEN不是HALF_UP。如果業(yè)務(wù)明確要求四舍五入必須手動(dòng)指定df.setRoundingMode(RoundingMode.HALF_UP);不設(shè)置的話2.5會(huì)被格式化成2而不是3這種問(wèn)題隱蔽性特別強(qiáng)通常要到最后的數(shù)據(jù)核對(duì)階段才會(huì)暴露。另外一個(gè)處理方式是從 BigDecimal 層面先完成精度控制再用字符串拼接這樣每一步都有明確邊界BigDecimal rounded amount.setScale(2, RoundingMode.HALF_UP); String result String.format(%,.2f, rounded);開發(fā)時(shí)也要避免讓 BigDecimal 繞道 double 去做格式化比如String.format(%.2f, bigDecimal.doubleValue())。這種寫法把 BigDecimal 轉(zhuǎn)換成了 double精度在轉(zhuǎn)換過(guò)程中就已經(jīng)丟失了格式化再好看都沒(méi)有意義。6. 項(xiàng)目實(shí)戰(zhàn)從工具類封裝到踩坑記錄6.1 我在項(xiàng)目中沉淀的 BigDecimal 工具類因?yàn)?BigDecimal 在代碼里出鏡率太高我習(xí)慣在項(xiàng)目里沉淀一個(gè)工具類把兜底邏輯集中管理。核心功能是安全轉(zhuǎn)換和空值兜底。下面是一個(gè)很實(shí)用的模板你直接可以拿去改改用public final class BigDecimalUtils { private BigDecimalUtils() { } public static BigDecimal toBigDecimal(Object value) { return toBigDecimal(value, BigDecimal.ZERO); } public static BigDecimal toBigDecimal(Object value, BigDecimal defaultValue) { if (value null) { return defaultValue; } if (value instanceof BigDecimal) { return (BigDecimal) value; } if (value instanceof Number) { return BigDecimal.valueOf(((Number) value).doubleValue()); } try { return new BigDecimal(value.toString().trim()); } catch (NumberFormatException e) { return defaultValue; } } public static BigDecimal toBigDecimalQuietly(String text, BigDecimal defaultValue) { if (text null || text.trim().isEmpty()) { return defaultValue; } try { return new BigDecimal(text.trim()); } catch (NumberFormatException e) { return defaultValue; } } public static boolean isPositive(BigDecimal value) { return value ! null value.signum() 0; } public static boolean isZero(BigDecimal value) { return value null || value.signum() 0; } public static BigDecimal scaleTo2(BigDecimal value) { return value.setScale(2, RoundingMode.HALF_UP); } public static BigDecimal divide(BigDecimal dividend, BigDecimal divisor) { return dividend.divide(divisor, 2, RoundingMode.HALF_UP); } }這個(gè)工具類里有兩個(gè)設(shè)計(jì)要點(diǎn)。第一toBigDecimal對(duì)參數(shù)是Number的情況使用BigDecimal.valueOf而不是直接強(qiáng)轉(zhuǎn)確保精度安全。第二所有轉(zhuǎn)換失敗的情況都返回默認(rèn)值避免調(diào)用方到處寫判空邏輯??罩羔樖?BigDecimal 使用中最常見(jiàn)的運(yùn)行時(shí)異常所以判空和兜底要前置到工具層。6.2 實(shí)際項(xiàng)目中踩過(guò)的三個(gè)典型坑我把自己在真實(shí)項(xiàng)目中遇到的三個(gè)問(wèn)題記錄在這里每一個(gè)都對(duì)應(yīng)一段痛苦的排查經(jīng)歷。第一個(gè)坑是數(shù)據(jù)庫(kù)字段類型與 BigDecimal 的映射問(wèn)題。MySQL 的decimal類型映射到 Java 的 BigDecimal 后scale 取決于表結(jié)構(gòu)的定義。如果數(shù)據(jù)庫(kù)字段是decimal(10, 2)查出來(lái)就是兩位小數(shù)如果字段是decimal(10, 4)查出來(lái)是四位小數(shù)。代碼里如果假設(shè)查出來(lái)的一定是兩位小數(shù)直接做展示會(huì)出現(xiàn)19.9000這種尾巴必須統(tǒng)一用工具類做setScale(2)。第二個(gè)坑是JSON 序列化與反序列化的精度丟失。早期項(xiàng)目用 Fastjson 或 Gson 時(shí)如果沒(méi)有注冊(cè)針對(duì) BigDecimal 的序列化器反序列化過(guò)程可能先把 JSON 中的數(shù)值轉(zhuǎn)成 double再轉(zhuǎn)成 BigDecimal精度直接受損。一個(gè)保底方案是JSON 里金額字段統(tǒng)一用字符串類型傳輸Java 側(cè)接收為 String 再做轉(zhuǎn)換徹底繞開中間態(tài)。如果用 Jackson也可以配置把 BigDecimal 序列化為字符串保證傳輸過(guò)程中不被 JS 精度問(wèn)題二次傷害。第三個(gè)坑是在循環(huán)里頻繁創(chuàng)建 BigDecimal 對(duì)象導(dǎo)致性能問(wèn)題。BigDecimal 是不可變對(duì)象每次運(yùn)算都會(huì)產(chǎn)生新對(duì)象在循環(huán)里如果每次創(chuàng)建新對(duì)象而不復(fù)用GC 壓力會(huì)明顯上升。比如在百萬(wàn)級(jí)數(shù)據(jù)量下計(jì)算平均值先把所有值累加為 BigDecimal再做一次除法而不是每筆數(shù)據(jù)都做一次除法。對(duì)于性能敏感的場(chǎng)景要評(píng)估是否可以用 long 以分為單位存儲(chǔ)金額讓計(jì)算發(fā)生在整數(shù)域里最后除以 100 轉(zhuǎn) BigDecimal犧牲一點(diǎn)代碼直觀性換取極高效率。6.3 不可變性的正確理解BigDecimal 是不可變對(duì)象這一點(diǎn)在 Java 里和 String 完全一致。任何add、subtract、multiply、divide、setScale操作都不會(huì)修改原對(duì)象而是返回一個(gè)新對(duì)象。理解不可變性對(duì)編寫正確代碼非常重要BigDecimal a new BigDecimal(10); BigDecimal b a.add(new BigDecimal(5)); System.out.println(a); // 10a 沒(méi)有變 System.out.println(b); // 15如果你需要的是修改后的結(jié)果必須接收返回值。這個(gè)特性也讓 BigDecimal 天然線程安全因?yàn)樗鼪](méi)有內(nèi)部狀態(tài)可以被修改。多個(gè)線程共享同一個(gè) BigDecimal 實(shí)例時(shí)不需要額外的同步處理。這在并發(fā)編程里是一個(gè)加分項(xiàng)面試時(shí)經(jīng)常被問(wèn)到BigDecimal 是線程安全的嗎答案就是它是不可變對(duì)象所以是線程安全的。7. 面試官視角的 BigDecimal 高頻問(wèn)題7.1 六個(gè)常考問(wèn)題及回答要點(diǎn)我參與過(guò)不少技術(shù)面試也問(wèn)過(guò)候選人 BigDecimal 相關(guān)的問(wèn)題。整理幾個(gè)高頻問(wèn)題列出我認(rèn)為合理的回答要點(diǎn)。問(wèn)題一為什么不能用 float 或 double 表示金額回答要點(diǎn)float/double 采用二進(jìn)制近似表示十進(jìn)制小數(shù)很多小數(shù)無(wú)法精確表達(dá)運(yùn)算會(huì)產(chǎn)生誤差在金額計(jì)算中可能導(dǎo)致對(duì)不上賬。BigDecimal 采用unscaledValue × 10^(-scale)的方式精確表示十進(jìn)制數(shù)值可以滿足精度要求。問(wèn)題二new BigDecimal(0.1)和new BigDecimal(0.1)有什么區(qū)別回答要點(diǎn)前者接收 double 的二進(jìn)制近似值并如實(shí)轉(zhuǎn)換結(jié)果是一長(zhǎng)串精度污染后的數(shù)值后者按字符串內(nèi)容精確解析得到0.1。線上代碼必須用字符串構(gòu)造或BigDecimal.valueOf。問(wèn)題三兩個(gè) BigDecimal 比較相等用什么回答要點(diǎn)compareTo忽略 scale比較的是數(shù)值本身equals會(huì)同時(shí)比較 scale。業(yè)務(wù)上判斷金額相等應(yīng)該使用compareTo 0或者用signum() 0判斷是否為零。問(wèn)題四BigDecimal 除法拋 ArithmeticException 是什么原因如何處理回答要點(diǎn)不指定舍入模式時(shí)如果除不盡結(jié)果是一個(gè)無(wú)限循環(huán)小數(shù)BigDecimal 無(wú)法確定保留位數(shù)就拋異常。處理方式是指定帶的scale和RoundingMode如divide(divisor, 2, RoundingMode.HALF_UP)。問(wèn)題五BigDecimal 是線程安全的嗎回答要點(diǎn)BigDecimal 是不可變對(duì)象所有運(yùn)算都會(huì)返回新對(duì)象不會(huì)修改內(nèi)部狀態(tài)所以在多線程環(huán)境下可以安全共享。這與 String 的設(shè)計(jì)類似。問(wèn)題六1.0和1.00用 equals 比較結(jié)果是什么為什么回答要點(diǎn)結(jié)果是 false。equals不僅比較數(shù)值還比較 scale。1.0的 scale 為 11.00的 scale 為 2內(nèi)部表示的 unscaledValue 不同。這是 BigDecimal 最容易踩坑的細(xì)節(jié)之一。7.2 回答面試題的小技巧面試中回答 BigDecimal 問(wèn)題時(shí)除了正確性展示你對(duì)原理的理解會(huì)加分。比如被問(wèn)到為什么 0.1 0.2 不等于 0.3比起直接說(shuō) float 有精度問(wèn)題更好的回答是先說(shuō)明二進(jìn)制表示十進(jìn)制小數(shù)的局限性再講 BigDecimal 的實(shí)現(xiàn)原理最后補(bǔ)一句實(shí)際開發(fā)中的最佳實(shí)踐字符串構(gòu)造、compareTo 比較、divide 帶舍入模式。這樣從原理到實(shí)踐都覆蓋了面試官能立刻感受到你的經(jīng)驗(yàn)深度。如果被問(wèn)到工具類設(shè)計(jì)這類開放性問(wèn)題可以把前面那個(gè)BigDecimalUtils的思路講出來(lái)判空兜底、統(tǒng)一精度、安全除法。這種問(wèn)題沒(méi)有標(biāo)準(zhǔn)答案考察的是編碼習(xí)慣和工程意識(shí)能說(shuō)出這幾個(gè)維度就說(shuō)明你在真實(shí)項(xiàng)目里是思考過(guò)的。寫在最后的心得BigDecimal 用起來(lái)不難但做到零坑需要把原理和邊界都摸透。我個(gè)人的習(xí)慣是金額字段一律 BigDecimal構(gòu)造優(yōu)先用字符串或valueOf比較統(tǒng)一用compareTo除法必須帶精度和舍入模式對(duì)外展示用toPlainString。這些習(xí)慣看起來(lái)瑣碎但就是它們攔住了一次又一次的線上事故。最后提醒一點(diǎn)不要在一個(gè)項(xiàng)目里混用 BigDecimal 的多種構(gòu)造方式。定好規(guī)范、寫在團(tuán)隊(duì)開發(fā)手冊(cè)里、讓每個(gè)新人都從規(guī)范開始寫比等到線上出了精度預(yù)警再回頭收拾要省太多力氣。Java 基礎(chǔ)的東西就是這樣——越基礎(chǔ)越重要越重要越值得提前研究透。