文档章节

Java BigDecimal详解

JackieYeah
 JackieYeah
发布于 2014/03/05 21:26
字数 1768
阅读 1813
收藏 48

一、引言

        借用《Effactive Java》这本书中的话,float和double类型的主要设计目标是为了科学计算和工程计算。他们执行二进制浮点运算,这是为了在广域数值范围上提供较为精确的快速近似计算而精心设计的。然而,它们没有提供完全精确的结果,所以不应该被用于要求精确结果的场合。但是,货币计算往往要求结果精确,这时候可以使用int、long或BigDecimal。本文主要讲述BigDecimal使用过程中的一些陷阱、建议和技巧。

二、不可变性

        BigDecimal是不可变类,每一个操作(加减乘除等)都会返回一个新的对象, 下面以加法操作为例

BigDecimal a =new BigDecimal("1.22");
System.out.println("construct with a String value: " + a);
BigDecimal b =new BigDecimal("2.22");
a.add(b);
System.out.println("a plus b is : " + a);

        我们很容易会认为会输出:

         construct with a String value: 1.22

        a plus b is :3.44

        但实际上a plus b is : 1.22

        下面我们就来分析一下加法操作的源码

public BigDecimal add(BigDecimal augend) {
    long xs =this.intCompact; //整型数字表示的BigDecimal,例a的intCompact值为122
    long ys = augend.intCompact;//同上
    BigInteger fst = (this.intCompact !=INFLATED) ?null :this.intVal;//初始化BigInteger的值,intVal为BigDecimal的一个BigInteger类型的属性
    BigInteger snd =(augend.intCompact !=INFLATED) ?null : augend.intVal;
    int rscale =this.scale;//小数位数

    long sdiff = (long)rscale - augend.scale;//小数位数之差
    if (sdiff != 0) {//取小数位数多的为结果的小数位数
        if (sdiff < 0) {
            int raise =checkScale(-sdiff);
            rscale =augend.scale;
            if (xs ==INFLATED ||(xs = longMultiplyPowerTen(xs,raise)) ==INFLATED)
                fst =bigMultiplyPowerTen(raise);
        }else {
            int raise =augend.checkScale(sdiff);
            if (ys ==INFLATED ||(ys =longMultiplyPowerTen(ys,raise)) ==INFLATED)
                snd = augend.bigMultiplyPowerTen(raise);
        }
    }
    if (xs !=INFLATED && ys !=INFLATED) {
        long sum = xs + ys;
        if ( (((sum ^ xs) &(sum ^ ys))) >= 0L)//判断有无溢出
            return BigDecimal.valueOf(sum,rscale);//返回使用BigDecimal的静态工厂方法得到的BigDecimal实例

    }

    if (fst ==null)
        fst =BigInteger.valueOf(xs);//BigInteger的静态工厂方法
    if (snd ==null)
        snd =BigInteger.valueOf(ys);
    BigInteger sum =fst.add(snd);
    return (fst.signum == snd.signum) ?new BigDecimal(sum,INFLATED, rscale, 0) :
new BigDecimal(sum,compactValFor(sum),rscale, 0);//返回通过其他构造方法得到的BigDecimal对象
}

       因为BigInteger与BigDecimal都是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象,所以a.add(b)虽然做了加法操作,但是a并没有保存加操作后的值,正确的用法应该是a=a.add(b); 减乘除操作也是一样的返回一个新的BigDecimal对象。     

三、构造函数和valueOf方法

        首先看如下一段代码: 

// use constructor BigDecimal(double)
BigDecimal aDouble =new BigDecimal(1.22);
System.out.println("construct with a double value: " + aDouble);

// use constructor BigDecimal(String)
BigDecimal aString = new BigDecimal("1.22");
System.out.println("construct with a String value: " + aString);

// use constructor BigDecimal.valueOf(double)
BigDecimal aValue = BigDecimal.valueOf(1.22);
System.out.println("use valueOf method: " + aValue);

        你认为输出结果会是什么呢?如果你认为第一个会输出1.22,那么恭喜你答错了,输出结果如下:

        construct with a double value: 1.2199999999999999733546474089962430298328399658203125

        construct with a String value: 1.22

        use valueOf method: 1.22

        为什么会这样呢?JavaDoc对于BigDecimal(double)有很详细的说明:

