Java-BigDecimal学习

1. BigDecimal简介

  Java中,如果涉及到大的整数或者大的小数的计算的时候,比如long整数与long整数的相乘,金额的精确计算等操作,会用到BigIntegerBigDecimal这两种类型。其中BigInteger是针对任意整数的运算,而BigDecimal则是针对任意浮点数的运算。本篇文章我们来简单学习下它们的用法。

2. BigDecimal用法
2.1 BigDecimal继承结构及构造方法
public class BigDecimal extends Number implements Comparable<BigDecimal> {

可以看到,BigDecimal继承了Number类,并且实现了Comparable接口,说明该对象是支持排序的。至于Number类,我们来简单看下:

public abstract class Number implements java.io.Serializable {
  
    public abstract int intValue();
    public abstract long longValue();
    public abstract float floatValue();
    public abstract double doubleValue();

    public byte byteValue() {
        return (byte)intValue();
    }

    public short shortValue() {
        return (short)intValue();
    }
}

根据Api文档上介绍,该类作为数值类型的类的基类,主要是提供了将包装类拆箱成基本类型的方法,其中数值相关的类基本都实现了该抽象类,比如DoubleInteger等类。

再来看下BigDecimal的构造方法:

public BigDecimal(String val)
public BigDecimal(char[] in)
public BigDecimal(double val)
public BigDecimal(BigInteger val)
public BigDecimal(int val) 
public BigDecimal(long val)

  由于BigDecimal的构造方法特别多,这里就不一一介绍了,这里只说一点,就是不建议使用参数是double类型的构造方法了,因为double本身就不是精确的,转换成BigDecimal之后同样也不是精确的。如果非要使用double类型的话,建议先通过Double.toString(double)转成String或者通过BigDecimal的静态方法valueof来进行操作。

2.2 BigDecimal变量

这里列举下主要用到的变量:

// 维护了一个BigInteger变量,当有效位数超过18位的时候会有值
private final BigInteger intVal;
// 小数位数
private final int scale;
// 精度,该数字的长度
private transient int precision;
// 数值0
public static final BigDecimal ZERO = zeroThroughTen[0];
// 数值1
public static final BigDecimal ONE = zeroThroughTen[1];
// 数值10
public static final BigDecimal TEN = zeroThroughTen[10];

这里主要说两个变量,scale表示小数点右边的位数,而precision表示精度,也就是有效数字的长度,这两个数值和符号位都没关系。比如1.234,该数的scale是3,而precision是4。

2.3 Rounding mode 舍入规则或者舍入模式

对于小数,我们在进行操作的时候一般都会涉及到舍入规则,比如我们所了解的四舍五入法,进一法等。在BigDecimal中定义了8中舍入规则,我们来简单看下:

public final static int ROUND_UP =           0;

public final static int ROUND_DOWN =         1;

public final static int ROUND_CEILING =      2;

public final static int ROUND_FLOOR =        3;

public final static int ROUND_HALF_UP =      4;

public final static int ROUND_HALF_DOWN =    5;

public final static int ROUND_HALF_EVEN =    6;

public final static int ROUND_UNNECESSARY =  7;

关于舍入模式,直接看下RoundingMode的文档所介绍的:

Input Number UP DOWN CEILING FLOOR HALF_UP HALF_DOWN HALF_EVEN UNNECESSARY
5.5 6 5 6 5 6 5 6 throw ArithmeticException
2.5 3 2 3 2 3 2 2 throw ArithmeticException
1.6 2 1 2 1 2 2 2 throw ArithmeticException
1.1 2 1 2 1 1 1 1 throw ArithmeticException
1.0 1 1 1 1 1 1 1 1
-1.0 -1 -1 -1 -1 -1 -1 -1 -1
-1.1 -2 -1 -1 -2 -1 -1 -1 throw ArithmeticException
-1.6 -2 -1 -1 -2 -2 -2 -2 throw ArithmeticException
-2.5 -3 -2 -2 -3 -3 -2 -2 throw ArithmeticException
-5.5 -6 -5 -5 -6 -6 -5 -6 throw ArithmeticException

这个表格基本上描述了这几种舍入模式的区别,我们再来简单说下这几种模式,不过先说一个小问题,先猜测下如下代码的输出:

BigDecimal bd = new BigDecimal("6.45007");
bd = bd.setScale(3, BigDecimal.ROUND_UP);
System.out.println(bd);

没错,你猜的很正确,就是打印:6.451,至于原因么,我猜测应该是在ROUND_UP的文档上:

Rounding mode to round away from zero. Always increments the digit prior to a nonzero discarded fraction. Note that this rounding mode never decreases the magnitude of the calculated value.

也就是说四舍五入的时候,总是以最后一个非0的数来计算舍入。由于我英文不咋地,这里就不翻译了,来简单了解下这几项吧:

