侧边栏壁纸
博主头像
phoenix-blog 博主等级

悄咪咪的,怕发现

  • 累计撰写 13 篇文章
  • 累计创建 23 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

Java中BidDecimal类的使用

phoenix
2024-09-22 / 0 评论 / 0 点赞 / 23 阅读 / 0 字 / 正在检测是否收录...

1. BigDecimal概述

Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数,但在实际应用中,可能需要对更大或者更小的数进行运算和处理。

一般情况下,对于那些不需要准确计算精度的数字可以直接使用FloatDouble处理,但是Double.valueOf(String)Float.valueOf(String)会丢失精度。所以开发中,如果需要精确计算的结果,则必须使用BigDecimal类来操作。

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

2. BigDecimal常用构造函数

2.1. 常用构造函数

- `BigDecimal(int)`:创建一个具有参数所指定整数值的对象
- `BigDecimal(double)`:创建一个具有参数所指定双精度值的对象
- `BigDecimal(long)`:创建一个具有参数所指定长整数值的对象
- `BigDecimal(String)`:创建一个具有参数所指定以字符串表示的数值的对象(比较常用)

2.2. 使用问题分析

BigDecimal a =new BigDecimal(0.1);
System.out.println("a values is:"+a);
System.out.println("=====================");
BigDecimal b =new BigDecimal("0.1");
System.out.println("b values is:"+b);
结果示例:
a values is:0.1000000000000000055511151231257827021181583404541015625
=====================
b values is:0.1
原因分析:

1. 参数类型为double的构造方法的结果有一定的不可预知性。有人可能认为在Java中写入`new BigDecimal(0.1)`所创建的`BigDecimal`正好等于 0.1(非标度值 1,其标度为 1),但是它实际上等于0.1000000000000000055511151231257827021181583404541015625。这是因为0.1无法准确地表示为 double(或者说对于该情况,不能表示为任何有限长度的二进制小数)。这样,传入到构造方法的值不会正好等于 0.1(虽然表面上等于该值)。
2. String 构造方法是完全可预知的:写入 `new BigDecimal("0.1")` 将创建一个 `BigDecimal`,它正好等于预期的 0.1。因此,比较而言, 通常建议优先使用String构造方法。
3. 当double必须用作`BigDecimal`的源时,请注意,此构造方法提供了一个准确转换;它不提供与以下操作相同的结果:先使用`Double.toString(double)`方法,然后使用`BigDecimal(String)`构造方法,将double转换为String。要获取该结果,请使用`static valueOf(double)`方法。

其实在点击到这个方法看源码的时候,注释都提醒慎重使用了。
* 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.
也就是说存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常

3. BigDecimal常用方法详解

3.1. 常用方法

- `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`对象中的值转换成整数

3.2. BigDecimal大小比较

Java中对`BigDecimal`比较大小一般用的是bigdemical的`compareTo`方法
int a = bigdemical.compareTo(bigdemical2)
返回结果分析:
a = -1,表示bigdemical小于bigdemical2;
a = 0,表示bigdemical等于bigdemical2;
a = 1,表示bigdemical大于bigdemical2;
举例:a大于等于b
new bigdemica(a).compareTo(new bigdemical(b)) >= 0

3.3. BigDecimal保留小数点问题

`ROUND_DOWN`:向零方向舍入

`ROUND_UP`:向远离0的方向舍入

`ROUND_CEILING`:向正无穷方向舍入

`ROUND_FLOOR`:向负无穷方向舍入

`ROUND_HALF_DOWN`:相当于五舍六入

`ROUND_HALF_UP`:相当于四舍五入(**经常使用**)

4. BigDecimal格式化

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

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

NumberFormat currency = NumberFormat.getCurrencyInstance(); //建立货币格式化引用 
NumberFormat percent = NumberFormat.getPercentInstance();  //建立百分比格式化引用 
percent.setMaximumFractionDigits(3); //百分比小数点最多3位 
BigDecimal loanAmount = new BigDecimal("15000.48"); //贷款金额
BigDecimal interestRate = new BigDecimal("0.008"); //利率   
BigDecimal interest = loanAmount.multiply(interestRate); //相乘
System.out.println("贷款金额:\t" + currency.format(loanAmount)); 
System.out.println("利率:\t" + percent.format(interestRate)); 
System.out.println("利息:\t" + currency.format(interest));

结果:

贷款金额: ¥15,000.48 利率: 0.8% 利息: ¥120.00

BigDecimal格式化保留2为小数,不足则补0:

public class NumberFormat {
    public static void main(String[] s){
        System.out.println(formatToNumber(new BigDecimal("3.435")));
        System.out.println(formatToNumber(new BigDecimal(0)));
        System.out.println(formatToNumber(new BigDecimal("0.00")));
        System.out.println(formatToNumber(new BigDecimal("0.001")));
        System.out.println(formatToNumber(new BigDecimal("0.006")));
        System.out.println(formatToNumber(new BigDecimal("0.206")));
    }
    /**
     * @desc 1.0~1之间的BigDecimal小数,格式化后失去前面的0,则前面直接加上0。
     * 2.传入的参数等于0,则直接返回字符串"0.00"
     * 3.大于1的小数,直接格式化返回字符串
     * @param obj传入的小数
     * @return
     */
    public static String formatBigDecimalToStr(BigDecimal obj) {
        DecimalFormat df = new DecimalFormat("#.00");
        if(obj.compareTo(BigDecimal.ZERO)==0) {
            return "0.00";
        }else if(obj.compareTo(BigDecimal.ZERO)>0&&obj.compareTo(new BigDecimal(1))<0){
            return "0"+df.format(obj).toString();
        }else {
            return df.format(obj).toString();
        }
    }
  
    // 简化版
    public static String formatBigDecimalToStr(BigDecimal premium) {
        final DecimalFormat decimalFormat = new DecimalFormat("#0.00");
        return decimalFormat.format(premium);
    }
  
}

结果为:

3.44
0.00
0.00
0.00
0.01
0.21

这个模式中 #,##0 表示使用千位分隔符,并保留整数部分,.00 表示保留两位小数。

DecimalFormat 中,格式化模式由一系列的格式字符组成,用于指定如何显示数字。以下是一些常用的格式字符及其含义:

  • 0:表示数字,如果该位不存在则用 0 补齐。
  • #:表示数字,如果该位不存在则不显示。
  • ,:表示千位分隔符。
  • .:表示小数点。
  • %:表示乘以 100 并显示为百分比。
  • E:表示科学计数法。

这些格式字符可以根据需求自由组合,例如 "#,##0.00" 表示使用千位分隔符,保留两位小数的数字格式。

5. BigDecimal常见异常

5.1. 除法的时候出现异常

`java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result`

原因分析:

通过`BigDecimal`的`divide`方法进行除法时当不整除,出现无限循环小数时,就会抛异常:
java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
解决方法:

`divide`方法设置精确的小数点,如:`divide(xxxxx,2)`

6. BigDecimal总结

6.1. 总结

在需要精确的小数计算时再使用`BigDecimal`,`BigDecimal`的性能比`double`和`float`差,在处理庞大,复杂的运算时尤为明显。故一般精度的计算没必要使用`BigDecimal`。

尽量使用参数类型为String的构造函数。

`BigDecimal`都是不可变的(immutable)的, 在进行每一次四则运算时,都会产生一个新的对象 ,所以在做加减乘除运算时要记得要保存操作后的值。

6.2. 工具类推荐

package com.vivo.ars.util;
import java.math.BigDecimal;
/**
 * 用于高精确处理常用的数学运算
 */
