问题记录:计算一个回收率的值时抛出异常 java.lang.ArithmeticException: Non-terminating decimal expansion; no exact represe
测试时,前几次都没有问题,但在某一次测试中,改变了一个可变的数值时,抛出异常。
问题定位:调用 BigDecimal 的 divide 方法时抛出异常,调用的方法是重载方法中,只有一个参数的 divide(BigDecimal divisor);
问题分析:
1.异常信息翻译:非终止十进制扩展;没有精确的可表示的十进制结果。
2.测试结果分析:前几次测试,divide的结果刚好是已除尽的数据,即非无限循环小数;异常的测试,结果是除不尽的小数(无限循环小数),即异常所表示的没有精确的可表示的十进制结果。
3.方法参数解析:divide(BigDecimal divisor, int scale, int roudingMode);
(1)divisor,分母
(2)scale,精确到小数点后几位;
divide(divisor, roudingMode)时,若没有提前设置调用者的scale了,scale默认为0,即取整
(3)roudingMode,约数规则,用BigDecimal的常量表示
(如:BigDecimal.ROUND_HALF_UP:超过一半进一,即四舍五入)
解决方法:用其他两个方法计算商,注意自定义scale和roudingMode,之前是先计算后设置scale导致的问题,应在发生无限循环小数前处理。
深入分析处理:
1.第二日想了一想,divide三个方法最终调用的底层方法是相同的,原则不变的还是参数的处理;
2.且昨日分析到抛出异常的地方并不是真正抛出异常的地方;
直观异常处
发现这里调用了另一个divide的双参方法,且参数MathContext 上面的注释说明:
/*
* If the quotient this/divisor has a terminating decimal
* expansion, the expansion can have no more than
* (a.precision() + ceil(10*b.precision)/3) digits.
* Therefore, create a MathContext object with this
* precision and do a divide with the UNNECESSARY rounding
* mode.
*/
如果商this/除数有一个终止的十进制扩展,
则扩展的位数不能超过(a.precision()+ceil(10*b.precision)/3)位数。
因此,创建一个具有此精度的MathContext对象,并使用不必要的舍入模式进行除法。
这里注意两个地方:(1)有终止的十进制扩展-可以除尽;(2)UNNECESSARY不必要的舍入模式;
(1)若是不可除尽的数,怎么办;
(2)ROUND_UNNECESSARY:舍入模式用于断言请求的操作具有精确的结果,因此不需要舍入。如果在生成不精确结果的操作上指定了此舍入模式,则抛出{@code arithmetricationexception}。
在第二点上是否就看出来了,然后我就一直 breakpoint 跟下去,基本都是对参数的各种判定处理,尽量返回一个精确的数据,最后到达抛出异常的地方
完全对应(2),ROUND_UNNECESSARY 模式用于操作具有不精确结果时,将直接抛出异常。
当然我们实际处理中,很难用到 ROUND_UNNECESSARY 模式,除非用于一些特殊结果的判定。
另外,divide(BigDecimal divisor, int roudingMode); 和 divide(BigDecimal divisor); 最终抛出的异常信息也不一样;
原因:divide(BigDecimal divisor);在初始调用时,进行了异常捕获处理,并抛出了一个新的 ArithmeticException
测试:
public static void main(String[] args) {
BigDecimal numerator = new BigDecimal(1);
BigDecimal denominator = new BigDecimal(3);
BigDecimal divide = numerator.divide(denominator, BigDecimal.ROUND_HALF_UP);
System.out.println(divide);
try {
divide = numerator.divide(denominator);
System.out.println(divide);
}catch (Exception e) {
System.out.println("divide(one)");
e.printStackTrace();
}
try {
divide = numerator.divide(denominator, BigDecimal.ROUND_UNNECESSARY);
System.out.println(divide);
}catch (Exception e) {
System.out.println("divide(two, UNNECESSARY)");
e.printStackTrace();
}
}
下面贴出相关源码:
divide(BigDecimal divisor):有捕获异常处理
/**
* Returns a {@code BigDecimal} whose value is {@code (this /
* divisor)}, and whose preferred scale is {@code (this.scale() -
* divisor.scale())}; if the exact quotient cannot be
* represented (because it has a non-terminating decimal
* expansion) an {@code ArithmeticException} is thrown.
*
* @param divisor value by which this {@code BigDecimal} is to be divided.
* @throws ArithmeticException if the exact quotient does not have a
* terminating decimal expansion
* @return {@code this / divisor}
* @since 1.5
* @author Joseph D. Darcy
*/
public BigDecimal divide(BigDecimal divisor) {
/*
* Handle zero cases first.
*/
if (divisor.signum() == 0) { // x/0
if (this.signum() == 0) // 0/0
throw new ArithmeticException("Division undefined"); // NaN
throw new ArithmeticException("Division by zero");
}
// Calculate preferred scale
int preferredScale = saturateLong((long) this.scale - divisor.scale);
if (this.signum() == 0) // 0/y
return zeroValueOf(preferredScale);
else {
/*
* If the quotient this/divisor has a terminating decimal
* expansion, the expansion can have no more than
* (a.precision() + ceil(10*b.precision)/3) digits.
* Therefore, create a MathContext object with this
* precision and do a divide with the UNNECESSARY rounding
* mode.
*/
MathContext mc = new MathContext( (int)Math.min(this.precision() +
(long)Math.ceil(10.0*divisor.precision()/3.0),
Integer.MAX_VALUE),
RoundingMode.UNNECESSARY);
BigDecimal quotient;
try {
quotient = this.divide(divisor, mc);
} catch (ArithmeticException e) {
throw new ArithmeticException("Non-terminating decimal expansion; " +
"no exact representable decimal result.");
}
int quotientScale = quotient.scale();
// divide(BigDecimal, mc) tries to adjust the quotient to
// the desired one by removing trailing zeros; since the
// exact divide method does not have an explicit digit
// limit, we can add zeros too.
if (preferredScale > quotientScale)
return quotient.setScale(preferredScale, ROUND_UNNECESSARY);
return quotient;
}
}
divide(BigDecimal divisor, int roundMode):未捕获异常再次处理
/**
* Returns a {@code BigDecimal} whose value is {@code (this /
* divisor)}, and whose scale is {@code this.scale()}. If
* rounding must be performed to generate a result with the given
* scale, the specified rounding mode is applied.
*
* The new {@link #divide(BigDecimal, RoundingMode)} method
* should be used in preference to this legacy method.
*
* @param divisor value by which this {@code BigDecimal} is to be divided.
* @param roundingMode rounding mode to apply.
* @return {@code this / divisor}
* @throws ArithmeticException if {@code divisor==0}, or
* {@code roundingMode==ROUND_UNNECESSARY} and
* {@code this.scale()} is insufficient to represent the result
* of the division exactly.
* @throws IllegalArgumentException if {@code roundingMode} does not
* represent a valid rounding mode.
* @see #ROUND_UP
* @see #ROUND_DOWN
* @see #ROUND_CEILING
* @see #ROUND_FLOOR
* @see #ROUND_HALF_UP
* @see #ROUND_HALF_DOWN
* @see #ROUND_HALF_EVEN
* @see #ROUND_UNNECESSARY
*/
public BigDecimal divide(BigDecimal divisor, int roundingMode) {
return this.divide(divisor, scale, roundingMode);
}
public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode) {
if (roundingMode < ROUND_UP || roundingMode > ROUND_UNNECESSARY)
throw new IllegalArgumentException("Invalid rounding mode");
if (this.intCompact != INFLATED) {
if ((divisor.intCompact != INFLATED)) {
return divide(this.intCompact, this.scale, divisor.intCompact, divisor.scale, scale, roundingMode);
} else {
return divide(this.intCompact, this.scale, divisor.intVal, divisor.scale, scale, roundingMode);
}
} else {
if ((divisor.intCompact != INFLATED)) {
return divide(this.intVal, this.scale, divisor.intCompact, divisor.scale, scale, roundingMode);
} else {
return divide(this.intVal, this.scale, divisor.intVal, divisor.scale, scale, roundingMode);
}
}
}
实际抛出异常
/**
* Shared logic of need increment computation.
*/
private static boolean commonNeedIncrement(int roundingMode, int qsign,
int cmpFracHalf, boolean oddQuot) {
switch(roundingMode) {
case ROUND_UNNECESSARY:
throw new ArithmeticException("Rounding necessary");
case ROUND_UP: // Away from zero
return true;
case ROUND_DOWN: // Towards zero
return false;
case ROUND_CEILING: // Towards +infinity
return qsign > 0;
case ROUND_FLOOR: // Towards -infinity
return qsign < 0;
default: // Some kind of half-way rounding
assert roundingMode >= ROUND_HALF_UP &&
roundingMode <= ROUND_HALF_EVEN: "Unexpected rounding mode" + RoundingMode.valueOf(roundingMode);
if (cmpFracHalf < 0 ) // We're closer to higher digit
return false;
else if (cmpFracHalf > 0 ) // We're closer to lower digit
return true;
else { // half-way
assert cmpFracHalf == 0;
switch(roundingMode) {
case ROUND_HALF_DOWN:
return false;
case ROUND_HALF_UP:
return true;
case ROUND_HALF_EVEN:
return oddQuot;
default:
throw new AssertionError("Unexpected rounding mode" + roundingMode);
}
}
}
}