BigDecimal精度问题

2023-11-20

BigDecimal精度问题

在近来项目展示价格的时候,遇到了一个问题,一个价格为99999.999的商品在购物车中的展示却是100000.00。

原因归结于在原项目代码中,是采用BigDecimal的格式,然而前端需要展示的时候,采用的却是String类型。

在类型转换的过程中,出现了精度损失。于是潜下心来对BigDecimal高精度的展示做了一番研究。

总结来看,BigDecimal转换成String主要有两种方式,且对于价格999999.999,两种方法的输出并不一致。具体代码如下:

BigDecimal price = new BigDecimal("999999.999");  //价格信息

//方法一
String format1 = String.format("%.2f", price); 
System.out.println("stringFormat:  "+format1); 	// 1000000.00

//方法二
String format2 = price.setScale(2, RoundingMode.DOWN).toPlainString();
System.out.println("scale:  "+ format2); 	// 999999.99

String.format方式

方法一,即String.format方式,是项目中最初使用的代码。为了一探究竟,追进format源码查看,可以看到代码如下:

public Formatter format(Locale l, String format, Object ... args) {
  ensureOpen();
  int last = -1;
  int lasto = -1;
  FormatString[] fsa = parse(format); // 首先解析字符串中特殊的标识符号
  for (int i = 0; i < fsa.length; i++) {
    FormatString fs = fsa[i]; 
    int index = fs.index(); // 获取到对应标识符号的下标
    try {
      switch (index) {
        case -2:  
          fs.print(null, l);
          break;
        case -1:  
          if (last < 0 || (args != null && last > args.length - 1))
            throw new MissingFormatArgumentException(fs.toString());
          fs.print((args == null ? null : args[last]), l);
          break;
        case 0:  
          lasto++;
          last = lasto;
          if (args != null && lasto > args.length - 1)
            throw new MissingFormatArgumentException(fs.toString());
          fs.print((args == null ? null : args[lasto]), l); // 关键代码
          break;
        default:  
          last = index - 1;
          if (args != null && last > args.length - 1)
            throw new MissingFormatArgumentException(fs.toString());
          fs.print((args == null ? null : args[last]), l);
          break;
      }
    } catch (IOException x) {
      lastException = x;
    }
  }
  return this;
}

可以看到关键方法其实是fs.print(),这里追进去查看print的源码。可以看到print源码首先会根据当前的类型做个判断,这里我们采用的是BigDecimal,因此会走到printFloat方法中。

    if (dt) {
        printDateTime(arg, l);
        return;
    }
    switch(c) {
    case Conversion.DECIMAL_INTEGER:
    case Conversion.OCTAL_INTEGER:
    case Conversion.HEXADECIMAL_INTEGER:
        printInteger(arg, l);
        break;
    case Conversion.SCIENTIFIC:
    case Conversion.GENERAL:
    case Conversion.DECIMAL_FLOAT:
    case Conversion.HEXADECIMAL_FLOAT: 
        printFloat(arg, l); // 方法会走到这里.
        break;
    case Conversion.CHARACTER:
    case Conversion.CHARACTER_UPPER:
        printCharacter(arg);
        break;
    case Conversion.BOOLEAN:
        printBoolean(arg);
        break;
    case Conversion.STRING:
        printString(arg, l);
        break;
    case Conversion.HASHCODE:
        printHashCode(arg);
        break;
    case Conversion.LINE_SEPARATOR:
        a.append(System.lineSeparator());
        break;
    case Conversion.PERCENT_SIGN:
        a.append('%');
        break;
    default:
        assert false;
    }
}

追入printFloat方法查看,可以看到,进一步的,printFloat方法又会对当前需要处理的对象进行类型的判断,并选取合适的方法进行处理。

private void printFloat(Object arg, Locale l) throws IOException {
  if (arg == null)
    print("null");
  else if (arg instanceof Float)
    print(((Float)arg).floatValue(), l);
  else if (arg instanceof Double)
    print(((Double)arg).doubleValue(), l);
  else if (arg instanceof BigDecimal)
    print(((BigDecimal)arg), l); // 关键方法
  else
    failConversion(c, arg);
}

追进BigDecimal对应的方法中,查看相应的print方法,可以看到关键方法其实是print(),因此我们继续追入查看相应代码。

