42c5e060458a665ae386f6c078c723ba.png链接:https://juejin.im/post/5e86085ce51d4547153d0769

前言

通常商业计算涉及到小数的,我们都会使用 BigDecimal 来完成加减乘除运算。但是利用 BigDecimal 进行浮点数精确运算时,需要注意使用正确的方法。如果方法选择不当,依旧会发生错误。

发现问题

1public class Test { 2    public static void main(String[] args) { 3        BigDecimal multiply = BigDecimal.valueOf(9.9f).multiply(BigDecimal.valueOf(100f)); 4        System.out.println(multiply.toString()); 5 6        BigDecimal multiply2 = new BigDecimal(Float.toString(9.9f)).multiply(BigDecimal.valueOf(100f)); 7        System.out.println(multiply2.toString()); 8    } 9}1011//result12989.999961853027300013990.00

测试 BigDecimal 的两种构造方法,发现浮点运算的结果不同。利用 BigDecimal.valueOf 方法构造对象的方法,获得的浮点数发生了精度异常。利用 new BigDecimal(String val) 方法,运算正确。

分析原因

查看 BigDecimal.valueOf 源码,我们可以发现问题。BigDecimal.valueOf(9.9f) 实际调用的方法是 java.math.BigDecimal#valueOf(double)。如下图所示:

1    /** 2     * Translates a {@code double} into a {@code BigDecimal}, using 3     * the {@code double}'s canonical string representation provided 4     * by the {@link Double#toString(double)} method. 5     * 6     * 

Note: This is generally the preferred way to convert 7     * a {@code double} (or {@code float}) into a 8     * {@code BigDecimal}, as the value returned is equal to that 9     * resulting from constructing a {@code BigDecimal} from the10     * result of using {@link Double#toString(double)}.11     *12     * @param  val {@code double} to convert to a {@code BigDecimal}.13     * @return a {@code BigDecimal} whose value is equal to or approximately14     *         equal to the value of {@code val}.15     * @throws NumberFormatException if {@code val} is infinite or NaN.16     * @since  1.517     */

18    public static BigDecimal valueOf(double val) {19        // Reminder: a zero double returns '0.0', so we cannot fastpath20        // to use the constant ZERO.  This might be important enough to21        // justify a factory approach, a cache, or a few private22        // constants, later.23        return new BigDecimal(Double.toString(val));24    }

比我们想象的多了一步强制类型转换的动作。9.9f 会被强制转换成 double 类型。测试这个强制转换,我们可以发现问题,其实出在强制转换这里。强制转换结果如下图所示。

1public class Test { 2    public static void main(String[] args) { 3        double v = 9.9f; 4        System.out.println(v); 5        double v2 = Double.valueOf(9.9f); 6        System.out.println(v2); 7    } 8} 910//result119.899999618530273129.899999618530273

拓展

假如参数是 double 类型,我们使用 BigDecimal.valueOf(...) 可以吗?

1public class Test {2    public static void main(String[] args) {3        BigDecimal multiply3 = BigDecimal.valueOf(9.9d).multiply(BigDecimal.valueOf(100d));4        System.out.println(multiply3.toString());5    }6}78//result9990.00

看结果是没问题的。但是,注意 BigDecimal.valueOf(...) 的源码,其内部依然是把入参转换了 double 数据对应的字符串。假如直接使用 new BigDecimal(double val) 构造函数来进行运算,则会发现计算结果发生来精度异常。如下图所示。

1public class Test {2    public static void main(String[] args) {3        BigDecimal multiply4 = new BigDecimal(9.9d).multiply(BigDecimal.valueOf(100d));4        System.out.println(multiply4.toString());5    }6}78//result9990.00000000000003552713678800500929355621337890625000

可以看到 jdk 内部的注释,已经说明,new BigDecimal(double val) 方法得到的结果是不可预知的。推荐使用入参类型为 String 的构造函数来进行浮点数的精确计算。

1    /** 2     * Translates a {@code double} into a {@code BigDecimal} which 3     * is the exact decimal representation of the {@code double}'s 4     * binary floating-point value.  The scale of the returned 5     * {@code BigDecimal} is the smallest value such that 6     * (10scale × val) is an integer. 7     * 

8     * Notes: 9     * 

10     * 11     * The results of this constructor can be somewhat unpredictable.12     * One might assume that writing {@code new BigDecimal(0.1)} in13     * Java creates a {@code BigDecimal} which is exactly equal to14     * 0.1 (an unscaled value of 1, with a scale of 1), but it is15     * actually equal to16     * 0.1000000000000000055511151231257827021181583404541015625.17     * This is because 0.1 cannot be represented exactly as a18     * {@code double} (or, for that matter, as a binary fraction of19     * any finite length).  Thus, the value that is being passed20     * in to the constructor is not exactly equal to 0.1,21     * appearances notwithstanding.22     *23     * 24     * The {@code String} constructor, on the other hand, is25     * perfectly predictable: writing {@code new BigDecimal("0.1")}26     * creates a {@code BigDecimal} which is exactly equal to27     * 0.1, as one would expect.  Therefore, it is generally28     * recommended that the {@linkplain #BigDecimal(String)29     * String constructor} be used in preference to this one.30     *31     * 32     * When a {@code double} must be used as a source for a33     * {@code BigDecimal}, note that this constructor provides an34     * exact conversion; it does not give the same result as35     * converting the {@code double} to a {@code String} using the36     * {@link Double#toString(double)} method and then using the37     * {@link #BigDecimal(String)} constructor.  To get that result,38     * use the {@code static} {@link #valueOf(double)} method.39     * 40     *41     * @param val {@code double} value to be converted to42     *        {@code BigDecimal}.43     * @throws NumberFormatException if {@code val} is infinite or NaN.44     */45    public BigDecimal(double val) {46        this(val,MathContext.UNLIMITED);47    }

总结

在使用 BigDecimal 进行科学运算的时候,我们需要清楚,BigDecimal.valueOf(...) 和 new BigDecimal(...) 的使用效果不同。当 BigDecimal.valueOf(...) 的入参是 float 类型时,BigDecimal 会把入参强制转换成 double 类型。

使用 new BigDecimal(String val) 来获得浮点数对应的 BigDecimal 对象,进而完成浮点数精确运算。

文章推荐

这8种常见的SQL错误用法!80%的人还在用!如何画出优秀的架构图?懵逼!Spring Boot竟然这么耗我内存!900万加数据从17s优化到300ms!怎么搞的。这10个程序员必备升级网站,你知道几个?别再写烂sql了,送你4款工具有了这款界面调试工具我笑了,knife4j这35个Java代码优化细节,你用了吗?Undertow技术:为什么很多SpringBoot开发者放弃了Tomcat最近很多小伙伴在问技术群的问题,那就建起来吧,加我微baiseyumaoxx,技术的小伙伴可以拉大家进群。ef9cc014f66dd0d088a5623147075c88.png54fb8bcdb6fa98a34b3c5d1bca29b8aa.png你点的每个好看,我都认真当变成喜欢
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