Java BigDecimal知多少?

简介: Java BigDecimal知多少?

大家都知道小六六是搞支付的,那么搞支付的话,对于金钱的精度还是要细心的,对于amout这个字段的取值类型还是需要我们多多我们注意的,因为一不小心就能搞出个不小的事故

BigDecimal概述

Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数,但在实际应用中,可能需要对更大或者更小的数进行运算和处理。一般情况下,对于那些不需要准确计算精度的数字,我们可以直接使用Float和Double处理,但是Double.valueOf(String) 和Float.valueOf(String)会丢失精度。所以开发中,如果我们需要精确计算的结果,则必须使用BigDecimal类来操作。

BigDecimal所创建的是对象,故我们不能使用传统的+、-、*、/等算术运算符直接对其对象进行数学运算,而必须调用其相对应的方法。方法中的参数也必须是BigDecimal的对象。构造器是类的特殊方法,专门用来创建对象,特别是带有参数的对象

阿里手册的BigDecimal

浮点数使用计算机存储时,存在精度丢失的问题。如果遇到浮点数算术运算或比较运算时,一种推荐的做法是使用BigDecimal

在使用BigDecimal进行浮点数运算时,根据阿里巴巴《Java开发手册》,有以下编程归约:

编程归约一:

image.png

大家都知道BigDecimal是一个对象,所以我们不能用==去比较大小,但是用equals的话 1.0和1.00是不想等的所以也不能用。

编程归约二:

image.png

BigDecimal的Api的用法

构造方法

  • BigDecimal(int) 创建一个具有参数所指定整数值的对象
  • BigDecimal(double) 创建一个具有参数所指定双精度值的对象
  • BigDecimal(long) 创建一个具有参数所指定长整数值的对象
  • BigDecimal(String) 创建一个具有参数所指定以字符串表示的数值的对象 推荐构造方法用String 因为参数类型为double的构造方法的结果有一定的不可预知性

加减乘除

  • add(BigDecimal) BigDecimal对象中的值相加,返回BigDecimal对象
  • subtract(BigDecimal) BigDecimal对象中的值相减,返回BigDecimal对象
  • multiply(BigDecimal) BigDecimal对象中的值相乘,返回BigDecimal对象
  • divide(BigDecimal) BigDecimal对象中的值相除,返回BigDecimal对象
  • toString() 将BigDecimal对象中的值转换成字符串
  • doubleValue() 将BigDecimal对象中的值转换成双精度数
  • floatValue() 将BigDecimal对象中的值转换成单精度数
  • longValue() 将BigDecimal对象中的值转换成长整数
  • intValue() 将BigDecimal对象中的值转换成整数
  • compareTo
    比较大小

BigDecimal格式化

由于NumberFormat类的format()方法可以使用BigDecimal对象作为其参数,可以利用BigDecimal对超出16位有效数字的货币值,百分值,以及一般数值进行格式化控制。

以利用BigDecimal对货币和百分比格式化为例。首先,创建BigDecimal对象,进行BigDecimal的算术运算后,分别建立对货币和百分比格式化的引用,最后利用BigDecimal对象作为format()方法的参数,输出其格式化的货币值和百分比。

DecimalFormat df1 = new DecimalFormat("#,###.000");
System.out.println(df1.format(1234.527));//1,234.527
DecimalFormat df2 = new DecimalFormat("#.00");
System.out.println(df2.format(1234.527));//1234.53,有四舍五入
DecimalFormat df3 = new DecimalFormat("##.00%");    //##.00%   百分比格式,后面不足2位的用0补齐
System.out.println(df3.format(0.527));//52.70%
NumberFormat nf = NumberFormat.getPercentInstance();
nf.setMinimumFractionDigits(3);    // 保留到小数点后几位
System.out.println(nf.format(0.527));//52.700%
System.out.println(String.format("%02d", 5));//05
System.out.println(String.format("%.3f", 3.14159));//3.142,%. 表示小数点前任意位数, 2 表示两位小数, f 表示浮点型
NumberFormat format = NumberFormat.getNumberInstance();
format.setMinimumFractionDigits(3);//设置小数部分允许的最小位数
format.setMaximumFractionDigits(5);//设置小数部分允许的最大位数
format.setMaximumIntegerDigits(10);//设置整数部分允许的最大位数。
format.setMinimumIntegerDigits(0);//设置整数部分允许的最小位数
System.out.println(format.format(2132323213.23266666666)); 2,132,323,213.23267

一些BigDecimal的坑

除了我们上面说的精度缺失和构造方法的坑之后,还有就是除法的一个坑

divide 当我们使用这个方法的时候,一个要设置保留的几位小数,不然会如果出现无限循环的小数,就会报错ArithmeticException异常

BigDecimal a = new BigDecimal("1.0"); 
BigDecimal b = new BigDecimal("3.0"); 
a.divide(b);

还有一个我们上面的格式化的坑,就是我们把字符转换成功科学计数法

BigDecimal a = BigDecimal.valueOf(1234567895255456719.987654321); System.out.println(a.toString());

