分享 第24周 Java解决精度问题

1,010 阅读6分钟

目前任何一种编程语言在进行浮点数运算时可能出现失精度问题。

比如 2.0 - 0.1 。正常结果肯定是1.9,但是程序中结果可能是1.899999999... 得出一个无限接近结果的循环小数。

该问题出现还是计算机底层二进制存储浮点数问题。感兴趣可以自行去查询资料。

Java语言角度解决精度问题主要有以下三种方式:

1、整数计算

这种方式主要是按照系统精度需求将所有浮点数编程整数运算。(该方式一般不适用金融或精度较高的系统)

例如xx系统要求所有数据最高保留4位小数,目前需要计算100/3

int a = (int)(100 * 1000 / 3); 
//需要保留4位小数则将小数位全部挪到整数位,即乘以1000,然后直接转化为int即可

该种方法使用比较少,不太建议使用

2、四舍五入

此种方法为目前很多种语言常用的方式,对进行结算结果按照精度要求四舍五入即可以得出准确答案。因为失精度得出来结果也是无限接近正确值,所有四舍五入就等于目标值。

注意事项:

1、如果计算过程涉及计算过程比较长,涉及金额较大和要求精度比较高。建议中间过程就需要进行四舍五入,否则后面误差会越来越大。

2、Java浮点数不能直接进行比较,及时他们打印出来的值一模一样。进行代码判断是否等于都可能出现不想等可能性。

3、BigDecimal对象

Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算。该对象不能直接使用+、-、*、/等运算符号进行运算。必须调用该对象函数进行运算。(Bigdemicalh运算过程中会消耗较大性能,性能不如四舍五入)

BigDecimal提供很多个构造函数如下

    BigDecimal(BigInteger, long, int, int)
    BigDecimal(char[], int, int)
    BigDecimal(char[], int, int, MathContext)
    BigDecimal(char[])
    BigDecimal(char[], MathContext)
    BigDecimal(String)
    BigDecimal(String, MathContext)
    BigDecimal(double)
    BigDecimal(double, MathContext)
    BigDecimal(BigInteger)
    BigDecimal(BigInteger, MathContext)
    BigDecimal(BigInteger, int)
    BigDecimal(BigInteger, int, MathContext)
    BigDecimal(int)
    BigDecimal(int, MathContext)
    BigDecimal(long)
    BigDecimal(long, MathContext)

一般我们都是使用BigDecimal(String)、BigDecimal(int)、BigDecimal(long)之类。

注意:不推荐大家使用 BigDecimal(double)函数。因为本身double存储对象已经存在失精度可能性,所有这样会也会导致BigDecimal的存的数值存在偏差。