private void print(BigDecimal value, Locale l) throws IOException {
  if (c == Conversion.HEXADECIMAL_FLOAT)
    failConversion(c, value);
  StringBuilder sb = new StringBuilder();
  boolean neg = value.signum() == -1; // 判断当前的数字符号是正数还是负数
  BigDecimal v = value.abs(); // 取其绝对值
  leadingSign(sb, neg); // 设置首个符号,'+' 或 ' ' 或 '(' 或 '-'
  print(sb, v, l, f, c, precision, neg); // 关键方法
  trailingSign(sb, neg); // 去除多余的符号
  a.append(justify(sb.toString()));
}
private void print(StringBuilder sb, BigDecimal value, Locale l,Flags f, char c, int precision, boolean neg)throws IOException{
  if(c == Conversion.SCIENTIFIC){
    ......
  } else if (c == Conversion.DECIMAL_FLOAT) {
    int prec = (precision == -1 ? 6 : precision); // 指的是我们当前设置的需要保留的精度,例子中为保留2位小数。
    int scale = value.scale(); // scale指的是当前数字的精度范围,例子中为3位小数因此是3。

    if (scale > prec) { // 如果当前位数大于要保留的位数
      int compPrec = value.precision();//这里comPrec指的是整个数的位数,例子中为9.
      if (compPrec <= scale) {
        value = value.setScale(prec, RoundingMode.HALF_UP);
      } else {
        compPrec -= (scale - prec); 
        // 重新计算减少后位数的值,随后调用BigDecimal方法重新整理位数
        // 而BigDecimal方法本身采用的RoundMode.HalfUp,因此在构造后会发生进位,导致结果出现差异。
        value = new BigDecimal(value.unscaledValue(), 
                               scale,
                               new MathContext(compPrec));
      }
    }
    BigDecimalLayout bdl = new BigDecimalLayout(
      value.unscaledValue(),
      value.scale(),
      BigDecimalLayoutForm.DECIMAL_FLOAT);

    char mant[] = bdl.mantissa();
    int nzeros = (bdl.scale() < prec ? prec - bdl.scale() : 0);

    if (bdl.scale() == 0 && (f.contains(Flags.ALTERNATE) || nzeros > 0))
      mant = addDot(bdl.mantissa());

    mant = trailingZeros(mant, nzeros);

    localizedMagnitude(sb, mant, f, adjustWidth(width, f, neg), l);
  }
}

至此,我们明白了第一种方法的弊端所在,即其是默认采用的RoundMode.halfUp方法,会自动进位导致数据出现偏差。

price.setScale方式

第二种方式,是修复后的方式,该方式的好处在于,可以自定义设置进位、舍入的策略,从而得到更符合逻辑的结果。具体源代码如下:

public BigDecimal setScale(int newScale, int roundingMode) {
  if (roundingMode < ROUND_UP || roundingMode > ROUND_UNNECESSARY)
    throw new IllegalArgumentException("Invalid rounding mode");
  int oldScale = this.scale;
  if (newScale == oldScale)        // 新旧位数一致则直接返回
    return this;
  if (this.signum() == 0)            // 0返回任意位数均可
    return zeroValueOf(newScale);
  if(this.intCompact!=INFLATED) { // 如果当前不超过-2^63(Long类型的上限),则进行计算
    long rs = this.intCompact; //rs为当前不带小数点的数值,eg. 999.99 ==> 99999 
    if (newScale > oldScale) { 
      // 如果新保留位数大于原位数
        int raise = checkScale((long) newScale - oldScale);
        if ((rs = longMultiplyPowerTen(rs, raise)) != INFLATED) {
          return valueOf(rs,newScale);
        }
        BigInteger rb = bigMultiplyPowerTen(raise);
        return new BigDecimal(rb, INFLATED, newScale, (precision > 0) ? precision + raise : 0);
      } else {	
      	// 否则计算需要丢弃的位数,并对应的创建新的BigDecimal对象
        int drop = checkScale((long) oldScale - newScale);
        if (drop < LONG_TEN_POWERS_TABLE.length) {
          // 关键方法
          return divideAndRound(rs, LONG_TEN_POWERS_TABLE[drop], newScale, roundingMode, newScale);
        } else {
          return divideAndRound(this.inflated(), bigTenToThe(drop), newScale, roundingMode, newScale);
        }
      }
  } else {
    if (newScale > oldScale) {
      int raise = checkScale((long) newScale - oldScale);
      BigInteger rb = bigMultiplyPowerTen(this.intVal,raise);
      return new BigDecimal(rb, INFLATED, newScale, (precision > 0) ? precision + raise : 0);
    } else {
      int drop = checkScale((long) oldScale - newScale);
      if (drop < LONG_TEN_POWERS_TABLE.length)
        return divideAndRound(this.intVal, LONG_TEN_POWERS_TABLE[drop], newScale, roundingMode,
                              newScale);
      else
        return divideAndRound(this.intVal,  bigTenToThe(drop), newScale, roundingMode, newScale);
    }
  }
}

