目前任何一种编程语言在进行浮点数运算时可能出现失精度问题。
比如 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就没有添加和修改的函数。