【浮点类型计算的误差】

  在财务模块的设计中,一定会涉及到金额的处理,其中字段类型的设计很关键,如果采用了float和double类型,计算结果会有误差。

        float i = 1.1f;
        float j = 1f;
        System.out.println(i - j);
        //0.100000024    

  所以,在涉及到要求精度精确的金额时,一般会采用decimal类型存储在DB中,而Java计算的过程则采用BigDecimal。

 

  【历史包袱】

  在一些早期的财务软件中,或者很多初期没有考虑这方面问题的软件中,金额类的字段在数据库中也会被设置为float。如金蝶、用友等企业的早期产品往往也是用float进行金额数据的存储。但这样还怎么保证数据的准确性呢?

  其实相当多的系统设计中,系统怎么去存储数据和怎么去计算数据并不统一。有的会以float存储,但是计算的时候还是会采用decimal的转换,再进行计算,虽然也能保证结果的准确性,但还是很麻烦;另外,在使用MySQL的自带函数时会有一些麻烦,还涉及到类型的转换(未尝试,读者可以自行验证)。也就是说,存储数据用什么格式,看具体的场景,可以是int,可以是float,但是只要计算过程保证使用了decimal,就保证了计算的精度。

 

  【为什么是decimal】

  在设计实体类型时,我们用BigDecimal定义字段,数据库中以decimal存储,那么存取数据,计算过程都统一这一种类型。不必在取出来时,再进行一次类型转换,避免了很多出bug的可能;如果是Integer型或者Long存以分为单位的数据,那么存取,展示(通常是以'元'为单位)的时候都要进行单位转换,这样也很容易出错;另外,在生产过程中,通常也会有一些场景,管理人员会直接从数据库中导出数据,不经过系统拿到数据,单位是分的话是不符合财务人员的使用习惯的,这人为地增加了沟通成本。所以综合来看,decimal是一个比较好的选择,也是业界常规的做法。

 

  【关于误差】

  上面讨论了字段类型和精度的关系,这里要说明的是财务数据的误差。财务数据的计算结果有误差是避免不了的,所以财会领域有一个专业词汇叫做调账,调账就是为了调整误差带来的账目差异。

  但是,我们的代码还是要保证整个计算结果的精确,这里要表达的是,就算是有误差,那么这个计算结果的误差,也应该同财务人员计算结果的误差保持一致。为了达到这个目的,我们需要保证2点:

  1.在关键的,会出现误差的计算过程中,精度与财务人员的要求保持一致;

  2.整个计算的步骤,也要与财务人员的计算步骤保持一致。

  关于第2点,并不是说,理论上和逻辑上与这个流程一致就行,而是要在步骤上一致。这是因为步骤不同,就算逻辑处理是等价的,最终的计算结果产生的误差肯定不会一样,这就违背了刚刚说明的原则。

  比如10笔订单,每笔订单收益1元,而某个代理商收益为30%。计算过程如果是先进行单笔计算,计算完汇总,保留2位小数,那么结果是3.30;但如果是先进行汇总,再进行收益计算,结果是3.33。当订单数据巨大时,这个收益的误差也会是巨大的。

  当然,如果是考虑计算效率,存储效率等问题,需要对这个计算过程进行调整,只要财务人员接受由此带来的误差,也是OK的。

  3.以上两点是基于技术与业务来说的,这一点是基于商业上的考虑。在对用户进行收费的过程中,计算的步骤,计算的精度保留,所带来的误差,如果商家的算法设置的合理,可以通过合理的舍或进的手段获取到由于计算精度带来的额外收益。在这个过程中,用户其实是无感的,比如一笔订单,用户的损失可能不足1分钱,但是对于商家来说,订单数量特别大时,这个额外的收益也是很可观的。

 

  【Java中的BigDecimal】

  基本用法:

    public static void main(String[] args) {
        BigDecimal number1 = new BigDecimal(0.005);
        BigDecimal number2 = new BigDecimal(1000000);

        BigDecimal stringParseNumber1 = new BigDecimal("0.005");
        BigDecimal stringParseNumber2 = new BigDecimal("1000000");

        //加法
        BigDecimal result = number1.add(number2);
        BigDecimal stringParsedResult = stringParseNumber1.add(stringParseNumber2);

        System.out.println("result=" + result);
        System.out.println("stringParsedResult=" + stringParsedResult);

        //减法
        BigDecimal subtract = stringParseNumber1.subtract(stringParseNumber1);
        //乘法
        BigDecimal multiply = stringParseNumber1.multiply(stringParseNumber2);
        //绝对值
        BigDecimal abs = stringParseNumber1.abs();

        //除法:必须制定小数后的精确位数,以及进位的原则
        BigDecimal divisor = new BigDecimal("10");
        BigDecimal dividend = new BigDecimal("3");
        BigDecimal divideResult = divisor.divide(dividend, 3, BigDecimal.ROUND_HALF_UP);
        System.out.println(divideResult);
    }

  由以上结果中,可以看到,如果构造函数中传入的是double或者float类型,那么BigDecimal的计算结果还是不准确。这里推荐使用String类型作为构造函数的参数。

  对于除法,第二个参数表示计算结果小数保留位数,第三个参数表示进位的算法。这里列举所有的算法,在实际场景中可以根据需求选用:

ROUND_UP //对非舍去的部分始终在保留的最低位加1
ROUND_DOWN //与ROUND_UP相反,始终不对非舍去的部分加1
ROUND_CEILING //如果为正,则舍入原则与ROUND_UP一致,如果为负,则舍入原则与ROUND_DOWN一致 ROUND_FLOOR //与CEILING刚好相反 可以将这两种模式理解为坐标轴的方向
ROUND_HALF_UP //四舍五入 ROUND_HALF_DOWN //五舍六入 ROUND_HALF_EVEN //银行家舍入法,主要在美国使用。四舍六入是肯定的,五分为两种情况,前一位为奇数,则入位,前一位为偶数,则舍去。 ROUND_UNNECESSARY //断言使用

  这里给出实际的使用场景,体会一下进位的结果:

 

        System.out.println(new BigDecimal("0.1203456789").divide(new BigDecimal("1"),3,BigDecimal.ROUND_UP));//0.121
        System.out.println(new BigDecimal("-0.1203456789").divide(new BigDecimal("1"),3,BigDecimal.ROUND_UP));//-0.121
        System.out.println(new BigDecimal("0.1891").divide(new BigDecimal("1"),3,BigDecimal.ROUND_UP));//0.190
        System.out.println(new BigDecimal("0.91").divide(new BigDecimal("1"),1,BigDecimal.ROUND_UP));//1.0

        System.out.println(new BigDecimal("0.1203456789").divide(new BigDecimal("1"),3,BigDecimal.ROUND_DOWN));//0.120
        System.out.println(new BigDecimal("-0.1203456789").divide(new BigDecimal("1"),3,BigDecimal.ROUND_DOWN));//-0.120
        System.out.println(new BigDecimal("0.1891").divide(new BigDecimal("1"),3,BigDecimal.ROUND_DOWN));//0.189
        System.out.println(new BigDecimal("0.91").divide(new BigDecimal("1"),1,BigDecimal.ROUND_DOWN));//0.9

 

 

posted on 2021-07-03 21:51  长江同学  阅读(869)  评论(0编辑  收藏  举报