1、参数类型为double的构造方法的结果有一定的不可预知性。有人可能认为在Java中new BigDecimal(0.1)所创建的BigDecimal的值正好等于 0.1(非标度值 1,其标度为 1),但是它实际上等于0.1000000000000000055511151231257827021181583404541015625。这是因为0.1无法准确地表示为 double(或者说对于该情况,不能表示为任何有限长度的二进制小数)。这样,传入到构造方法的值不会正好等于 0.1(虽然表面上等于该值)。 
 2、另一方面,String 构造方法是完全可预知的:new BigDecimal("0.1") 将创建一个 BigDecimal,它的值正好等于期望的0.1。因此,比较而言,通常建议优先使用String构造方法。 
 3、当 double 必须用作BigDecimal的来源时,请注意,此构造方法提供了一个精确转换;它不提供与以下操作相同的结果:先使用Double.toString(double)方法将double转换为String,然后使用BigDecimal(String)构造方法。要获取该结果,使用static valueOf(double)方法。
          BigDecimal.valueOf(double) 使用由 Double.toString(double)方法提供的 double的标准化字符串表示形式( canonical string representation) double 转换成 BigDecimal 。这也是比较推荐的一种方式。
         BigDecimal.valueOf(double)还有一个重载的方法 BigDecimal.valueOf(long),对于某些常用值(0到10) BigDecimal在内部做了缓存, 如果传递的参数值范围为[0, 10], 这个方法直接返回缓存中相应的BigDecimal对象。
        Java源码如下:

/**
     * Translates a {@code long} value into a {@code BigDecimal}
     * with a scale of zero. This {@literal "static factory method"}
     * is provided in preference to a ({@code long}) constructor
     * because it allows for reuse of frequently used
     * {@code BigDecimal} values.
     *
     * @param val value of the {@code BigDecimal}.
     * @return a {@code BigDecimal} whose value is {@code val}.
     */
        public static BigDecimal valueOf(long val) {
        if (val >= 0 && val < zeroThroughTen.length)
            return zeroThroughTen[(int)val];
        else if (val != INFLATED)
            return new BigDecimal(null, val, 0, 0);
        return new BigDecimal(INFLATED_BIGINT, val, 0, 0);
    }

    // Cache of common small BigDecimal values.
    private static final BigDecimal zeroThroughTen[] = {
        new BigDecimal(BigInteger.ZERO, 0, 0, 1),
        new BigDecimal(BigInteger.ONE, 1, 0, 1),
        new BigDecimal(BigInteger.valueOf(2), 2, 0, 1),
        new BigDecimal(BigInteger.valueOf(3), 3, 0, 1),
        new BigDecimal(BigInteger.valueOf(4), 4, 0, 1),
        new BigDecimal(BigInteger.valueOf(5), 5, 0, 1),
        new BigDecimal(BigInteger.valueOf(6), 6, 0, 1),
        new BigDecimal(BigInteger.valueOf(7), 7, 0, 1),
        new BigDecimal(BigInteger.valueOf(8), 8, 0, 1),
        new BigDecimal(BigInteger.valueOf(9), 9, 0, 1),
        new BigDecimal(BigInteger.TEN, 10, 0, 2),
    };

    附上相应的测试代码:

BigDecimal a1 = BigDecimal.valueOf(10);
BigDecimal a2 = BigDecimal.valueOf(10);
System.out.println(a1 == a2); // true

BigDecimal a3 = BigDecimal.valueOf(11);
BigDecimal a4 = BigDecimal.valueOf(11);
System.out.println(a3 == a4); // false

四、equals方法

       BigDecimal.equals方法是有问题的.仅当你确定比较的值有着相同的标度时才可使用. 因此,当你校验相等性时注意 - BigDecimal有一个标度,用于相等性比较. 而compareTo方法则会忽略这个标度(scale).

       BigDecimal的equals方法源码如下:

@Override
    public boolean equals(Object x) {
        // 必须是BigDecimal实例
        if (!(x instanceof BigDecimal))
            return false;
        BigDecimal xDec = (BigDecimal) x;
        if (x == this)
            return true;
        // 标度必须相同
        if (scale != xDec.scale)
            return false;
        long s = this.intCompact;
        long xs = xDec.intCompact;
        if (s != INFLATED) {
            if (xs == INFLATED)
                xs = compactValFor(xDec.intVal);
            return xs == s;
        } else if (xs != INFLATED)
            return xs == compactValFor(this.intVal);

        return this.inflated().equals(xDec.inflated());
    }

        参见以下测试代码:

// 打印false
System.out.println(new BigDecimal("0.0").equals(new BigDecimal("0.00")));

// 打印false 
System.out.println(new BigDecimal("0.0").hashCode() == (new BigDecimal("0.00")).hashCode());

// 打印0 
System.out.println(new BigDecimal("0.0").compareTo(new BigDecimal("0.00")));

五、对除法使用标度

      BigDecimal对象的精度没有限制。如果结果不能终止,divide方法将会抛出ArithmeticException, 如1 / 3 = 0.33333...。所以强烈推荐使用重载方法divide(BigDecimal d, int scale, int roundMode)指定标度和舍入模式来避免以上异常。

      参见以下测试代码:

//java.lang.ArithmeticException: Non-terminating decimal expansion;
//no exact representable decimal result.
try {
    BigDecimal.valueOf(1).divide(BigDecimal.valueOf(3));
} catch (ArithmeticException ex) {
    System.out.println(ex.getMessage());
}
// always use a scale and the rounding mode of your choice
// 0.33
System.out.println(BigDecimal.valueOf(1).divide(BigDecimal.valueOf(3), 2, BigDecimal.ROUND_HALF_UP));

