https://javaguide.cn/java/basis/bigdecimal.html
BigDecimal详解
Java中,浮点数的运算有精度丢失的风险
为什么浮点数运算的时候会有精度丢失的风险? 计算机是二进制的,浮点数在计算机中是通过二进制的方式来表示的。但是,浮点数的表示方式是有限的,所以在进行浮点数运算的时候,会存在精度丢失的风险。
例如,在Java中,浮点数的表示方式是 IEEE 754 标准,使用 64 位二进制来表示一个浮点数。其中,1 位用于表示符号位,11 位用于表示指数位,52 位用于表示尾数位。但是,浮点数的表示方式是有限的,所以在进行浮点数运算的时候,会存在精度丢失的风险。
BigDecimal 类的常用方法
BigDecimal可以实现对小数的运算,不会造成精度损失
通常情况下,大部分需要小数精确运算结果的业务场景都是通过BigDecimal来做的。
《阿里巴巴 Java 开发手册》中提到:浮点数之间的等值判断,基本数据类型不能用 == 来比较,包装数据类型不能用 equals 来判断。
创建
我们在使用BigDecimal的时候,需要注意以下几点:
-
不能使用new BigDecimal(double)的方式来创建BigDecimal对象,因为double类型的精度是有限的,所以在创建BigDecimal对象的时候,会存在精度丢失的风险。
-
可以使用new BigDecimal(String)的方式来创建BigDecimal对象,因为String类型的精度是无限的,所以在创建BigDecimal对象的时候,不会存在精度丢失的风险。
-
可以使用BigDecimal的valueOf()方法来创建BigDecimal对象,因为valueOf()方法的参数是double类型,但是在内部会将double类型的参数转换为String类型,所以在创建BigDecimal对象的时候,不会存在精度丢失的风险。
加减乘除
add subtract multiply divide
divide可以指定保留的小数位数,以及四舍五入的方式。
public BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode) { return divide(divisor, scale, roundingMode.oldMode);}
我们使用 divide 方法的时候尽量使用 3 个参数版本,并且RoundingMode 不要选择 UNNECESSARY,否则很可能会遇到 ArithmeticException(无法除尽出现无限循环小数的时候),其中 scale 表示要保留几位小数,roundingMode 代表保留规则。
scale是保留几位小数,roundingMode是保留规则。
roundingMode:
- UP 向上四舍五入
- DOWN 向下截取
- CEILING 向上截取
- FLOOR 向下截取
- HALF_UP 四舍五入
- HALF_DOWN 五舍六入
- HALF_EVEN 四舍六入五取偶
RoundingMode枚举详解 📚📊
各种舍入模式详细说明
1. UP - 向上舍入 ⬆️
// 绝对值增大方向舍入,远离零的方向2.4 -> 3 // 正数向上1.6 -> 2 // 正数向上-1.6 -> -2 // 负数向更小(绝对值更大)-2.4 -> -3 // 负数向更小
2. DOWN - 向下舍入 ⬇️
// 绝对值减小方向舍入,趋向零的方向2.4 -> 2 // 正数向下1.6 -> 1 // 正数向下-1.6 -> -1 // 负数向更大(绝对值更小)-2.4 -> -2 // 负数向更大
3. CEILING - 向正无穷舍入 ☁️
// 向数轴右侧舍入2.4 -> 3 // 正数向上1.6 -> 2 // 正数向上-1.6 -> -1 // 负数向更大(向右)-2.4 -> -2 // 负数向更大
4. FLOOR - 向负无穷舍入 ⚡
// 向数轴左侧舍入2.4 -> 2 // 正数向下1.6 -> 1 // 正数向下-1.6 -> -2 // 负数向更小(向左)-2.4 -> -3 // 负数向更小
5. HALF_UP - 四舍五入 🎯
// 遇5向上舍入2.4 -> 2 // 小于5,向下2.5 -> 3 // 等于5,向上2.6 -> 3 // 大于5,向上-1.5 -> -2 // 负数也一样,-1.5 -> -2-1.4 -> -1 // -1.4 -> -1
实际代码示例 💡
import java.math.BigDecimal;import java.math.RoundingMode;
public class RoundingModeDemo { public static void main(String[] args) { BigDecimal[] testNumbers = { new BigDecimal("2.4"), new BigDecimal("2.5"), new BigDecimal("2.6"), new BigDecimal("-1.4"), new BigDecimal("-1.5"), new BigDecimal("-1.6") };
for (BigDecimal num : testNumbers) { System.out.println("\n原数: " + num); System.out.println("UP: " + num.setScale(0, RoundingMode.UP)); System.out.println("DOWN: " + num.setScale(0, RoundingMode.DOWN)); System.out.println("CEILING: " + num.setScale(0, RoundingMode.CEILING)); System.out.println("FLOOR: " + num.setScale(0, RoundingMode.FLOOR)); System.out.println("HALF_UP: " + num.setScale(0, RoundingMode.HALF_UP)); } }}
输出结果展示 📊
原数: 2.4UP: 3DOWN: 2CEILING: 3FLOOR: 2HALF_UP: 2
原数: 2.5UP: 3DOWN: 2CEILING: 3FLOOR: 2HALF_UP: 3
原数: -1.5UP: -2DOWN: -1CEILING: -1FLOOR: -2HALF_UP: -2
使用场景建议 🎯
public class RoundingMode应用场景 { public static void main(String[] args) { // 金融计算 - 通常使用HALF_UP(银行家舍入) BigDecimal money = new BigDecimal("123.455"); BigDecimal roundedMoney = money.setScale(2, RoundingMode.HALF_UP);
// 统计计算 - 可能使用HALF_EVEN(银行家舍入) BigDecimal average = new BigDecimal("87.345"); BigDecimal roundedAvg = average.setScale(2, RoundingMode.HALF_EVEN);
// 科学计算 - 根据需要选择合适的模式 BigDecimal scientific = new BigDecimal("99.999"); BigDecimal ceilingResult = scientific.setScale(2, RoundingMode.CEILING); }}
BigDecimal等值比较
使用compareTo进行比较,因为equals会比较值和精度,但是compareTo会忽略精度
compareTo() 方法可以比较两个 BigDecimal 的值,如果相等就返回 0,如果第 1 个数比第 2 个数大则返回 1,反之返回-1。
BigDecimal工具类
import java.math.BigDecimal;import java.math.RoundingMode;
/** * 简化BigDecimal计算的小工具类 */public class BigDecimalUtil {
/** * 默认除法运算精度 */ private static final int DEF_DIV_SCALE = 10;
private BigDecimalUtil() { }
/** * 提供精确的加法运算。 * * @param v1 被加数 * @param v2 加数 * @return 两个参数的和 */ public static double add(double v1, double v2) { BigDecimal b1 = BigDecimal.valueOf(v1); BigDecimal b2 = BigDecimal.valueOf(v2); return b1.add(b2).doubleValue(); }
/** * 提供精确的减法运算。 * * @param v1 被减数 * @param v2 减数 * @return 两个参数的差 */ public static double subtract(double v1, double v2) { BigDecimal b1 = BigDecimal.valueOf(v1); BigDecimal b2 = BigDecimal.valueOf(v2); return b1.subtract(b2).doubleValue(); }
/** * 提供精确的乘法运算。 * * @param v1 被乘数 * @param v2 乘数 * @return 两个参数的积 */ public static double multiply(double v1, double v2) { BigDecimal b1 = BigDecimal.valueOf(v1); BigDecimal b2 = BigDecimal.valueOf(v2); return b1.multiply(b2).doubleValue(); }
/** * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到 * 小数点以后10位,以后的数字四舍六入五成双。 * * @param v1 被除数 * @param v2 除数 * @return 两个参数的商 */ public static double divide(double v1, double v2) { return divide(v1, v2, DEF_DIV_SCALE); }
/** * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指 * 定精度,以后的数字四舍六入五成双。 * * @param v1 被除数 * @param v2 除数 * @param scale 表示表示需要精确到小数点以后几位。 * @return 两个参数的商 */ public static double divide(double v1, double v2, int scale) { if (scale < 0) { throw new IllegalArgumentException( "The scale must be a positive integer or zero"); } BigDecimal b1 = BigDecimal.valueOf(v1); BigDecimal b2 = BigDecimal.valueOf(v2); return b1.divide(b2, scale, RoundingMode.HALF_EVEN).doubleValue(); }
/** * 提供精确的小数位四舍六入五成双处理。 * * @param v 需要四舍六入五成双的数字 * @param scale 小数点后保留几位 * @return 四舍六入五成双后的结果 */ public static double round(double v, int scale) { if (scale < 0) { throw new IllegalArgumentException( "The scale must be a positive integer or zero"); } BigDecimal b = BigDecimal.valueOf(v); BigDecimal one = new BigDecimal("1"); return b.divide(one, scale, RoundingMode.HALF_UP).doubleValue(); }
/** * 提供精确的类型转换(Float) * * @param v 需要被转换的数字 * @return 返回转换结果 */ public static float convertToFloat(double v) { BigDecimal b = new BigDecimal(v); return b.floatValue(); }
/** * 提供精确的类型转换(Int)不进行四舍六入五成双 * * @param v 需要被转换的数字 * @return 返回转换结果 */ public static int convertsToInt(double v) { BigDecimal b = new BigDecimal(v); return b.intValue(); }
/** * 提供精确的类型转换(Long) * * @param v 需要被转换的数字 * @return 返回转换结果 */ public static long convertsToLong(double v) { BigDecimal b = new BigDecimal(v); return b.longValue(); }
/** * 返回两个数中大的一个值 * * @param v1 需要被对比的第一个数 * @param v2 需要被对比的第二个数 * @return 返回两个数中大的一个值 */ public static double returnMax(double v1, double v2) { BigDecimal b1 = new BigDecimal(v1); BigDecimal b2 = new BigDecimal(v2); return b1.max(b2).doubleValue(); }
/** * 返回两个数中小的一个值 * * @param v1 需要被对比的第一个数 * @param v2 需要被对比的第二个数 * @return 返回两个数中小的一个值 */ public static double returnMin(double v1, double v2) { BigDecimal b1 = new BigDecimal(v1); BigDecimal b2 = new BigDecimal(v2); return b1.min(b2).doubleValue(); }
/** * 精确对比两个数字 * * @param v1 需要被对比的第一个数 * @param v2 需要被对比的第二个数 * @return 如果两个数一样则返回0,如果第一个数比第二个数大则返回1,反之返回-1 */ public static int compareTo(double v1, double v2) { BigDecimal b1 = BigDecimal.valueOf(v1); BigDecimal b2 = BigDecimal.valueOf(v2); return b1.compareTo(b2); }
}