【浮点类型计算的误差】
在财务模块的设计中,一定会涉及到金额的处理,其中字段类型的设计很关键,如果采用了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
原网址: 访问
创建于: 2022-04-13 10:20:01
目录: default
标签: 无
未标明原创文章均为采集,版权归作者所有,转载无需和我联系,请注明原出处,南摩阿彌陀佛,知识,不只知道,要得到
java windows火焰图_mob64ca12ec8020的技术博客_51CTO博客 - 在windows下不可行,不知道作者是怎样搞的 监听SpringBoot 服务启动成功事件并打印信息_监听springboot启动完毕-CSDN博客 SpringBoot中就绪探针和存活探针_management.endpoint.health.probes.enabled-CSDN博客 u2u转换板 - 嘉立创EDA开源硬件平台 Spring Boot 项目的轻量级 HTTP 客户端 retrofit 框架,快来试试它!_Java精选-CSDN博客 手把手教你打造一套最牛的知识笔记管理系统! - 知乎 - 想法有重合-理论可参考 安宇雨 闲鱼 机械键盘 客制化 开贴记录 文本 linux 使用find命令查找包含某字符串的文件_beijihukk的博客-CSDN博客_find 查找字符串 ---- mac 也适用 安宇雨 打字音 记录集合 B站 bilibili 自行搭建 开坑 真正的客制化 安宇雨 黑苹果开坑 查找工具包maven pom 引用地 工具网站 Dantelis 介绍的玩轴入坑攻略 --- 关于轴的一些说法 --- 非官方 ---- 心得而已 --- 长期开坑更新 [本人问题][新开坑位]关于自动化测试的工具与平台应用 机械键盘 开团 网站记录 -- 能做一个收集的程序就好了 不过现在没时间 -- 信息大多是在群里发的 - 你要让垃圾佬 都去一个地方看难度也是很大的 精神支柱 [超级前台]sprinbboot maven superdesk-app 记录 [信息有用] [环境准备] [基本完成] [sebp/elk] 给已创建的Docker容器增加新的端口映射 - qq_30599553的博客 - CSDN博客 [正在研究] Elasticsearch, Logstash, Kibana (ELK) Docker image documentation elasticsearch centos 安装记录 及 启动手记 正式服务器 39 elasticsearch 问题合集 不断更新 6.1.1 | 6.5.1 两个版本 博客程序 - 测试 - bug记录 等等问题 laravel的启动过程解析 - lpfuture - 博客园 OAuth2 Server PHP 用 Laravel 搭建带 OAuth2 验证的 RESTful 服务 | Laravel China 社区 - 高品质的 Laravel 和 PHP 开发者社区 利用Laravel 搭建oauth2 API接口 附 Unauthenticated 解决办法 - 煮茶的博客 - SegmentFault 思否 使用 OAuth2-Server-php 搭建 OAuth2 Server - 午时的海 - 博客园 基于PHP构建OAuth 2.0 服务端 认证平台 - Endv - 博客园 Laravel 的 Artisan 命令行工具 Laravel 的文件系统和云存储功能集成 浅谈Chromium中的设计模式--终--Observer模式 浅谈Chromium中的设计模式--二--pre/post和Delegate模式 浅谈Chromium中的设计模式--一--Chromium中模块分层和进程模型 DeepMind 4 Hacking Yourself README.md update 20211011
Laravel China 简书 知乎 博客园 CSDN博客 开源中国 Go Further Ryan是菜鸟 | LNMP技术栈笔记 云栖社区-阿里云 Netflix技术博客 Techie Delight Linkedin技术博客 Dropbox技术博客 Facebook技术博客 淘宝中间件团队 美团技术博客 360技术博客 古巷博客 - 一个专注于分享的不正常博客 软件测试知识传播 - 测试窝 有赞技术团队 阮一峰 语雀 静觅丨崔庆才的个人博客 软件测试从业者综合能力提升 - isTester IBM Java 开发 使用开放 Java 生态系统开发现代应用程序 pengdai 一个强大的博主 HTML5资源教程 | 分享HTML5开发资源和开发教程 蘑菇博客 - 专注于技术分享的博客平台 个人博客-leapMie 流星007 CSDN博客 - 舍其小伙伴 稀土掘金 Go 技术论坛 | Golang / Go 语言中国知识社区
最新评论