紧接着我们追入divideAndRound方法中,

private static BigDecimal divideAndRound(long ldividend, long ldivisor, int scale, int roundingMode,
                                         int preferredScale) {
  int qsign; // 符号
  long q = ldividend / ldivisor; // 约到相应的位数,eg. 999999999/10 == > 99999999 
  if (roundingMode == ROUND_DOWN && scale == preferredScale)
    //如果是舍去的方案,则直接返回对应的值即可。
    return valueOf(q, scale); // 该方法会用对应的值创造一个BigDecimal的对象。 
  long r = ldividend % ldivisor; 
  qsign = ((ldividend < 0) == (ldivisor < 0)) ? 1 : -1;
  if (r != 0) {
    boolean increment = needIncrement(ldivisor, roundingMode, qsign, q, r);
    return valueOf((increment ? q + qsign : q), scale);
  } else {
    if (preferredScale != scale)
      return createAndStripZerosToMatchScale(q, scale, preferredScale);
    else
      return valueOf(q, scale);
  }
}

总结

总结来看,String.format方式默认采用的是Round_HalfUp的方式创建对应的对象,导致了数字出现进位的情况。而setScale方法,可以自由的选择对应的舍入方式,总体上看更为灵活。配合toPlainString方法可以很好的实现所有需要的功能。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

BigDecimal精度问题 的相关文章

  • 将特定项目移至列表末尾

    我有一个ArrayList in Java deleteItem createitem exportitem deleteItems createItems 我想移动包含的所有字符串delete到列表的末尾 所以我会得到下一个 create
  • 通过 WebStart 运行时 Java 7 更新 55 JacORB 错误

    自从更新到 Java 7 update 55 以来 我无法运行我的 WebStart java 应用程序 当通过 WebStart 启动时 该应用程序在 Java 7 update 51 下运行良好 当在 WebStart 之外启动时 它还
  • Java 1.4 上的 ActiveMQ 客户端

    我们在最新版本 5 6 0 中使用 Active MQ 现在我们遇到一个问题 必须连接一个新客户端 不幸的是这个客户端是用IBM JDK 1 4开发的 将 ActiveMQ 添加到应用程序会导致以下错误 UNEXPECTED ERROR O
  • setSize() 不起作用?

    我有一个程序 需要两个按钮 一个是常规按钮 另一个具有根据鼠标悬停而变化的图片 目前 由于图片很大 JButton自定义也很大 我可以更改自定义的大小并保持图像 和翻转图像 成比例吗 我尝试过 setSize 但它没有任何作用 对于任何反馈
  • InflateException 膨胀类 android.support.design.widget.CoordinatorLayout 时出错

    E AndroidRuntime 致命异常 main 进程 com atech a business PID 30662 java lang RuntimeException 无法启动活动 ComponentInfo com atech a
  • Android 上 WebRTC 的自定义视频源

    Overview 我想使用自定义视频源通过 WebRTC Android 实现来直播视频 如果我理解正确的话 现有的实现仅支持 Android 手机上的前置和后置摄像头 以下类与此场景相关 Camera1Enumerator java ht
  • 如何在android中播放内部和外部SD卡中的mp3文件?

    我正在开发一个 mp3 播放器应用程序 它可以播放内部 SD 卡内任何位置的 mp3 文件 我使用以下代码来获取内部存储中存在的 mp3 文件 ArrayList
  • 将 Flash 文件上传与 JSF 集成

    我看到我们可以通过flash文件上传来上传多个文件 喜欢SWF上传 http code google com p swfupload or YUI上传器 http yuilibrary com yui docs uploader 是否可以将
  • 使用 Jquery Ajax 将数据从 jsp 发送到 struts2 操作类

    我需要使用 jquery Ajax 将表单数据从 jsp 传递到 struts2 并从 Struts2 操作类接收回 JSON 数据 我已经给出了下面的代码 当我传递 AJAX 数据时 url search action searchTex
  • Play Framework 2.5.1 路由和依赖项注入(适用于 Java)

    我的 路线 文件中有这个 POST accounts controllers AccountsController createOneAccount 在我的 AccoutsController java 中 package controll
  • “传输协议线程失败” – “套接字为 EOF”,使用 Java 进行 J2SSH 连接

    我正在尝试通过我的 Java 代码建立 SSH 连接 但遇到异常 我通过 Putty Winscp 工具测试了我的连接 它工作正常 问题出在我的 Java 代码上 SEVERE The Transport Protocol thread f
  • 无法为对象堆保留足够的空间

    每次尝试运行该程序时 我都会重复出现以下异常 VM初始化期间发生错误 无法为对象堆保留足够的空间 无法创建Java虚拟机 我尝试增加虚拟内存 页面大小 和 RAM 大小 但无济于事 我怎样才能消除这个错误 运行 JVM XX MaxHeap
  • 计算事件之间的天数 - Android

    我一直在研究 Android API 并一直在寻找一种方法来计算当前日期和未来日期之间的天数 我对 android 还很陌生 而且我已经有几年没有做过 java 了 计算这个最简单的方法是什么 Thanks 最简单的方法是使用乔达时间 ht
  • JPA中如何连接多个数据库?

    我有一个 Spring Boot 应用程序 当前使用 JPA 连接到单个数据库 application properties 文件中的连接详细信息 spring datasource url jdbc oracle thin localho
  • 需要使用自定义类而不是在 Web 服务中生成(通过 wsimport)

    您能帮忙解决以下问题吗 当生成 WS 客户端代码 使用 wsimport ant 任务 时 所有类都会在与 Web 服务相同的包 例如 helloservice endpoint 中自动生成 例如如果我的网络服务有方法 公共节点 getNo
  • 如何使用 Java 1.4 和 SAX 将任意数据编码为 XML?

    我们使用 SAX 来解析 XML 因为它不需要将整个 XML 文档读入内存来解析单个值 我读过很多文章 坚持认为 SAX 只能用于解析 解码 XML 而不能创建它 这是真的 不 这不是真的 您可以使用类似于以下内容的方式将 XML 编码为
  • 根据另一个列表的顺序对列表进行排序[重复]

    这个问题在这里已经有答案了 我需要对列表进行排序Person对象 List
  • 运行外部进程的非阻塞线程

    我创建了一个 Java GUI 应用程序 它充当许多低级外部进程的包装器 该实用程序按原样运行 但迫切需要一项重大改进 我希望我的外部进程以非阻塞方式运行 这将允许我并行服务其他请求 简而言之 我希望能够在生成数据时处理来自外部进程的数据
  • 与 System.in.read() 一起使用的文件结尾/流键盘组合是什么

    如果这个小问题已经得到解答 我深表歉意 我无法在SO找到它 使用以下 Java 简单代码从 IDE 控制台读取行 Windows 7 和 Eclipse Kepler int v try while v System in read 1 S
  • Java 和 SQL Server 中的精度噩梦

    我一直在与 Java 和 SQL Server 中的精确噩梦作斗争 直到我不再知道了 就我个人而言 我理解这个问题及其根本原因 但向地球另一端的客户解释这一点是不可行的 至少对我来说 情况是这样的 我在 SQL Server 中有两列 Qt

