为什么不该用 Double 表示金额及解决方案
<blockquote><p>众所周知,double 和 float 这些浮点数其实是不精确的。</p>
</blockquote>
<p>比如 0.1 + 0.2 并不等于 0.3,而是等于 0.30000000000000004——这也一度成为程序员圈子里的经典梗。所以用浮点数表示金额这种需要精确计算的数值,是会出现精度丢失问题的。</p>
<pre><code class="language-java">double a = 0.1;
double b = 0.2;
System.out.println(a + b);// 输出: 0.30000000000000004
System.out.println(a + b == 0.3);// 输出: false
</code></pre>
<p>再看一个更实际的例子,假设你在做一个电商系统的金额计算:</p>
<pre><code class="language-java">double price = 2.0;
double discount = 0.9;
System.out.println(price * discount);// 输出: 1.7999999999999998
</code></pre>
<p>你看,原本应该是 1.8 的结果,却变成了 1.7999999999999998。如果这是真实的订单金额,那可就出大问题了</p>
<h4 id="为什么会精度丢失">为什么会精度丢失</h4>
<p>为什么会有这种精度丢失呢?因为计算机底层都是用二进制存储的,但并不是所有十进制数都能用二进制精确表示。各位有兴趣的话可以试着算一下 0.1 的二进制是多少,算出来可以在评论区分享一下。</p>
<p>算了一会你可能会发现:这怎么算不完?没错,出现了无限循环的情况——(0.1)₁₀ = (0.000110011001100...)₂ 像这种情况,计算机就没办法用二进制精确表示 0.1 了。</p>
<p>而 double 类型在 Java 中占 64 位,按照 IEEE 754 标准,其中 1 位是符号位,11 位是指数位,52 位是尾数位。当遇到无限循环的二进制小数时,只能截断保存,这就导致了精度丢失。</p>
<h2 id="bigdecimal">BigDecimal</h2>
<p>在 Java 中,无论是单精度还是双精度,表示的都是近似值。</p>
<p>为了表示精确的小数值,Java 提供了 <code>BigDecimal</code> 类型。<code>BigDecimal</code> 由两个部分组成:无标度值(unscaled value)和标度(scale)。无标度值是一个整数,表示实际的数值;标度也是一个整数,表示小数点后的位数。</p>
<p>举个例子,数字 123.45 在 BigDecimal 中:</p>
<ul>
<li>无标度值是 12345</li>
<li>标度是 2</li>
</ul>
<p>实际值就是:12345 × 10⁻² = 123.45</p>
<p>用 BigDecimal 来处理刚才的金额计算:</p>
<pre><code class="language-java">BigDecimal price = new BigDecimal("2.0");
BigDecimal discount = new BigDecimal("0.9");
BigDecimal result = price.multiply(discount);
System.out.println(result);// 输出: 1.80
</code></pre>
<p>这下结果就对了</p>
<h3 id="equals-的坑">equals 的坑</h3>
<p>在 BigDecimal 中不能用 equals 方法做等值比较,因为 equals 会同时比较无标度值和标度这两个内容。</p>
<pre><code class="language-java">BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.10");
System.out.println(a.equals(b));// 输出: false
</code></pre>
<p>我们都知道 0.1 和 0.10 在数值上是相等的,但 equals 的结果却是 false。这是因为:</p>
<ul>
<li>a 的无标度值是 1,标度是 1</li>
<li>b 的无标度值是 10,标度是 2</li>
</ul>
<p>虽然值相同,但它们的标度不同,所以 equals 返回 false。</p>
<h2 id="compareto">compareTo</h2>
<p>比较 BigDecimal 大小时应该使用 compareTo 方法,返回值为 1、-1、0,分别代表大于、小于、等于。</p>
<pre><code class="language-java">BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.10");
System.out.println(a.compareTo(b));// 输出: 0,表示相等
BigDecimal c = new BigDecimal("0.2");
System.out.println(a.compareTo(c));// 输出: -1,表示 a < c
System.out.println(c.compareTo(a));// 输出: 1,表示 c > a
</code></pre>
<h2 id="创建-bigdecimal-的正确姿势">创建 BigDecimal 的正确姿势</h2>
<p>创建 BigDecimal 时,建议使用 String 类型的构造方法,也就是 <code>new BigDecimal("0.1")</code> 这样。</p>
<pre><code class="language-java">BigDecimal right = new BigDecimal("0.1");
System.out.println(right);// 输出: 0.1
BigDecimal wrong = new BigDecimal(0.1);
System.out.println(wrong);// 输出: 0.1000000000000000055511151231257827021181583404541015625
</code></pre>
<p>如果你用了 <code>new BigDecimal(0.1)</code> 的方式,创建出来的值其实也不是 0.1,而是一个近似值。这是因为传入的 double 本身就已经是近似值了,BigDecimal 只是忠实地把这个近似值保存下来而已。</p>
<h5 id="还有一个更方便的方法">还有一个更方便的方法:</h5>
<pre><code class="language-java">BigDecimal bd = BigDecimal.valueOf(0.1);
System.out.println(bd);// 输出: 0.1
</code></pre>
<p><code>valueOf</code> 方法内部会先把 double 转成 String,再调用 String 构造方法,所以也是安全的。</p>
<h2 id="常用的-bigdecimal-运算">常用的 BigDecimal 运算</h2>
<pre><code class="language-java">BigDecimal a = new BigDecimal("10.5");
BigDecimal b = new BigDecimal("2.3");
// 加法
System.out.println(a.add(b));// 12.8
// 减法
System.out.println(a.subtract(b));// 8.2
// 乘法
System.out.println(a.multiply(b));// 24.15
// 除法(需要指定精度和舍入模式)
System.out.println(a.divide(b, 2, RoundingMode.HALF_UP));// 4.57
</code></pre>
<p>注意除法操作时,如果不指定精度,遇到除不尽的情况会抛出 <code>ArithmeticException</code>。所以建议都加上精度和舍入模式。</p>
<p>总之,涉及金额计算时,千万别图省事用 double,老老实实用 BigDecimal 才是王道。</p><br><br>
来源:https://www.cnblogs.com/longlonglong777/p/19133663
頁:
[1]