public class ArithmeticUtils {
    // 默认除法运算精度
    private static final int DEF_DIV_SCALE = 10;
    /**
     * 提供精确的加法运算
     *
     * @param v1 被加数
     * @param v2 加数
     * @return 两个参数的和
     */
    public static double add(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.add(b2).doubleValue();
    }
    /**
     * 提供精确的加法运算
     *
     * @param v1 被加数
     * @param v2 加数
     * @return 两个参数的和
     */
    public static BigDecimal add(String v1, String v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.add(b2);
    }
    /**
     * 提供精确的加法运算
     *
     * @param v1    被加数
     * @param v2    加数
     * @param scale 保留scale 位小数
     * @return 两个参数的和
     */
    public static String add(String v1, String v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.add(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
    }
    /**
     * 提供精确的减法运算
     *
     * @param v1 被减数
     * @param v2 减数
     * @return 两个参数的差
     */
    public static double sub(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.subtract(b2).doubleValue();
    }
    /**
     * 提供精确的减法运算
     *
     * @param v1 被减数
     * @param v2 减数
     * @return 两个参数的差
     */
    public static BigDecimal sub(String v1, String v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.subtract(b2);
    }
    /**
     * 提供精确的减法运算
     *
     * @param v1    被减数
     * @param v2    减数
     * @param scale 保留scale 位小数
     * @return 两个参数的差
     */
    public static String sub(String v1, String v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.subtract(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
    }
    /**
     * 提供精确的乘法运算
     *
     * @param v1 被乘数
     * @param v2 乘数
     * @return 两个参数的积
     */
    public static double mul(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.multiply(b2).doubleValue();
    }
    /**
     * 提供精确的乘法运算
     *
     * @param v1 被乘数
     * @param v2 乘数
     * @return 两个参数的积
     */
    public static BigDecimal mul(String v1, String v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.multiply(b2);
    }
    /**
     * 提供精确的乘法运算
     *
     * @param v1    被乘数
     * @param v2    乘数
     * @param scale 保留scale 位小数
     * @return 两个参数的积
     */
    public static double mul(double v1, double v2, int scale) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return round(b1.multiply(b2).doubleValue(), scale);
    }
    /**
     * 提供精确的乘法运算
     *
     * @param v1    被乘数
     * @param v2    乘数
     * @param scale 保留scale 位小数
     * @return 两个参数的积
     */
    public static String mul(String v1, String v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.multiply(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
    }
    /**
     * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到小数点以后10位,以后的数字四舍五入
     *
     * @param v1 被除数
     * @param v2 除数
     * @return 两个参数的商
     */
    public static double div(double v1, double v2) {
        return div(v1, v2, DEF_DIV_SCALE);
    }
    /**
     * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指定精度,以后的数字四舍五入
     *
     * @param v1    被除数
     * @param v2    除数
     * @param scale 表示表示需要精确到小数点以后几位。
     * @return 两个参数的商
     */
    public static double div(double v1, double v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException("The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
    }
    /**
     * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指定精度,以后的数字四舍五入
     *
     * @param v1    被除数
     * @param v2    除数
     * @param scale 表示需要精确到小数点以后几位
     * @return 两个参数的商
     */
    public static String div(String v1, String v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException("The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v1);
        return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).toString();
    }
    /**
     * 提供精确的小数位四舍五入处理
     *
     * @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 = new BigDecimal(Double.toString(v));
        return b.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
    }
    /**
     * 提供精确的小数位四舍五入处理
     *
     * @param v     需要四舍五入的数字
     * @param scale 小数点后保留几位
     * @return 四舍五入后的结果
     */
    public static String round(String v, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b = new BigDecimal(v);
        return b.setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
    }
    /**
     * 取余数
     *
     * @param v1    被除数
     * @param v2    除数
     * @param scale 小数点后保留几位
     * @return 余数
     */
    public static String remainder(String v1, String v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.remainder(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
    }
    /**
     * 取余数  BigDecimal
     *
     * @param v1    被除数
     * @param v2    除数
     * @param scale 小数点后保留几位
     * @return 余数
     */
    public static BigDecimal remainder(BigDecimal v1, BigDecimal v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        return v1.remainder(v2).setScale(scale, BigDecimal.ROUND_HALF_UP);
    }
    /**
     * 比较大小
     *
     * @param v1 被比较数
     * @param v2 比较数
     * @return 如果v1 大于v2 则 返回true 否则false
     */
    public static boolean compare(String v1, String v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        int bj = b1.compareTo(b2);
        boolean res;
        if (bj > 0)
            res = true;
        else
            res = false;
        return res;
    }
}

6.3. 关于金额的java类工具

<!-- https://mvnrepository.com/artifact/org.javamoney/moneta -->
<dependency>
    <groupId>org.javamoney</groupId>
    <artifactId>moneta</artifactId>
    <version>1.4.4</version>
    <type>pom</type>
</dependency>

注意事项

在日常开发中,BigDecimal类被广泛用于精确的数值、金额的计算。但是在使用BigDecimal的过程中,存在以下这几个坑,大家要注意一下哈~~

1、浮点数初始化的坑

反例:

BigDecimal problematic = new BigDecimal(0.1);
System.out.println("Problematic: " + problematic.toString()); 
//输出 Problematic: 0.1000000000000000055511151231257827021181583404541015625

可以发现,直接使用new BigDecimal(double)构造方法可能会得到一个看起来相当奇怪的结果。这是因为double本身的精度问题会被带入BigDecimal中。为了避免这个问题,推荐使用String参数的构造方法或者使用BigDecimal.valueOf方法,例如new BigDecimal("0.1"),这样可以确保BigDecimal的精度。

由于计算机的资源是有限的,所以是没办法用二进制精确的表示 0.1,只能用「近似值」来表示,就是在有限的精度情况下,最大化接近 0.1 的二进制数,于是就会造成精度缺失的情况。

正例:

BigDecimal problematic1 =  BigDecimal.valueOf(0.1);
System.out.println("Problematic: " + problematic1.toString());

BigDecimal problematic2 =  new BigDecimal("0.1");
System.out.println("Problematic: " + problematic2.toString());
//输出
Problematic: 0.1
Problematic: 0.1

2、比较数值时使用compareTo()方法而非equals()

BigDecimal bd1 = new BigDecimal("0.10");
BigDecimal bd2 = new BigDecimal("0.1");
System.out.println(bd1.equals(bd2)); // 输出: false
System.out.println(bd1.compareTo(bd2) == 0); // 输出: true

BigDecimal的equals方法不仅比较数值,还会比较对象的scale(小数点后的位数),如果只想比较数值,而不考虑scale,应该使用compareTo方法。

3、做除法时,未指定精度可能异常

BigDecimal dividend = new BigDecimal("10");
BigDecimal divisor = new BigDecimal("3");
BigDecimal result = dividend.divide(divisor); // 未指定精度和舍入模式
System.out.println(result);
//输出
Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
at java.math.BigDecimal.divide(BigDecimal.java:1693)
at com.example.demo.controller.Test.main(Test.java:26)

除法操作将10除以3,结果是无限循环小数3.3333...,但由于未指定精度和舍入模式,会抛出ArithmeticException异常。

官方有给出解释:

"If the quotient has a nonterminating decimal expansion and the operation is specified to return an exact result, an ArithmeticException is thrown. Otherwise, the exact result of the division is returned, as done for other operations."

要使用BigDecimal时,要记得指定精度,避免因为精度问题带来的损失。

4、BigDecimal转String,科学计数法展示问题

System.out.println( new BigDecimal("0.0000000000001").toString());
BigDecimal bigDecimal = new BigDecimal("1E+12");
System.out.println(bigDecimal.toString());
//输出 1E-13
//输出 1E+12

这是因为 toString() 某些场景下使用科学计数法展示。如果不想用任何计数法,可以使用 toPlainString()

System.out.println( new BigDecimal("0.0000000000001").toPlainString());
BigDecimal bigDecimal = new BigDecimal("1E+12");
System.out.println(bigDecimal.toPlainString());
//输出 
0.0000000000001
1000000000000

5、注意使用setScale方法设置精度

BigDecimal number = new BigDecimal("123.4567");
BigDecimal roundedNumber = number.setScale(2, RoundingMode.HALF_UP);
//输出
123.46

因为BigDecimal的精度是无限的,因此一般在计算的时候,要注意设置精度几位。

并且,RoundingMode.HALF_UP 是一种舍入模式,用于四舍五入,即当数字的一部分被舍去时,如果剩余部分大于或等于0.5,则向上舍入。除了HALF_UP之外,还有几个常用的舍入模式:

  • UP:远离零方向舍入的舍入模式。总是在非零舍弃部分之前增加数字。
  • DOWN:接近零方向舍入的舍入模式。总是在非零舍弃部分之前减少数字。
  • CEILING:接近正无穷大的方向舍入的舍入模式。如果BigDecimal是正的,则舍入行为与UP相同;如果是负的,则舍入行为与DOWN相同。
  • FLOOR:接近负无穷大的方向舍入的舍入模式。如果BigDecimal是正的,则舍入行为与DOWN相同;如果是负的,则舍入行为与UP相同。
  • HALF_DOWN:向“最近邻居”舍入,除非两边距离相等,此时向下舍入。
  • HALF_EVEN:向“最近邻居”舍入,除非两边距离相等,此时向偶数舍入。这种模式也称为“银行家舍入法”,因为它减少了累计错误。

还有一个点,就是:使用setScale方法实际上会产生一个全新的BigDecimal实例,而不会更改原有对象。所以,当你用setScale调整了数字精度后,别忘了把新生成的对象赋值回原来的变量

0

评论区