BigDecimal

BigDecimal

在商业计算中,一般都会用到 Java 中的 BigDecimal 或者 BigInteger。

这篇文章就来记录 BigDecimal 的一些用法知识。


Java 基本数据类型

在JAVA中一共有八种基本数据类型,他们分别是 :
byte、short、int、long、float、double、char、boolean


整数型:

在计算机中,所有的文件都是以二进制的形式存储的,所有规定,一个字节(byte)占8位(bit)二进制位数。在表示整数型时,最高位符号位表示数字的正负,剩余的位数表示一个数值,它们合起来表示一个整数。

  • byte :占1个字节,用8位二进制表示,所以其取值范围 -128~127(-2的7次方到2的7次方-1)
  • short:占2个字节,用16位二进制表示,所以其取值范围-32768~32767(-2的15次方到2的15次方-1)
  • int:占4个字节,用32位二进制表示,其取值范围-2147483648~2147483647(-2的31次方到2的31次方-1)
  • long:占8个字节,用64位二进制表示,其取值范围-9223372036854774808~9223372036854774807(-2的63次方到2的63次方-1)


浮点型:

浮点型的存储方式与整数型不同,其采用科学计数方式表示,最高位依然是表示正负的符号位,然后中间多了表示指数的指数位,其余的是尾数位。浮点型的范围主要取决于指数位,精度取决于尾数位。java 中默认的浮点型是 double。

  • float:占4个字节,用32位二进制表示,第1位符号位表示正负,第2~9位这八位表示指数位,后面23位都表示尾数。float的指数部分有8bit(2^8),对应的指数范围-128~128,其取值范围 -2^128到2^128 ,用科学计数表示3.402823e+38 ~ 1.401298e-45(e+38表示是乘以10的38次方,同样,e-45表示乘以10的负45次方),float的尾数位是23bit,对应 2^23=8388608,最多7位有效数字,保证有6位有效数字。
  • double:占8个字节,用64位二进制表示,1位符号位,然后中间11位表示指数位,52位尾数。double的指数部分有11bit(2^11),对应的指数范围-1024~1024,其取值范围 -2^1024到2^1024 ,用科学计数表示1.797693e+308~ 4.9000000e-324,double的尾数位是52bit,对应2^52 = 4503599627370496,最多16位有效数字,保证15位有效。


char 型

文本型用于存放字符的数据类型,占用2个字节,采用unicode编码,不过8位的ASCII码包含在Unicode中,是从0~127兼容的。

  • char:占2个字节,用16位表示


布尔型:

布尔数据类型只有两个可能的值:true和false

  • boolean:布尔数据类型没有给出具体的占用字节数,因为对虚拟机来说根本就不存在 boolean 这个类型,boolean类型在编译后会使用其他数据类型来表示。官方文档并没有给出精确的定义,《Java虚拟机规范》给出了4个字节,和boolean数组1个字节的定义,具体还要看虚拟机实现是否按照规范来,所以1个字节、4个字节都是有可能的。


当然,这篇文章并不是讲上面的基本数据类型的。


BigDecimal 介绍

在实际开发中,java 的浮点型只能用来进行科学计算或工程计算,在大多数的商业计算中,一般采用 java.math.BigDecimal 类来进行精确计算。商业计算中,要尽可能保证运算的准确性,所以在计算时要尽可能多的保证有效位数,减小精度损失,而 double 最多只能保证16位有效数字,因此,Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算。


BigDecimal 是不可变的、任意精度的有符号十进制数,由任意精度的整数非标度值 和32 位的整数标度 (scale) 组成。如果为零或正数,则标度是小数点后的位数。如果为负数,则将该数的非标度值乘以 10 的负scale 次幂。


基本用法

在使用BigDecimal类来进行计算的时候,主要分为以下步骤:

  1. 用float或者double变量构建BigDecimal对象。
  2. 通过调用BigDecimal的加,减,乘,除等相应的方法进行算术运算。
  3. 把BigDecimal对象转换成float,double,int等类型。


BigDecimal类是对象,不能使用传统的+、-、*、/等算术运算符直接对其进行数学运算,而必须调用其对应的方法.方法的参数也必须是BigDecimal类型的对象。

首先,使用BigDecimal的构造方法或者静态方法的valueOf()方法把基本类型的变量构建成BigDecimal对象

