使用 double/long 和 BigDecimal 进行货币计算

发布于 2021-03-21 20:09:30 字数 5549 浏览 1701 评论 0

使用 long/double 进行货币操作

如何正确使用?

最简单的方式:使用本货币的最小单位,比如使用美分代替美元,分代替元,采用long数据类型进行存储。

缺点:对于需要进行乘法/除法操作的地方,需要采用浮点运算,这样计算的结果会受精度影响。

不要使用float数据类型进行任何货币计算,除非你确定这样做没问题。float精度太低,只有23bits。

double计算也不精确,即使是简单的加减运算:

System.out.println( "362.2 - 362.6 = " + ( 362.2 - 362.6 ) );   

输出结果为:362.2 - 362.6 = -0.4000000000000341

这意味着我们应该:

  1. 在最小的货币单位计算中,避免使用非整数的double值。
  2. 根据系统要求,使用Math.round/rint/ceil/floor对乘法/除法计算结果进行取整操作。

PS:只要能遵守上面的两条建议,还是能够使用long/double数据类型进行加减运算的。

什么情况下使用?

先看一个使用double和BigDecimal进行货币操作的测试用例,分别使用double和BigDecimal计算362.2¥的1.5%,循环100M次。

int res = 0;
final BigDecimal orig = new BigDecimal( "362.2" );
final BigDecimal mult = new BigDecimal( "0.015" ); //1.5%
for ( int i = 0; i < ITERS; ++i )
{
    final BigDecimal result = orig.multiply( mult, MathContext.DECIMAL64 );
    if ( result != null ) res++;
}

我们使用double和long不能完全模拟上面的计算。在下面的代码中,JIT会将常量Math.round( orig * mult )移出循环。

final double orig = 36220; //362.2 in cents
final double mult = 0.015; //1.5%
for ( int i = 0; i < ITERS; ++i )
{
    final long result = Math.round( orig * mult );
    if ( result != 543 ) res++;    //543.3 cents actually
}

所以,我们使用下面稍微不同的测试用例以提高可比性:

final double orig = 36220; //362.2 in cents
for ( long i = 0; i < ITERS; ++i )
{
    final long result = Math.round( orig * i );
    if ( result != 543 ) res++;    //compare with something
}

使用BigDecimal计算时花费4.899秒,使用double计算花费0.58秒。从测试结果可以看出,如果你的计算结果不超过52位(double精度),并且你坚持遵守上面的两条规则,那你就能使用long/double完成快速,精确的货币计算!

使用 BigDecimal 进行货币操作

如何正确使用?

对于 BigDecimals,如果需要定义取整模式和精度,可以使用MathContext类。该类中预定义了一些常量,比如MathContext.DECIMAL32/DECIMAL64/DECIMAL128,可用于模拟float/double/decimal_128算术运算,而不会出现任何rounding问题。MathContext.UNLIMITED是MathContext默认的值。

加减运算中,你可以不定义 MathContext,但乘除运算中最好定义 DECIMAL* 上下文中的一个。因为,乘除运算在计算结果为无限小数时需要定义精度,比如1除3。否则将会抛出ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result。

可以尝试运行代码:

final BigDecimal three = new BigDecimal( "3" );
try
{
    System.out.println( BigDecimal.ONE.divide( three ) );
}
catch ( ArithmeticException ex )
{
    System.out.println( "Got an exception while calculating 1/3 : " + ex.getMessage() );
}

BigDecimal 性能如何?

测试用例:计算 10M E*E+E 的和,其中 E=Math.E

BigDecimal res = BigDecimal.ZERO;
final BigDecimal a = new BigDecimal( Math.E, context );
final BigDecimal b = new BigDecimal( Math.E, context );
final BigDecimal c = new BigDecimal( Math.E, context );
for ( int i = 0; i < 10000000; ++i )
{
    final BigDecimal val = a.multiply( b, context ).add( c, context );
    res = res.add( val, context );
}

使用 double,没有设置 MathContext,设置不同的 MathContext 的测试结果:

类型耗时(秒)计算结果
double0.018Sec1.010733792587689E8
noMathContext4.1sec101073379.273896945320908905278183855697464192452494578591950602844407036684515333035960793495178222656250000000
MathContext.UNLIMITED3.9sec101073379.273896945320908905278183855697464192452494578591950602844407036684515333035960793495178222656250000000
MathContext.DECIMAL324.2sec100000000
MathContext.DECIMAL649.5sec101073379.2938854
MathContext.DECIMAL12813.9sec101073379.2738969453209089052948157

从测试结果中可以看出,使用BigDecimal进行运算开销很大,在可以避免的情况下需要尽量避免。比如,有个String型的数值,需要除以10的n次方(n为输入),采用对输入进行小数点移位会更快!对于double类型的数值,乘以或除以2的幂,一般情况都会得到正确的结果,因为浮点数值的指数部分表示2的幂。

数值转字符串

浮点数转字符串相当困难,比如双精度double转换,你需要知道浮点数的二进制表示形式(IEEE-754),具体实现可以参考JDK sun.misc.FloatingDecimal类。

1、Java6转换Double对象到String需要经过一系列调用:

  Double
  public String toString() {
      return String.valueOf(value);
  }

  String
  public static String valueOf(double d) {
      return Double.toString(d);
  }

  Double
  public static String toString(double d) {
      return new FloatingDecimal(d).toJavaFormatString();
  }

2、Java 7 中就非常直接:

jdk 7 Double
public String toString() {
  return toString(value);
}

3、BigDecimal 转换为 String

BigDecimal 有3个用于转换为String的方法:toString,toPlainString和toEngineeringString。toString会缓存toEngineeringString的结果,用于后续的调用(这样做可能是因为BigDecimal的值是不可变的)。下面测试了将Math.E转为字符串10M次耗时:

Double.toString(double)BigDecimal.toPlainStringBigDecimal.toEngineeringString
4.1 sec12.4 sec12.5 sec

最好不要将double转换为BigDecimal,因为double会先转为String,然后String转为BigDecimal。如果你的算法输入为String,那就直接将它转换为BigDecimal,这样你可以避免rounding错误!

总结

如果你想使用Java实现快速且正确的货币计算操作,你应该遵守以下规则:

  1. 将货币值以最小的货币单位(比如分)存放到long类型变量中。
  2. 如果以最小的货币单位计算时使用了double类型,要避免产生非整数的值,否则会产生精度问题。
  3. 使用long类型进行加减运算。
  4. 根据系统需求,使用Math.round/rint/ceil/floor对乘除结果进行取整。
  5. 计算要满足double精度(52位)。

为了避免产生无限小数时抛出ArithmeticException,在BigDecimal乘法和除法运算中需要使用MathContext。由于MathContext.UNLIMITED相当与没有使用上下文,所以不建议使用它。

可能的情况,不要将double转化为BigDecimal,而是将String转化为BigDecimal。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

JSmiles

生命进入颠沛而奔忙的本质状态,并将以不断告别和相遇的陈旧方式继续下去。

0 文章
0 评论
84960 人气
更多

推荐作者

沧笙踏歌

文章 0 评论 0

山田美奈子

文章 0 评论 0

佚名

文章 0 评论 0

岁月无声

文章 0 评论 0

暗藏城府

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文