我们来看BigDecimal变成string的三个方法

  • toPlainString():不使用任何科学计数法;
  • toString():在必要的时候使用科学计数法;
  • toEngineeringString() :在必要的时候使用工程计数法。类似于科学计数法,只不过指数的幂都是3的倍数,这样方便工程上的应用,因为在很多单位转换的时候都是10^3;

BigDecimal实现原理分析

常用几个重要的属性

// 若BigDecimal的绝对值小于Long.MAX_VALUE,放在这个变量中
//public static final long MIN_VALUE = 0x8000000000000000L;
private final transient long intCompact;
//BigDecimal的标度(小数点),
//输入数除以10的scale次幂(32 位的整数标度)
private final int scale;
// BigDecimal的未scale的值,BigInteger是
// 一个任意长度的整数(整数非标度值)
private final BigInteger intVal;
// BigDecimal的精度(精度是非标度值的数字个数)
private transient int precision;
//toString后缓存
private transient String stringCache;

BigDecimal是没有无参构造方法的,所以这里从上面案例中的第一种创建方式使用的构造方法开始:

public BigDecimal(String val) {
//字符串转换成char数组,offset设置为0
  this(val.toCharArray(), 0, val.length());
}

看得出来,这里没做什么,然后直接调用了重载的构造方法:

 public BigDecimal(char[] in, int offset, int len) {
    //MathContext.UNLIMITED这个在老版本中有使用,新版本没有使用了
    this(in,offset,len,MathContext.UNLIMITED);
 }