我们通过源代码对该函数的说明

     * The results of this constructor can be somewhat unpredictable.
     * One might assume that writing {@code new BigDecimal(0.1)} in
     * Java creates a {@code BigDecimal} which is exactly equal to
     * 0.1 (an unscaled value of 1, with a scale of 1), but it is
     * actually equal to
     * 0.1000000000000000055511151231257827021181583404541015625.
     * This is because 0.1 cannot be represented exactly as a
     * {@code double} (or, for that matter, as a binary fraction of
     * any finite length).  Thus, the value that is being passed
     * <i>in</i> to the constructor is not exactly equal to 0.1,
     * appearances notwithstanding.
     
     该构造函数的结果存在不确定性。
     假设在代码中写入{@code new BigDecimal(0.1)}
     Java会创建一个BigDecimal对象,它完全等于0.1(非标值1,精度为1)
     但是实际上它等于0.1000000000000000055511151231257827021181583404541015625。
     这是应为0.1无法精确表示为double类型(或则说任何有限的长度)
     因为说明值被传进入了,但是实际不等于0.1
     
     * <li>
     * The {@code String} constructor, on the other hand, is
     * perfectly predictable: writing {@code new BigDecimal("0.1")}
     * creates a {@code BigDecimal} which is <i>exactly</i> equal to
     * 0.1, as one would expect.  Therefore, it is generally
     * recommended that the {@linkplain #BigDecimal(String)
     * <tt>String</tt> constructor} be used in preference to this one.
     *
     使用字符为入参数的构造函数是可以预测的
     代码中写入编写{@code new BigDecimal(“ 0.1”)}
     会创建一个BigDecimal对象,完全等于0.1。
     所有建议优先使用BigDecimal(String)
     
     * <li>
     * When a {@code double} must be used as a source for a
     * {@code BigDecimal}, note that this constructor provides an
     * exact conversion; it does not give the same result as
     * converting the {@code double} to a {@code String} using the
     * {@link Double#toString(double)} method and then using the
     * {@link #BigDecimal(String)} constructor.  To get that result,
     * use the {@code static} {@link #valueOf(double)} method.
     * </ol>
     
     当必须使用double类型创建BigDecimal对象。
     请注意,此构造函数提供了一个精确转换;
     或使用以下方式
     使用Double的toString(double)将double类型转化为String,再BigDecimal(String)
     valueOf(double)也可以转化为String

通过源码的介绍得知道我们还是谨慎使用BigDecimal(double)函数。

然后运算过程中就如下

        BigDecimal a = new BigDecimal("100");
        BigDecimal b = new BigDecimal("3");

        //加法
        BigDecimal c = a.add(b);
        //减法
        c = a.subtract(b);
        //乘法
        c = a.divide(b);
        //除法
        c = a.multiply(b);
        //保留2位小数,然后四舍五入
        //其他保留小数方式可以直接查询源码可知
        c = c.setScale(2,BigDecimal.ROUND_HALF_UP);

注意:

使用BigDecimal计算过程中,要使用返回值作为计算结果。以加法为例子


    public BigDecimal add(BigDecimal augend) {
        if (this.intCompact != INFLATED) {
            if ((augend.intCompact != INFLATED)) {
                return add(this.intCompact, this.scale, augend.intCompact, augend.scale);
            } else {
                return add(this.intCompact, this.scale, augend.intVal, augend.scale);
            }
        } else {
            if ((augend.intCompact != INFLATED)) {
                return add(augend.intCompact, augend.scale, this.intVal, this.scale);
            } else {
                return add(this.intVal, this.scale, augend.intVal, augend.scale);
            }
        }
    }
    
 private static BigDecimal add(final long xs, int scale1, final long ys, int scale2) {
        long sdiff = (long) scale1 - scale2;
        if (sdiff == 0) {
            return add(xs, ys, scale1);
        } else if (sdiff < 0) {
            int raise = checkScale(xs,-sdiff);
            long scaledX = longMultiplyPowerTen(xs, raise);
            if (scaledX != INFLATED) {
                return add(scaledX, ys, scale2);
            } else {
                BigInteger bigsum = bigMultiplyPowerTen(xs,raise).add(ys);
                return ((xs^ys)>=0) ? // same sign test
                    new BigDecimal(bigsum, INFLATED, scale2, 0)
                    : valueOf(bigsum, scale2, 0);
            }
        } else {
            int raise = checkScale(ys,sdiff);
            long scaledY = longMultiplyPowerTen(ys, raise);
            if (scaledY != INFLATED) {
                return add(xs, scaledY, scale1);
            } else {
                BigInteger bigsum = bigMultiplyPowerTen(ys,raise).add(xs);
                return ((xs^ys)>=0) ?
                    new BigDecimal(bigsum, INFLATED, scale1, 0)
                    : valueOf(bigsum, scale1, 0);
            }
        }
    }

最后都是返回一个新的BigDecimal对象,对原来BigDecimal无改变。(setScale函数处理小数位同样是这样的逻辑)

使用BigDecimal进行大小比较

//通过结果判断大小
//-1 a小于b
//0 a等于b
//1 a大于b
a.compareTo(b);

注意:如果需要跟指定常量做比较,例如0,可以使用BigDecimal.ZERO进行比较,这样不需要充分创建0对象。(其实还有1到10的常量可以使用)

小知识:其实在Java设计中还存在很多例如BigDecimal.ZERO的情况。例如

  //主要用于返回一个空集合,方便调用判断逻辑。而不是直接返回一个null
  Collections.EMPTY_SET;
  Collections.EMPTY_MAP;
  Collections.EMPTY_LIST;

查看源码

public static final List EMPTY_LIST = new EmptyList<>();

虽然从代码上面看到EMPTY_LIST是不可修改,但是只是说明EMPTY_LIST所指向对象地址不可以变。对象还是可能存在修改内容可能性。所以使用EmptyList对象。

private static class EmptyList<E>
        extends AbstractList<E>
        implements RandomAccess, Serializable {
        private static final long serialVersionUID = 8842843931221139166L;

        public Iterator<E> iterator() {
            return emptyIterator();
        }
        public ListIterator<E> listIterator() {
            return emptyListIterator();
        }

        public int size() {return 0;}
        public boolean isEmpty() {return true;}

        public boolean contains(Object obj) {return false;}
        public boolean containsAll(Collection<?> c) { return c.isEmpty(); }

        public Object[] toArray() { return new Object[0]; }

        public <T> T[] toArray(T[] a) {
            if (a.length > 0)
                a[0] = null;
            return a;
        }

        public E get(int index) {
            throw new IndexOutOfBoundsException("Index: "+index);
        }

        public boolean equals(Object o) {
            return (o instanceof List) && ((List<?>)o).isEmpty();
        }

        public int hashCode() { return 1; }

        @Override
        public boolean removeIf(Predicate<? super E> filter) {
            Objects.requireNonNull(filter);
            return false;
        }
        @Override
        public void replaceAll(UnaryOperator<E> operator) {
            Objects.requireNonNull(operator);
        }
        @Override
        public void sort(Comparator<? super E> c) {
        }

        // Override default methods in Collection
        @Override
        public void forEach(Consumer<? super E> action) {
            Objects.requireNonNull(action);
        }

        @Override
        public Spliterator<E> spliterator() { return Spliterators.emptySpliterator(); }

        // Preserves singleton property
        private Object readResolve() {
            return EMPTY_LIST;
        }
    }

查看源码可以知道EmptyList就没有添加和修改的函数。