六、总结

        (1)商业计算使用BigDecimal。

        (2)使用参数类型为String的构造函数,将double转换成BigDecimal时用BigDecimal.valueOf(double),做除法运算时使用重载的方法divide(BigDecimal d, int scale, int roundMode)

        (3)BigDecimal是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象,所以在做加减乘除运算时千万要保存操作后的值。

        (4)尽量使用compareTo方法比较两个BigDecimal对象的大小。

七、参考资料

         《Effective Java》

         http://www.stichlberger.com/software/java-bigdecimal-gotchas/

         http://stackoverflow.com/questions/7186204/bigdecimal-to-use-new-or-valueof

         http://www.javaworld.com/article/2073176/caution--double-to-bigdecimal-in-java.html

© 著作权归作者所有

JackieYeah
粉丝 45
博文 70
码字总数 90004
作品 0
武汉
程序员
私信 提问
加载中

评论(7)

boopo
boopo
嗯嗯
JackieYeah
JackieYeah 博主

引用来自“andying”的评论

每天進步一點點.

共同学习,共同进步,13
andying
andying
每天進步一點點.
JackieYeah
JackieYeah 博主

引用来自“临渊_ARQ”的评论

学习了!感谢楼主分享!

共同学习13
临渊_ARQ
学习了!感谢楼主分享!
JackieYeah
JackieYeah 博主

引用来自“潮汐猎人”的评论

79

感谢支持13
重度恐高症
重度恐高症
79
Java Double相加出现的怪事

问题的提出: 编译运行下面这个程序会看到什么 [java] view plaincopy public class test { public static void main(String args[]) { System.out.println(0.05 + 0.01); System.out.print......

onedotdot
2017/11/05
32
0
Hibernate QBC语言

节 8.01 基本查询 以下是HQL/QBC/Native SQL三种查询策略 HQL策略: session.createQuery("FROM Category c where c.name like 'Laptop%'"); QBC策略: session.createCriteria(Category.cl......

ddtt
2011/12/22
231
0
JDK 6u25 发布

该版本继续提升性能和稳定性,提升了 BigDecimal 处理性能,更新 Olson 时区数据到 2011b 版本,包含 HotSpot VM 版本20,修复了很多小bug。 Java Development Kit (JDK) 是Sun公司(已被Ora...

红薯
2011/04/24
4.3K
10
你所不知道的 BigDecimal

本文首发于个人微信公众号《andyqian》, 期待你的关注! 前言 在Java中,我们通常使用 BigDecimal 类型来表示金额,特别是在金融,财务系统中,使用的特别多。例如:转账金额,手续费等等。今...

andyqian
07/18
43
0
180706-BigDecimal除法的精度问题

BigDecimal除法的精度问题 在使用BigDecimal的除法时,遇到一个鬼畜的问题,本以为的精度计算,结果使用返回0,当然最终发现还是自己的使用姿势不对导致的,因此记录一下,避免后面重蹈覆辙 ...

小灰灰Blog
2018/07/06
120
0

没有更多内容

加载失败,请刷新页面

加载更多

OpenStack 简介和几种安装方式总结

OpenStack :是一个由NASA和Rackspace合作研发并发起的,以Apache许可证授权的自由软件和开放源代码项目。项目目标是提供实施简单、可大规模扩展、丰富、标准统一的云计算管理平台。OpenSta...

小海bug
昨天
5
0
DDD(五)

1、引言 之前学习了解了DDD中实体这一概念,那么接下来需要了解的就是值对象、唯一标识。值对象,值就是数字1、2、3,字符串“1”,“2”,“3”,值时对象的特征,对象是一个事物的具体描述...

MrYuZixian
昨天
6
0
数据库中间件MyCat

什么是MyCat? 查看官网的介绍是这样说的 一个彻底开源的,面向企业应用开发的大数据库集群 支持事务、ACID、可以替代MySQL的加强版数据库 一个可以视为MySQL集群的企业级数据库,用来替代昂贵...

沉浮_
昨天
6
0
解决Mac下VSCode打开zsh乱码

1.乱码问题 iTerm2终端使用Zsh,并且配置Zsh主题,该主题主题需要安装字体来支持箭头效果,在iTerm2中设置这个字体,但是VSCode里这个箭头还是显示乱码。 iTerm2展示如下: VSCode展示如下: 2...

HelloDeveloper
昨天
7
0
常用物流快递单号查询接口种类及对接方法

目前快递查询接口有两种方式可以对接,一是和顺丰、圆通、中通、天天、韵达、德邦这些快递公司一一对接接口,二是和快递鸟这样第三方集成接口一次性对接多家常用快递。第一种耗费时间长,但是...

程序的小猿
昨天
10
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部