一、前言
在我们日常工作中,经常会有涉及到数字的运算,其中金额的运算尤其重要且敏感,因为金额的运算若不注意处理的话,很容易因为精度的丢失,从而导致最终数据的异常,造成严重的系统错误。本文将对java中金额的运算处理进行简单小结。
二、NumberFormat类、DecimalFormat类、BigDecimal类简介
1、NumberFormat类
NumberFormat类是所有数值格式的抽象基类,它继承了Format抽象类。NumberFormat类提供了格式化和分析数值的接口,还提供了一些方法来确定哪些语言环境具有数值格式,以及它们的名称是什么。其常用的方法说明如下:
//返回当前缺省语言环境的缺省数值格式
public final static NumberFormat getInstance();
//返回当前缺省语言环境的通用格式
public final static NumberFormat getCurrencyInstance();
//返回当前缺省语言环境的通用数值格式
public final static NumberFormat getNumberInstance();
//返回当前缺省语言环境的百分比格式
public final static NumberFormat getPercentInstance();
另外,这几个方法都有相应的通过Locale类指定当前环境的方法。
例1:
//输出格式化后的数字
public class NumberTest {
public static void main(String[] args) {
double a = 12345.123456;
double b = 0.123456;
double c = 12345.67896789;
double d = 0.125555;
String s1 = NumberFormat.getInstance().format(a);
String s2 = NumberFormat.getCurrencyInstance().format(a);
String s3 = NumberFormat.getNumberInstance().format(a);
String s4 = NumberFormat.getPercentInstance().format(b);
String s5 = NumberFormat.getInstance().format(c);
String s6 = NumberFormat.getPercentInstance().format(d);
System.out.println("s1->" + s1);
System.out.println("s2->" + s2);
System.out.println("s3->" + s3);
System.out.println("s4->" + s4);
System.out.println("s5->" + s5);
System.out.println("s6->" + s6);
}
}
输出
s1->12,345.123
s2->¥12,345.12
s3->12,345.123
s4->12%
s5->12,345.679
s6->13%
注意:格式化后,保留位数小数位后是四舍五入的。
当然,如果说你想对某一个数字精确保留指定位数的话,可以通过相关参数来设置。
例2:
public class NumberTest {
public static void main(String[] args) {
double a = 12345.6789;
double b = 1.2;
NumberFormat format = NumberFormat.getInstance();
//设置数值的整数部分允许的最大位数
format.setMaximumIntegerDigits(3);
//设置数值的整数部分允许的最小位数
format.setMinimumIntegerDigits(3);
// 设置数值的小数部分允许的最大位数
format.setMaximumFractionDigits(3);
//设置数值的小数部分允许的最小位数
format.setMinimumFractionDigits(3);
System.out.println("a->" + format.format(a));
System.out.println("b->" + format.format(b));
}
}
输出
a->345.679
b->001.200
2、DecimalFormat类
DecimalFormat类是NumberFormat的一个具体子类,用于格式化十进制数字,通常用于涉及高精度的运算。DecimalFormat类主要靠 # 和 0 两种占位符号来指定数字长度;0 表示如果位数不足则以 0 填充,# 表示只要有可能就把数字拉上这个位置。
例3:
public class NumberTest {
public static void main(String[] args) {
double a = 123.456789;
double b = 1.2;
double c = 0.12345;
long d = 123456789;
//最少取2位整数,整数不足部分以0填补
String s1 = new DecimalFormat("00").format(a);
//最少取1位整数、取3位小数
String s2 = new DecimalFormat("0.000").format(a);
//最少取2位整数、取3位小数,位数不足以0填补
String s3 = new DecimalFormat("00.000").format(b);
//取所有整数部分
String s4 = new DecimalFormat("#").format(a);
//以百分比方式计数,并最多取两位小数
String s5 = new DecimalFormat("#.##%").format(c);
//以百分比方式计数,且整数部分、小数部分都保留2位,位数不足以0填补
String s6 = new DecimalFormat("00.00%").format(b);
//"\u2030"表示乘以1000并显示为千分数,要放在最后
String s7 = new DecimalFormat("00.00\u2030").format(c);
//显示为科学计数法,并取5位小数
String s21 = new DecimalFormat("#.#####E0").format(d);
//显示为2位整数的科学计数法,并取4位小数
String s22 = new DecimalFormat("00.####E0").format(d);
//每3位以逗号进行分隔
String s23 = new DecimalFormat(",###").format(d);
//每3位以逗号进行分隔,且最少三位
String s24 = new DecimalFormat(",000").format(b);
//将格式嵌入文本
String s25 = new DecimalFormat("嵌入的数字是,###这个数").format(d);
//用#和0的唯一区别是0在数位不足时会自动补足
String s31 = new DecimalFormat("00.00").format(a);
String s32 = new DecimalFormat("##.##").format(a);
String s33 = new DecimalFormat("00.00").format(b);
String s34 = new DecimalFormat("##.##").format(b);
//可以用applyPattern()方法修改Format的模式
DecimalFormat sf = new DecimalFormat("00");
String s41 = sf.format(a);
sf.applyPattern("0.000");
String s42 = sf.format(a);
System.out.println("s1->" + s1);
System.out.println("s2->" + s2);
System.out.println("s3->" + s3);
System.out.println("s4->" + s4);
System.out.println("s5->" + s5);
System.out.println("s6->" + s6);
System.out.println("s7->" + s7);
System.out.println("s21->" + s21);
System.out.println("s22->" + s22);
System.out.println("s23->" + s23);
System.out.println("s24->" + s24);
System.out.println("s25->" + s25);
System.out.println("s31->" + s31);
System.out.println("s32->" + s32);
System.out.println("s33->" + s33);
System.out.println("s34->" + s34);
System.out.println("s41->" + s41);
System.out.println("s42->" + s42);
}
}
输出
s1->123
s2->123.457
s3->01.200
s4->123
s5->12.34%
s6->120.00%
s7->123.45‰
s21->1.23457E8
s22->12.3457E7
s23->123,456,789
s24->001
s25->嵌入的数字是123,456,789这个数
s31->123.46
s32->123.46
s33->01.20
s34->1.2
s41->123
s42->123.457
备注:用#和0的唯一区别是0在数位不足时会自动补足。更多Format模式详情请参考JDK文档。
3、BigDecimal类
float、double类型的主要设计目标是为了科学计算和工程计算,并没有提供完全精确的结果;java.math包下的BigDecimal类则可以满足高精度运算结果,通常用于商业上的精确运算。
BigDecimal类运算结果精确,也可用于大数的运算,但BigDecimal类的运算是通过构造函数创建运算对象,然后对对象进行运算,因此,其不能像 int 、float 、double 、long 等数据类型的数字,直接使用+ 、- 、* 、/ 等算术运算符对其对象进行数学运算 ,而必须调用其相对应的方法进行运算。
其常用构造方法如下:
序号 | 方法 | 描述 |
---|---|---|
1 | public BigDecimal(double val); | 将double表示形式转换为BigDecimal |
2 | public BigDecimal(int val); | 将int表示形式转换为BigDecimal |
3 | public BigDecimal(long val); | 将long表示形式转换为BigDecimal |
4 | public BigDecimal(String val); | 将String表示形式转换为BigDecimal |
其中,因为String构造方法的结果是完全可预知的, 所以通常建议优先使用。
其常用运算方法如下:
序号 | 方法 | 描述 |
---|---|---|
1 | public BigDecimal add(BigDecimal augend); | 加法 |
2 | public BigDecimal subtract(BigDecimal subtrahend); | 减法 |
3 | public BigDecimal multiply(BigDecimal multiplicand); | 乘法 |
4 | public BigDecimal divide(BigDecimal divisor); | 除法; 如果准确的商值没有无穷的十进制扩展,抛ArithmeticException异常 |
5 | public BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode); | 除法;设置精确位数、保留位数的策略。RoundingMode为舍入模式,更多舍入模式请查阅API文档 |
6 | public int compareTo(BigDecimal val); | 比较;当此 BigDecimal 在数字上小于、等于或大于 val 时,返回 -1、0 或 1 |
7 | public int scale(); | 返回此 BigDecimal 的标度(即小数点后位数) |
8 | public BigDecimal setScale(int newScale, RoundingMode roundingMode); | 返回 BigDecimal,其标度为指定值,其非标度值通过此 BigDecimal 的非标度值乘以或除以十的适当次幂来确定,以维护其总值 |
例4:
public class NumberTest {
public static void main(String[] args) {
BigDecimal a = new BigDecimal("12.345");
BigDecimal b = new BigDecimal("6.78");
String s1 = a.add(b).toString();
String s2 = a.subtract(b).toString();
String s3 = a.multiply(b).toString();
String s4 = a.divide(b,5,RoundingMode.HALF_UP).toString(); //四舍五入
String s5 = new BigDecimal("10").divide(new BigDecimal("4")).toString(); //商要是有限位数,否侧会抛异常
System.out.println("s1:a+b=" + s1);
System.out.println("s2:a-b=" + s2);
System.out.println("s3:a*b=" + s3);
System.out.println("s4:a/b=" + s4);
System.out.println("s5->" + s5);
}
}
输出
s1:a+b=19.125
s2:a-b=5.565
s3:a*b=83.69910
s4:a/b=1.82080
s5->2.5
三、处理数字精度问题常用方法
通过以上NumberFormat类、DecimalFormat类、BigDecimal类这3个类的简单介绍,我们可以知道,在不用的应用场景下,通过灵活运用这3个类及相关类,我们就可以实现高精度的运算。具体用法可参照以上介绍,下面对数字运算做一些其它方面的补充。
1、在业务系统涉及到金额时,不少人的做法是,金额的单位为元(业务显示一般为元),然后运算、DB存储时均使用double类型、保留2位小数。其实这样很容易造成精度的丢失,更合适的做法是:我们用分来表示金额的单位,在运算的时候直接用int、long数据类型,最后显示的时候再转化成元,这样的话,很大程度上就避免精度丢失的问题了。(金额运算大多数为加、减,乘、除较少)
2、可以合理地运用一些第三方工具包,如:apache.commons.lang3包中的math包,其Fraction 类可用于分数的计算、NumberUtils类可用于数字大小比较、RandomUtils类可用于随机数操作等。
四、总结
1、存储计算金额时,最好直接存整数(表示单位为分、厘、毫),然后直接对整数进行加减运算,最后在最终展示的时候,再换算成所需的单位。
2、需要保证精度的运算最好使用BigDecimal类,因为其精度准确,且与其它基本数据类型装换方便。
3、合理利用一些成熟可靠的第三方工具类,可以给数字相关运算带来很大的便利。