JAVA中如何处理金额类数据

    技术2026-02-09  3

    由闲鱼的转账BUG引起的思考

    想写这一篇文章主要是前两天看到了一个闲鱼的BUG,而且自己也复现了这个问题。在使用闲鱼的时候当我向好友转账2.1的时候,最终支付宝显示的却只有2.09(前天出现的问题,目前闲鱼已经修复了这个问题)。作为一个消费者这只是一个BUG,但是作为一个JAVA开发,就让我思考到假如这个金额的数据是需要服务端进行处理?JAVA要如何处理这些数据。

    我的一分钱呢??????


    这从未设想的问题啊,所以这里就整理下对于服务端 的开发,对于金额的处理、储存和传输应该如何操作

    JAVA对金额数据的处理

    错误的付款金额

    在设计商品的数据结构时候,我们可能尝试将商品的价格设置为float或者double类型,而购买数量因为产品不同可能被设计为int或者long。当需要我们计算总价的时候,如果我们直接将单价*购买数量就会出现下面的情况:

    public static void main(String[] args) { float a = 72.49f; System.out.println("商品a单价:" + a); int n = 10; System.out.println("购买a数量:" + n); System.out.println("商品a总价:" + a*n); double d1 = 0.58D; long n1 = 100L; System.out.println("商品b单价:" + d1); System.out.println("购买b数量:" + n1); System.out.println("商品b总价:" + d1*n1); }

    上面代码看起来就是很简单的乘法运算,但是投入到生产中会出现很大的问题,它会得到下面的结果

    商品a单价:72.49 购买a数量:10 商品a总价:724.89996 商品b单价:0.58 购买b数量:100 商品b总价:57.99999999999999

    可以看到本来应该支付724.9元的订单只需要支付724.89。而本来需要58块的订单缺只需要57.99。无论是float还是double都出现了金额缺失的情况。

    使用BigDecimal进行金额计算

    因为float和double存在精度丢失问题所以在进行数字的精确计算的时候,我们需要通过BigDecmal来进行精确计算。

    将数字转换为BigDecimal

    BigDecimal提供了相当多的构造方法

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NeOuTEcX-1593868618419)(BCC0E85C61C04C38A0C7D2A4C3AD8F5F)]

    上面方法虽然多但是我们常用的构造方法就是下面几种,通过下面的方法来将String、int和long类型的数据转换为BigDecimal

    // BigDecimal(int) 创建一个具有参数所指定整数值的对象 BigDecimal num1 = new BigDecimal(10); // BigDecimal(long) 创建一个具有参数所指定长整数值的对象。 BigDecimal num2 = new BigDecimal(1000000L); //BigDecimal(String) 创建一个具有参数所指定以字符串表示的数值的对象 BigDecimal num12 = new BigDecimal("0.005");

    对于double和float的特殊处理

    如果我们将double或者float数据使用上面方式获取BigDecimal则会得到下面这种错误结果

    // 输出:72.48999786376953125 float a = 72.49f; BigDecimal num3 = new BigDecimal(a); System.out.println(num3); // 输出:0.57999999999999996003197111349436454474925994873046875 double d1 = 0.58D; BigDecimal num4 = new BigDecimal(d1); System.out.println(num4);

    有些文章中介绍可以使用其静态方法BigDecimal.valueOf(d1),但是此方法面对float的数据类型依旧无法准确输出内容。所以对于float我们最好将其转换为String后进行处理

    float a = 72.49f; String s = String.valueOf(a); System.out.println(s);

    对BigDecimal 数据进行操作

    BigDecimal提供了一系列的方法让我们更加精确的对数据进行处理

    方法作用例子解释add加法num1.add(num1和num2相加)num1和num2相加subtract减法num1.subtract(num2)num1减去num2multiply乘法num1.multiply(num2)num1乘 num2divide除法num2.divide(num1,2,BigDecimal.ROUND_HALF_UP)num2 除以 num1,并且保留两位小数divideToIntegralValue除法并获取其整数部分num2.divideToIntegralValue(num1)num2 除以 num1,并获取其整数部分compareTo比较大小num1.compareTo(num2)num1和num2比大小,如果num1小于num2则返回-1,相等则返回0,大于则返回1abs绝对值num3.abs()返回num3的绝对值

    特别需要注意!进行相关操作后并不会作用到原始数据上

    在BigDecimal数据进行上面操作后并不会影响其原始数据的值,下面的操作中最终会存在三个不一样的值,原始的数据bigDecimal1、bigDecimal2计算后的结果add。

    BigDecimal add = bigDecimal1.add(bigDecimal2); System.out.println(add); System.out.println(bigDecimal1); System.out.println(bigDecimal2);

    除法四舍五入操作

    除法操作时调用的方法

    public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode); public BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode);

    其最后一个参数用来确定小数的舍入的策略。

    数字参数枚举参数作用BigDecimal.ROUND_UPRoundingMode.UP被舍弃的小数位如果不是0,则舍弃部分前面的数字+1BigDecimal.ROUND_DOWNRoundingMode.DOWN不会对舍弃部分前面的数字+1BigDecimal.ROUND_CEILINGRoundingMode.CEILING如果结果是正数则使用RoundingMode.UP规则;如果结果是负数则使用RoundingMode.DOWN规则BigDecimal.ROUND_FLOORRoundingMode.FLOOR使用和RoundingMode.CEILING相反的策略BigDecimal.ROUND_HALF_UPRoundingMode.HALF_UP可以理解为四舍五入BigDecimal.ROUND_HALF_DOWNRoundingMode.HALF_DOWN舍弃部分 > 0.5,则舍入行为同 RoundingMode.UP;否则舍入行为同RoundingMode.DOWNBigDecimal.ROUND_HALF_EVENRoundingMode.HALF_EVEN如果距离相邻的数字相等,则向相邻的偶数舍入,如果不相等,则如果舍弃部分左边的数字为奇数,则舍入行为同RoundingMode.HALF_UP;如果为偶数,则舍入行为同RoundingMode.HALF_DOWNBigDecimal.ROUND_UNNECESSARYRoundingMode.UNNECESSARY判断是否精确操作,如果需要进行舍入操作则抛出异常

    金额类数据如何保存(MySQL)

    在金额类数据处理完后,我们需要保存到数据库中,而对于这些交易数据,根据每个系统涉及交易的规模和业务不同,目前有三种选择(实际上只写了两种,网上有人介绍使用String或者说varchar,说实话我是不喜欢将金额存储为字符串)。

    decimal

    使用decimal在数据库中可以非常精确的表示一个数据的值,而一般保存交易金额我们可以将其类型设置为decimal(M,S),M表示整数和小数部分的总长度,S表示其中小数部分的位数,对于日常交易过程中我们所使用的的最小单位是分,也就是0.01元,所以可以设置为2;

    long

    有些设计中,将数据库中金额的单位认为是分。对于这种设计对于金额的数据类型可以设置为long,此时对于值为100的数据,会被认定为1元,而不是100元。

    金额类数据如何传输

    关于在通过跨服务跨系统进行金额数据传输的时候,数据类型如何确定,可以直接参照支付宝SDK上的要求使用String数据类型


    个人水平有限,上面的内容可能存在没有描述清楚或者错误的地方,假如开发同学发现了,请及时告知,我会第一时间修改相关内容。假如我的这篇内容对你有任何帮助的话,麻烦给我点一个赞。你的点赞就是我前进的动力。

    Processed: 0.035, SQL: 9