1
2
3
4
5
6
BigDecimal BigDecimal(double d); //不允许使用,直接使用 double 得到值不精确。
BigDecimal BigDecimal(String s); //常用,推荐使用
static BigDecimal valueOf(double d); //常用,推荐使用
//示例
BigDecimal b1 = new BigDecimal(String.valueOf("0.48"));
BigDecimal b2 = BigDecimal.valueOf(0.48);

然后,对数据进行加减乘除,BigDecimal都是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象。

1
2
3
4
5
6
public BigDecimal add(BigDecimal value); //加法
public BigDecimal subtract(BigDecimal value); //减法
public BigDecimal multiply(BigDecimal value); //乘法
public BigDecimal divide(BigDecimal value); //除法
public BigDecimal remainder(BigDecimal divisor); //求余数
public BigDecimal negate(); //求相反数

注意,在使用除法时,一般需要指定精度以及舍入模式,如果不指定,一旦出现无限小数就会直接抛算术异常。

最后,将结果转换为float,double,int等类型

1
2
3
public int intValue() //转int
public float floatValue() //转float
public double doubleValue() //转double


8种舍入模式

在运算或者转换的过程中,可以设定精度,如果不指定舍入模式,则默认ROUND_UNNECESSARY,但是这种模式断言请求的操作具有精确的结果,因此不需要设定精度,所以在这个模式下去设定精度,一旦出现精度损失,必然会抛出异常,真是坑~~

1
2
3
BigDecimal decimal = new BigDecimal("3.1");
BigDecimal scale1 = decimal.setScale(1); //设置保留一位小数,正常运行,显示 3.1
BigDecimal scale2 = decimal.setScale(0);//设置不保留小数,造成精度损失,会抛出异常

所以,设定精度时,最好指定舍入模式。


由于BigDecimal 不可变,所有的变动都会返回新的BigDecimal对象,所以需要保存新对象来使用。

1
2
3
4
BigDecimal bigDecimal = new BigDecimal("3.1415");
//保留小数点后3位小数,采用四舍五入
BigDecimal scale = bigDecimal.setScale(3, RoundingMode.HALF_UP);
System.out.println(scale.toString()); //打印3.142


舍入模式一共有8种,都在 BigDecimal 设定了,并且通过 RoundingMode 这个枚举类进行了包装。

模式 描述
ROUND_UP 舍入远离零的舍入模式。在丢弃非零部分之前始终增加数字(始终对非零舍弃部分前面的数字加1)。注意,此舍入模式始终不会减少计算值的大小。
ROUND_DOWN 接近零的舍入模式。在丢弃某部分之前始终不增加数字(从不对舍弃部分前面的数字加1,即截短)。注意,此舍入模式始终不会增加计算值的大小。
ROUND_CEILING 接近正无穷大的舍入模式。如果 BigDecimal 为正,则舍入行为与 ROUND_UP 相同;如果为负,则舍入行为与 ROUND_DOWN 相同。注意,此舍入模式始终不会减少计算值。
ROUND_FLOOR 接近负无穷大的舍入模式。如果 BigDecimal 为正,则舍入行为与 ROUND_DOWN 相同;如果为负,则舍入行为与 ROUND_UP 相同。注意,此舍入模式始终不会增加计算值。
ROUND_HALF_UP 向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则为向上舍入的舍入模式。如果舍弃部分 >= 0.5,则舍入行为与 ROUND_UP 相同;否则舍入行为与 ROUND_DOWN 相同。注意,这是我们大多数人在小学时就学过的舍入模式(四舍五入)。
ROUND_HALF_DOWN 向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则为上舍入的舍入模式。如果舍弃部分 > 0.5,则舍入行为与 ROUND_UP 相同;否则舍入行为与 ROUND_DOWN 相同(五舍六入)。
ROUND_HALF_EVEN 向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则向相邻的偶数舍入。如果舍弃部分左边的数字为奇数,则舍入行为与 ROUND_HALF_UP 相同;如果为偶数,则舍入行为与 ROUND_HALF_DOWN 相同。注意,在重复进行一系列计算时,此舍入模式可以将累加错误减到最小。此舍入模式也称为“银行家舍入法”,主要在美国使用。四舍六入,五分两种情况。如果前一位为奇数,则入位,否则舍去。以下例子为保留小数点1位,那么这种舍入方式下的结果。1.15 -> 1.2 1.25 -> 1.2
ROUND_UNNECESSARY 默认的模式,断言请求的操作具有精确的结果,因此不需要舍入。如果对获得精确结果的操作指定此舍入模式,则抛出ArithmeticException。