随机推荐

  • IDEA运行缓慢卡顿,解决idea卡顿,控制台中文乱码 以及其它常用设置

    IDEA运行缓慢卡顿 解决idea卡顿问题以及常用设置 IDEA卡顿原因 优化IDEA配置 重点推荐的方法 手动修改IDEA配置步骤 其他卡顿优化 参考 1 idea启动时会有两个快捷方式 2 卸载不需要用的插件 3 减少内存 4 适当关闭
  • HttpClient 简介说明

    转自 HttpClient 简介说明 下文笔者将讲述HttpClient框架的简介说明 如下所示 HttpCient简介说明 HttpClient是一个开源项目 它是Apache Jakarta Common下的一个子项目 HttpClie
  • Invalid Address specified to RtlValidateHeap

    Invalid Address specified to RtlValidateHeap VC编程 最后推出对话框的时候 会有错误提示声音 硄 但是没有弹出错误提示对话框 症状描述与下面的类似 声音就和Assertion Failure一样
  • html遍历数组,JS数组遍历的几种方式

    JS数组遍历 基本就是for forin foreach forof map等等一些方法 以下介绍几种本文分析用到的数组遍历方式以及进行性能分析对比 第一种 普通for循环 代码如下 for j 0 j lt arr length j 简要
  • 【三电平SVPWM学习

    导读 本期对三电平SVPWM的原理和建模做一个简单介绍 并与两电平SVPWM做了一个对比 后面把三电平的SVPWM运用到异步电机直接转矩控制中 看与传统的两电平SVPWM 控制性能是否得到改善 模型可分享 关注公众号 浅谈电机控制 留下邮箱
  • 八大排序算法(六)——优先队列、堆和堆排序

    6 1 API 优先队列是一种抽象数据类型 它表示了一组值和对这些值的操作 优先队列最重要的操作就是删除最大元素和插入元素 6 2 初级实现 6 2 1 数组实现 无序 或许实现优先队列最简单方法就是基于下压栈的代码 insert 方法的代
  • java通过文件路径读取该路径下的所有文件并将其放入list中

    java通过文件路径读取该路径下的所有文件并将其放入list中 java中可以通过递归的方式获取指定路径下的所有文件并将其放入List集合中 假设指定路径为path 目标集合为fileList 遍历指定路径下的所有文件 如果是目录文件则递归
  • 旋转链表(leetcode)

    61 旋转链表 给你一个链表的头节点 head 旋转链表 将链表每个节点向右移动 k 个位置 示例 1 输入 head 1 2 3 4 5 k 2 输出 4 5 1 2 3 示例 2 输入 head 0 1 2 k 4 输出 2 0 1 提
  • centos安装配置hadoop超详细过程(含故障排除)

    1 集群部署介绍 1 1 Hadoop简介 Hadoop是Apache软件基金会旗下的一个开源分布式计算平台 以Hadoop分布式文件系统 HDFS Hadoop Distributed Filesystem 和MapReduce Goog
  • 计算机科学丛书(2014-2018.Q1)

    ISBN 名称 作者 出版时间 978 7 111 53451 8 数学设计和计算机体系结构 原书第2版 美 戴维 莫尼 哈里斯 莎拉 L 哈里斯著 978 7 111 44075 8 嵌入式计算系统设计原理 美 Marilyn Wolf著
  • C#中string.Format输出内容中含有花括号的解决方法

    转载一篇 版权声明 本文为CSDN博主 九德真君 的原创文章 遵循CC 4 0 BY SA版权协议 转载请附上原文出处链接及本声明 原文链接 https blog csdn net lzdidiv article details 69469
  • python matrix用法_numpy中matrix使用方法

    matrix T transpose 返回矩阵的转置矩阵 matrix H hermitian conjugate transpose 返回复数矩阵的共轭元素矩阵 matrix I inverse 返回矩阵的逆矩阵 matrix A bas
  • 基于C语言实现的文件压缩算法-哈夫曼编码

    哈夫曼编码 是一种数据压缩算法 通常用于无损数据压缩 该算法是由 David A Huffman在麻省理工学院就读理学博士 Doctor of Science 的时候发明的 这位大佬在1952年发表了相关的一篇论文A Method for
  • 页面上input输入框宽度实现自动调整

    input输入框宽度实现自动调整 本文介绍两种方式 一是通过获取input内容的宽度实现输入框宽度的自动调整 二是通过内容字符串的长度乘以文本字体大小的积 来实现输入框宽度的自动调整 1 input输入框宽度的获取方式一 由于input输入
  • 利用maven项目创建一个web项目工程(图文详解)

    最近正开始学习java 老师布置的第一个作业便是用maven项目创建一个web项目工程 不会 跑去百度了半天 跳出来的全是我们csdn的教程 但到创建资源包的时候就没法跟着做了 因为显示已存在 没法跟着创建了 那些文章基本一样的讲法 都是默
  • 超详细的用IDEA整合SSM框架和profile 配置环境

    SSM作为现在最流行的开发框架 很大的提升了开发效率 一些同学在SSM基础上的整合一些更实用的开发基础框架 被用来作为快速开发的基础框架 本次实践主要是以下3个目标 从最基础的SSM框架做起 摸索和处理SSM框架整合中可能会遇到的问题 使用
  • 手机知识:手机的快充技术是什么,看完本文你就懂了

    目录 1 什么是手机快充 2 目前主流的手机快充协议 2 1 PD协议 2 2 PE协议 联发科 2 3 QC协议 高通 2 4 VOOC闪充 OPPO厂商 2 5 SCP FCP闪充 华为厂商 2 6 FlashCharge闪充 Vivo
  • 【前后端数据交互:Axios】

    前后端数据交互 Axios Axios 介绍 在前端页面展示的数据大多数都是通过访问一个API获取的 做这件事的方法有好几种 例如jquery ajax vue resource axios 而vue resource是vue插件 但3版本
  • 基于RGB颜色空间使用OpenCV-Python实现照片换底

    前往老猿Python博文目录 https blog csdn net LaoYuanPython 一 引言 前一阵子家人报考教师资格证考试 因报名需要将蓝底的数字相片换成白底的 老猿虽然在学习图像处理相关开发技术 但并没有熟练使用的图像编辑
  • BigDecimal精度问题

    BigDecimal精度问题 在近来项目展示价格的时候 遇到了一个问题 一个价格为99999 999的商品在购物车中的展示却是100000 00 原因归结于在原项目代码中 是采用BigDecimal的格式 然而前端需要展示的时候 采用的却是