继续调用重载的方法:

 /**
     * 将 BigDecimal 的字符数组表示形式转换为 BigDecimal,接受与 
     *  BigDecimal(String) 构造方法相同的字符序列,同时允许指定子数组。 
     * 注意,如果字符数组中已经提供字符的序列,则使用此构造方法要比将
     * char 数组转换为字符串并使用 BigDecimal(String) 构造方法更快。 
     * @param in  作为源字符的 char 数组
     * @param offset  要检查的数组中的第一个字符
     * @param len  要考虑的字符数
     * @param mc  没有使用
     */
 public BigDecimal(char[] in, int offset, int len, MathContext mc) {
        // 防止长度过大。
        if (offset+len > in.length || offset < 0)
            throw new NumberFormatException();
      /*
       * 这是BigDecimal构造函数的主字符串;所有传入字符串都在这里结束;
       * 它使用显式(内联)解析来提高速度,并为非紧凑情况生成最多
       * 一个中间(临时)对象(char[]数组)。
       */
        // 对所有字段值使用局部变量,直到完成
        int prec = 0;   // BigDecimal的数字的长度
        int scl = 0;   // BigDecimal的标度
        long rs = 0;   // intCompact值
        BigInteger rb = null; // BigInteger的值
        // 使用数组边界检查来处理太长、len == 0、错误偏移等等。
        try {
            //符号的处理
            boolean isneg = false; // '+'为false,'-'为true
            if (in[offset] == '-') {// 第一个字符为'-'
                isneg = true;
                offset++;
                len--;
            } else if (in[offset] == '+') {// 第一个字符为'+'
                offset++;
                len--;
            }
            //数字有效部分
            boolean dot = false;//当有“.”时为真。
            int cfirst = offset; //记录integer的起始点
            long exp = 0;  //exponent
            char c; //当前字符
            boolean isCompact = (len <= MAX_COMPACT_DIGITS);
            // 大于18位是BigInteger,创建数组
            char coeff[] = isCompact ? null : new char[len];
            int idx = 0;
            for (; len > 0; offset++, len--) {
                c = in[offset];
                // 有数字,确定c(Unicode 代码点)是否为数字
                if ((c >= '0' && c <= '9') || Character.isDigit(c)) {
                    // 第一个紧化情况,我们不需要保留字符我们可以就地计算值。
                    if (isCompact) { // 非BigInteger数值
                        // 获取使用10进制的字符 c 的数值
                        int digit = Character.digit(c, 10);
                        if (digit == 0) {// 为 0
                            if (prec == 0)
                                prec = 1;
                            else if (rs != 0) {
                                rs *= 10;
                                ++prec;
                            }// 否则,数字为冗余前导零
                        } else { // 非0
                            if (prec != 1 || rs != 0)
                                ++prec; // 如果前面加0,则prec不变
                            rs = rs * 10 + digit;
                        }
                    } else {// the unscaled value可能是一个BigInteger对象。
                        if (c == '0' || Character.digit(c, 10) == 0) {// 为0
                            if (prec == 0) {
                                coeff[idx] = c;
                                prec = 1;
                            } else if (idx != 0) {
                                coeff[idx++] = c;
                                ++prec;
                            } // 否则c一定是多余的前导零
                        } else {
                            if (prec != 1 || idx != 0)
                                ++prec; // 如果前面加0,则prec不变
                            coeff[idx++] = c;
                        }
                    }
                    if (dot)// 如果有小数点
                        ++scl;
                    continue;
                }
                // 当前字符等于小数点
                if (c == '.') {
                    // have dot
                    if (dot)// 存在两个小数点
                        throw new NumberFormatException();
                    dot = true;
                    continue;
                }
                // exponent 预期
                if ((c != 'e') && (c != 'E'))
                    throw new NumberFormatException();
                offset++;
                c = in[offset];
                len--;
                boolean negexp = (c == '-'); // 当前字符是否为'-'
                if (negexp || c == '+') { // 为符号
                    offset++;
                    c = in[offset];
                    len--;
                }
                if (len <= 0)// 没有 exponent 数字
                    throw new NumberFormatException();
                // 跳过exponent中的前导零 
                while (len > 10 && Character.digit(c, 10) == 0) {
                    offset++;
                    c = in[offset];
                    len--;
                }
                if (len > 10) // 太多非零 exponent 数字
                    throw new NumberFormatException();
                // c 现在是 exponent的第一个数字
                for (;; len--) {
                    int v;
                    if (c >= '0' && c <= '9') {
                        v = c - '0';
                    } else {
                        v = Character.digit(c, 10);
                        if (v < 0) // 非数字
                            throw new NumberFormatException();
                    }
                    exp = exp * 10 + v;
                    if (len == 1)
                        break;   // 最终字符
                    offset++;
                    c = in[offset];
                }
                if (negexp)                 // 当前字符为'-',取相反数
                    exp = -exp;
                // 下一个测试需要向后兼容性
                if ((int)exp != exp)         // 溢出
                    throw new NumberFormatException();
                break;
            }
            // 这里没有字符了
            if (prec == 0)              // 没有发现数字
                throw new NumberFormatException();
            // 如果exp不为零,调整标度。
            if (exp != 0) {                 // 有显著的exponent
                // 不能调用基于正确的字段值的checkScale
                long adjustedScale = scl - exp;
                if (adjustedScale > Integer.MAX_VALUE ||
                        adjustedScale < Integer.MIN_VALUE)
                    throw new NumberFormatException("Scale out of range.");
                scl = (int)adjustedScale;
            }
            // 从precision中删除前导零(数字计数)
            if (isCompact) {
                rs = isneg ? -rs : rs;
            } else {
                char quick[];
                if (!isneg) {
                    quick = (coeff.length != prec) ?
                            Arrays.copyOf(coeff, prec) : coeff;
                } else {
                    quick = new char[prec + 1];
                    quick[0] = '-';
                    System.arraycopy(coeff, 0, quick, 1, prec);
                }
                rb = new BigInteger(quick);
                // 获取rb(BigInteger)的compact值。
                rs = compactValFor(rb);
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            throw new NumberFormatException();
        } catch (NegativeArraySizeException e) {
            throw new NumberFormatException();
        }
        this.scale = scl;
        this.precision = prec;
        this.intCompact = rs;
        this.intVal = (rs != INFLATED) ? null : rb;
    }

结束

好了,我们今天对BigDecimal的分析就这些吧,我是小六六,三天打鱼,两天晒网!

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

相关文章
|
6月前
|
Java
【Java】如果一个集合中类型是String如何使用拉姆达表达式 进行Bigdecimal类型计算?
【Java】如果一个集合中类型是String如何使用拉姆达表达式 进行Bigdecimal类型计算?
114 0
|
6月前
|
存储 Java
你知道Java中的BigInteger类和BigDecimal类吗?
你知道Java中的BigInteger类和BigDecimal类吗?
|
3月前
|
Java
【Java】Math、System、RunTime、BigDecimal类常用方法
【Java】Math、System、RunTime、BigDecimal类常用方法
|
24天前
|
安全 Java
java BigDecimal 的赋值一个常量
在 Java 中,`BigDecimal` 是一个用于精确计算的类,特别适合处理需要高精度和小数点运算的场景。如果你需要给 `BigDecimal` 赋值一个常量,可以使用其静态方法 `valueOf` 或者直接通过字符串构造函数。 以下是几种常见的方法来给 `BigDecimal` 赋值一个常量: ### 使用 `BigDecimal.valueOf` 这是推荐的方式,因为它可以避免潜在的精度问题。 ```java import java.math.BigDecimal; public class BigDecimalExample { public static void
|
3月前
|
安全 Java
12 Java常用类(二)(String类+时间类+BigDecimal类等等)
12 Java常用类(二)(String类+时间类+BigDecimal类等等)
38 2
|
5月前
|
Java
深入了解Java中的BigDecimal类及其方法
深入了解Java中的BigDecimal类及其方法
|
5月前
|
安全 Java
java中BigDecimal详解
java中BigDecimal详解
|
6月前
|
算法 Java API
java BigDecimal使用详细介绍
java BigDecimal使用详细介绍
java BigDecimal使用详细介绍
|
6月前
|
存储 Java
Java中BigDecimal怎样取反
在上述示例中,`number.negate()`会将BigDecimal对象 `number`的值取反,并将结果存储在新的BigDecimal对象 `negated`中。
132 0
|
6月前
|
Java 数据库
Java中BigDecimal比较大小的方法BigDecimal转换为Integer
Java中BigDecimal比较大小的方法BigDecimal转换为Integer
87 0