这8种模式中,最常用的应该是 ROUND_HALF_UP 四舍五入,这个都知道。

还有一种ROUND_HALF_DOWN 五舍六入,这个是模式下,要看精度的后面的数值,一旦超过5 ,就入位。

1
2
3
new BigDecimal("3.1415").setScale(3, RoundingMode.HALF_DOWN); //3.141
new BigDecimal("3.14150001").setScale(3, RoundingMode.HALF_DOWN); //3.142
new BigDecimal("3.14156").setScale(3, RoundingMode.HALF_DOWN); //3.142

在这个模式下可以看到,即使是3.14150001 ,在保留3位小数的情况下,结果依然是进一位。


最难理解的是ROUND_HALF_EVEN 银行家舍入,四舍六入,五分两种情况,如果前一位为奇数,则入位,否则舍去。这种模式可以将错误减到最小,所以一般在银行使用。

1
2
3
4
5
6
new BigDecimal("3.14140").setScale(3, RoundingMode.HALF_EVEN); //3.141
new BigDecimal("3.14149").setScale(3, RoundingMode.HALF_EVEN); //3.141
new BigDecimal("3.14150").setScale(3, RoundingMode.HALF_EVEN); //3.142
new BigDecimal("3.14050").setScale(3, RoundingMode.HALF_EVEN); //3.140
new BigDecimal("3.14151").setScale(3, RoundingMode.HALF_EVEN); //3.142
new BigDecimal("3.14160").setScale(3, RoundingMode.HALF_EVEN); //3.142

看到这个示例,应该很清楚的了解这种模式。


四舍五入常用写法

一、Math(小数舍入为整数)

Math.ceil() 向上舍入,即它总是将数值向上舍入为最接近的整数;

Math.floor() 向下舍入,即它总是将数值向下舍入为最接近的整数;

Math.round()标准舍入,即它总是将数值四舍五入为最接近的整数。

1
2
3
4
5
6
7
8
9
10
11
Math.ceil(25.9) //26
Math.ceil(25.1) //26
Math.ceil(25.0)//25
Math.round(25.9) //26
Math.round(25.5) //26
Math.round(25.1) //25
Math.floor(25.9) //25
Math.floor(25.5) //25
Math.floor(25.1) //25


二、BigDecimal

这种方式就是前面说的设置精度和舍入模式。

1
2
BigDecimal b = new BigDecimal(Double.toString(3.1415));
double f1 = b.setScale(3, BigDecimal.ROUND_HALF_UP).doubleValue(); //3.142

这里就不多说了。


三、DecimalFormat

DecimalFormat 默认采用了RoundingMode.HALF_EVEN这种舍入模式,可以通过 setRoundingMode() 来重新设置舍入模式,而且format 之后的结果是一个字符串类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
double pi=3.1415927; //圆周率
//取一位整数
new DecimalFormat("0").format(pi);   //3
//取一位整数和两位小数
new DecimalFormat("0.00").format(pi); //3.14
//取两位整数和三位小数,整数不足部分以0填补。
new DecimalFormat("00.000").format(pi);// 03.142
//取所有整数部分
new DecimalFormat("#").format(pi);   //3
//以百分比方式计数,并取两位小数
new DecimalFormat("#.##%").format(pi); //314.16%
long c=299792458;  //光速
//显示为科学计数法,并取五位小数
new DecimalFormat("#.#####E0").format(c); //2.99792E8
//显示为两位整数的科学计数法,并取四位小数
new DecimalFormat("00.####E0").format(c); //29.9792E7
//每三位以逗号进行分隔。
new DecimalFormat(",###").format(c);   //299,792,458
//将格式嵌入文本
new DecimalFormat("光速大小为每秒,###米。").format(c);

DecimalFormat 类主要靠 # 和 0 两种占位符号来指定数字长度。0 表示如果位数不足则以 0 填充,# 表示只要有可能就把数字拉上这个位置。

详细见Java DecimalFormat的主要功能及使用方法


四、String.format

这是利用String 的 format 方法进行舍入,舍入模式默认就是四舍五入,而且不能更改舍入模式,至少我是不知道如何更改~~~

1
2
double d = 3.14149;
String result = String.format("%.3f", d); //3.141

关于 String.format 的用法,详见String.format


参考

Java DecimalFormat的主要功能及使用方法

Java大数类BigDecimal及八种舍入模式的介绍




坚持分享技术,但行好事,莫问前程 ~^o^~