  1. ROUND_UP,向远离零的方向舍入。非标准的四舍五入,这个和ROUND_HALF_UP有点像,不过对负数的处理是不同的;
  2. ROUND_DOWN,向接近零的方向舍入。也就是将指定位数之后的全部舍掉,相当于截取操作;
  3. ROUND_CEILING, 向正无穷大的方向舍入。其实也就是进一法,对于指定位数之后的非0的全部加1;或者另一种说法,如果 BigDecimal 为正,则舍入行为与ROUND_UP 相同,如果为负,则舍入行为与 ROUND_DOWN 相同;
  4. ROUND_FLOOR,如果 BigDecimal 为正,则舍入行为与 ROUND_DOWN 相同,如果为负,则舍入行为与 ROUND_UP 相同;
  5. ROUND_HALF_UP,标准的四舍五入,官方文档还特地说明了下:Note that this is the rounding mode that most of us were taught in grade school.(注意一下,这是我们大多数人在小学时候所学的舍入模式)
  6. ROUND_UNNECESSARY,该模式表示该数值是精确的,如果某一个数值指定了该模式,并且不是精确的,则会抛出异常;
  1. ROUND_HALF_DOWN,其实这种有时候也被称为五舍六入,也就是大于5的时候舍入,小于等于5的时候舍弃。比如1.64250,保留3位小数的话,该舍入模式的值就是1.642,因为0.50 = 0.5;而如果1.64251,因为0.51 > 0.5,那么就是舍入,就是1.643;该模式和ROUND_HALF_UP模式不同的地方就是对5的操作。
  1. ROUND_HALF_EVEN,这个有点意思,该模式被称为银行家舍入(至于为什么叫银行家舍入,原因好像是美国的一个银行家提出的该算法),四舍六入,如果是5的话,要看5后面一位是否是0,如果不是0,舍入的时候进1;如果是0,然后看5之前的数是奇书还是偶数,奇书同样是进一,偶数的话就舍去。简单的说,就是:四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一。如要了解更多,可以使用Google百度一下:银行家舍入。

看个简单例子:

public static void main(String[] args) {
    // 使用 1.64250  1.64251   1.64350  1.64351进行测试
    BigDecimal bd1 = new BigDecimal("1.64250");
    BigDecimal bd2 = new BigDecimal("1.64251");
    BigDecimal bd3 = new BigDecimal("1.64350");
    BigDecimal bd4 = new BigDecimal("1.64351");
    // output:  1.642
    System.out.println(bd1.setScale(3, RoundingMode.HALF_EVEN));
    // output:  1.643
    System.out.println(bd2.setScale(3, RoundingMode.HALF_EVEN));
    // output:  1.644
    System.out.println(bd3.setScale(3, RoundingMode.HALF_EVEN));
    // output:  1.644
    System.out.println(bd4.setScale(3, RoundingMode.HALF_EVEN));
}
2.4 一些常用的方法

BigDecimal提供了一些常用的方法,我们来了解下:

  1. valueof方法,将对应类型转为BigDecimal,前面也说过,如果需要将double类型转为BigDecimal的话,可以使用该方法,而不用构造方法,不过该方法内部还是通过将字符串作为构造方法的参数的形式来实现的。
public static BigDecimal valueOf(double val) {
    // Reminder: a zero double returns '0.0', so we cannot fastpath
    // to use the constant ZERO.  This might be important enough to
    // justify a factory approach, a cache, or a few private
    // constants, later.
    return new BigDecimal(Double.toString(val));
}
  1. add方法,表示两个数值类型的加法运算操作;
  2. subtract方法,表示两个数值类型的减法运算操作;
  3. multiply方法,表示两个数值类型的乘法操作;
  4. divide方法,表示两个数值类型的除法操作;
  1. divideToIntegralValue方法,返回两个数值进行除法操作之后,也就是商的整数部分,而该返回值BigDecimal的scale,也就是小数位数 = (this.scale() - divisor.scale()),也就是除数的位数 - 被除数的位数,比如 12.345 除于 3.00,结果是4.0;
  2. remainder方法,计算两个数值的余数,该值有可能是负值,比如12.345 与3.00的余数,结果是0.345,计算方式是通过this.subtract(this.divideToIntegralValue(divisor).multiply(divisor))得到的;
  3. divideAndRemainder方法,返回一个数组,包含了两个数值除法之后得到的商的整数和余数。注意如果同时需要整数商和余数,这个方法比单独使用divideToIntegralValue和余数方法要快,因为除法只需要执行一次。
  4. pow(n)方法,精确的计算幂的操作,也就是thisn,参数n的范围是0到999999999,如计算3.00的3次方,结果是27.000000;
  5. abs方法,计算绝对值操作;negate方法,取反操作;max方法,获取最大值;min方法,获取最小值;
  1. longValueExact方法,除了常规的longValue,intValue之外,该类还提供了一种精确转换的方法,该方法表示如果要转换的BigDecimal有一个非零小数部分或者长度超出了long类型的范围,就会抛出一个异常,而不会像longValue方法那样进行舍弃。
  2. scale方法,获取小数位数;precision,获取精度;
  3. unscaledValue方法,获取小数值,比如对0.1234调用该方法,获取到的结果是1234,计算方式:this * 10this.scale()
  4. setScale方法,设置小数位数,注意下就是由于BigDecimal的不可变性,该方法和常规setX设置字段有些不同,会返回一个新的对象;
  5. signum方法,返回该数值的正负号,正数,0,负数分别对应返回结果:1,0,-1;
  6. movePointLeft,小数点左移,movePointRight,小数点右移;
  7. scaleByPowerOfTen,返回对象与10的幂的乘积,计算方式:this * 10n
  8. stripTrailingZeros方法,去除末尾多余的0,用科学记数法表示;
  9. compareTo方法,用于比较大小,同样返回1,0,-1;
  1. toStringtoPlainStringtoEngineeringString方法,这三个方法都是用于BigDecimal的字符串表示,不同的是toString有可能会使用科学记数法,toPlainString只展示数值,不使用科学记数法,toEngineeringString工程计数法,与科学技术法类似,但要求10的幂必须是3的倍数;
toPlainString toString toEngineeringString
1000 1 * 103 1 * 103
10000 1 * 104 10 * 103
100000 1 * 105 100 * 103
1000000 1 * 106 1 * 106

测试代码:

public static void main(String[] args) {
    BigDecimal bigDecimal = new BigDecimal("1000000");
    System.out.println(bigDecimal.stripTrailingZeros().toPlainString());
    System.out.println(bigDecimal.stripTrailingZeros().toString());
    System.out.println(bigDecimal.stripTrailingZeros().toEngineeringString());
}
  1. toBigInteger方法, 转成BigInteger对象;
  2. ulp方法,返回BigDecimal最后一位单位的大小,整数和0的话都是1,而小数的话,则取决于对应的位数,比如43.3,返回0.1,43.33返回0.01;

另外两个方法,plus和round,不太清楚具体含义。

2.5 注意事项

  由于BigDecimal和String一样具有不可变性,所以我们在进行操作之后都会得到一个新的对象,所以要记得保存下:

BigDecimal b = new BigDecimal("3.012");
// wrong
b.negate();
// right 
b = b.negate();
System.out.println(b);
3. RoundingMode枚举类

关于该枚举类的用法,先看一段官方文档:

This enum is intended to replace the integer-based enumeration of rounding mode constants in BigDecimal (BigDecimal.ROUND_UP, BigDecimal.ROUND_DOWN, etc. ).

很明显,该枚举类是为了取代BigDecimal中所定义的静态常量,所以说,以后我们如果有需要的话,尽量使用RoundingMode枚举类。由于该枚举中的类型和BigDecimal中的类型是一一对应的,这里就不多说了。

4. MathContext类

  如果我们使用BigDecimal的时候,需要同时指定精度和舍入模式的话,可以使用MathContext类。该对象是对BigDecimal中精度和舍入模式的一个封装,也就是可以指定有效位数和具体的舍入模式,使用起来比较简单:

public static void main(String[] args) {
    // output: 12.35
    System.out.println(new BigDecimal("12.345", new MathContext(4, RoundingMode.HALF_UP)));
}
5. BigInteger类

BigInteger类是对大整数进行操作的类,这里面有一些方法和BigDecimal类的一些方法是很像的,这里我们只简单介绍下BigInteger独有的方法。

1.gcd 求两个数的最大公约数;

  1. mod 取模运算,结果不会为负数 this mod m;
  2. modPow 取模运算,结果可以是负数 thisexponent mod m
  3. modInverse 计算 (this-1 mod m)
  4. isProbablePrime 判断是否是质数
  5. bitCount 返回此 该BigInteger 的二进制补码表示形式中与符号位不同的位的数量,比如8,二进制1000,则该方法返回1;再比如-8,二进制 1 1000,第一个是符号位,计算补码1 1000,则该方法返回 3;
  6. bitLength 返回此 BigInteger 的最小的二进制补码表示形式的位数,不包括 符号位
  7. flipBit 返回按指定位进行反转之后的值,计算方式:this ^ (1<<n)
    clearBit 返回清除指定位之后的值,计算方式:this & ~(1<<n)
    setBit 返回设置指定位之后的值,计算方式:this | (1<<n)
    testBit 当且仅当指定的位被设置之后,返回true,计算方式:(this & (1<<n)) != 0
  8. shiftLeftshiftRight 左移和右移操作;
  9. and,执行 this & val;
    or 执行 this | val;
    xor 执行 this ^ val;
    not 执行 ~this;
    andNot 执行 this & ~val;
  10. nextProbablePrime 大于该值的下一个可能是质数的整数;

参考:JDK 8.0 官方Api

推荐阅读更多精彩内容