Java中的金额表示与计算

为什么不能用浮点数表示金额?

浮点数在计算机中是以二进制表示的,但是很多十进制的小数(如0.1、0.01等)在二进制中无法精确表示,因为它们的二进制表示是无限循环的。

例如,0.1在二进制表示中是0.00011001100110011...(无限循环),由于计算机的存储是有限的,所以在存储和计算过程中会进行截断,导致精度丢失。这样,进行一系列浮点数运算后,可能得到的结果会存在舍入误差,从而导致计算结果与预期不符。

这种舍入误差在数值计算中是非常危险的,尤其在涉及金额和金融计算时更为严重。小的舍入误差可能在单个计算中不明显,但在大量的计算中会累积起来,导致计算结果出现较大的偏差,影响到财务数据的准确性和可靠性。

为了避免在金额计算中产生舍入误差,更好的做法是使用整数类型(如整型或长整型)来表示金额,或者使用专门的高精度计算库(如Java中的BigDecimal类),这样可以确保金额计算的精确性。这些方法在进行金融和货币计算时更为可靠,避免了因浮点数运算而产生的不确定性。

使用BigDecimal类进行高精度运算时,可以设置精确的小数位数、进行四舍五入等操作,确保计算结果的准确性和可靠性。对于涉及金额等重要数据的计算,建议始终使用BigDecimal来进行处理,从而避免由于浮点数带来的不确定性和精度丢失问题。这样可以保证在金融等关键领域的应用中数据的准确性和可信度。

十进制转换为二进制

  1. 十进制整数转换为二进制整数的方法是“除2取余,逆序排列”法。

  2. 用2整除十进制整数,得到一个商和余数,再用2去除商,继续得到商和余数,直到商为小于1时停止。将先得到的余数作为二进制数的低位有效位,后得到的余数作为高位有效位,依次排列起来。

  3. 十进制小数转换为二进制小数的方法是“乘2取整,顺序排列”法。

  4. 用2乘十进制小数,得到一个积,将积的整数部分取出,再用2乘余下的小数部分,继续得到积,再取出积的整数部分。如此进行,直到积中的小数部分为零,此时0或1为二进制的最后一位,或者达到所要求的精度为止。

举例:

  • 十进制整数127转换成二进制为1111111。
  • 十进制小数0.625转换成二进制为0.101。

不是所有数都能用二进制表示

在计算机中,浮点数的表示是有限的,而不是精确的。无论是单精度浮点数(float)还是双精度浮点数(double),它们都是采用有限的位数来表示小数部分的。因此,对于某些十进制小数,其在二进制表示中会产生无限循环的情况,从而无法精确地表示。

0.1 的二进制表示是无限循环的,即 (0.1)10 = (0.000110011001100…)2。由于计算机的浮点数表示是有限的,它只能取近似值来表示这种无限循环的小数。因此,对于像 0.1 这样的小数,计算机无法精确地表示它,而只能使用近似值来表示,从而导致精度丢失。

在实际编程中,特别是涉及到涉及到金融和精确计算的场景,我们应该避免直接使用浮点数进行高精度运算,因为会导致精度丢失。而应该使用专门的精确计算类,如 Java 中的 BigDecimal,来进行高精度的计算,从而避免精度丢失的问题。

IEEE 754

IEEE 754标准定义了浮点数的表示方法,它通过三元组{S, E, M}来表示一个浮点数N,其中:

  • S(sign)表示N的符号位,对应值s满足:N > 0时,s = 0;N ≤ 0时,s = 1。
  • E(exponent)表示N的指数位,位于S和M之间的若干位。对应值e值也可正可负。
  • M(mantissa)表示N的尾数位,恰好位于N末尾。M也叫有效数字位(significand)、系数位(coefficient),甚至被称作“小数”。

根据IEEE 754标准,浮点数的实际值n由下方的式子表示:

n = (-1)^s * (1.M) * 2^(e - Bias)

其中,Bias是一个偏移值,它在单精度浮点数中为127,在双精度浮点数中为1023。这样,通过浮点数的表示方式,计算机可以用有限的位数来表示一个范围更大的数,并提供一定的精度。

然而,正如您指出的,由于浮点数表示的精度是有限的,对于某些小数,特别是无限循环的二进制数,计算机无法精确地表示它们。这导致在浮点数运算中可能会出现一些精度丢失的情况。

为了避免精度丢失,特别是在涉及到金额等精确计算的场景,我们应该使用专门的精确计算类,如Java中的BigDecimal,来进行高精度的计算。BigDecimal能够提供更高的精度,从而避免了浮点数运算带来的精度问题。


为什么不能用BigDecimal的equals方法做等值比较?

在Java中,对于比较浮点数,包括BigDecimal,不能简单地使用equals()方法进行等值比较,这是因为浮点数是近似值的表示,在进行浮点数运算时可能会存在精度损失。这个问题同样适用于使用doublefloat进行浮点数运算。

BigDecimal中,equals()方法是继承自Object类的方法,它用于比较对象的引用是否相等,而不是比较数值是否相等。如果要在BigDecimal中比较数值是否相等,应该使用compareTo()方法或equals()方法的重载版本。

例如,BigDecimalcompareTo()方法用于比较两个BigDecimal对象的数值是否相等,它返回一个整数值,表示两个数值的大小关系。如果返回值为0,则表示两个BigDecimal对象的数值相等。

示例:

BigDecimal num1 = new BigDecimal("0.1");
BigDecimal num2 = new BigDecimal("0.100");

if (num1.equals(num2)) {
    System.out.println("num1 and num2 are equal using equals() method.");
} else {
    System.out.println("num1 and num2 are not equal using equals() method.");
}

if (num1.compareTo(num2) == 0) {
    System.out.println("num1 and num2 are equal using compareTo() method.");
} else {
    System.out.println("num1 and num2 are not equal using compareTo() method.");
}

输出:

num1 and num2 are not equal using equals() method.
num1 and num2 are equal using compareTo() method.

在上面的示例中,equals()方法返回false,因为它比较的是对象的引用,而不是数值。而compareTo()方法返回0,表示num1num2的数值是相等的。

因此,要在BigDecimal中比较数值是否相等,应该使用compareTo()方法或equals()方法的重载版本,并根据具体情况选择适当的精度来比较数值。例如,可以使用compareTo()方法来判断两个BigDecimal对象的数值是否相等,或使用setScale()方法设置精度后再使用equals()方法进行比较。


Compares this BigDecimal with the specified Object for equality. Unlike compareTo, this method considers two BigDecimal objects equal only if they are equal in value and scale (thus 2.0 is not equal to 2.00 when compared by this method)

BigDecimal 类的标度问题确实是一个相对复杂的主题。标度代表着小数部分的位数。

首先,我们来解释一下 BigDecimal 构造方法的标度问题:

  1. BigDecimal(int val)BigDecimal(long val):这两个构造方法用于创建整数类型的 BigDecimal 对象,所以它们的标度都是0。

  2. BigDecimal(double val):使用 BigDecimal(double) 构造方法时,由于 double 类型本身就是近似值,创建出来的 BigDecimal 对象也会是近似值。比如,new BigDecimal(0.1) 所创建的 BigDecimal 值并不是精确的0.1,而是近似值0.1000000000000000055511151231257827021181583404541015625。这样的近似值决定了标度是这个近似值的位数,即55。

现在,让我们通过代码示例来验证上述描述:

import java.math.BigDecimal;

public class BigDecimalScaleExample {
    public static void main(String[] args) {
        // 使用BigDecimal(int)构造方法
        BigDecimal intValue = new BigDecimal(100);
        System.out.println("整数的标度:" + intValue.scale()); // 输出:整数的标度:0

        // 使用BigDecimal(long)构造方法
        BigDecimal longValue = new BigDecimal(1000000000000L);
        System.out.println("长整数的标度:" + longValue.scale()); // 输出:长整数的标度:0

        // 使用BigDecimal(double)构造方法
        BigDecimal doubleValue1 = new BigDecimal(0.1);
        BigDecimal doubleValue2 = new BigDecimal(0.10);
        System.out.println("double值1的标度:" + doubleValue1.scale()); // 输出:double值1的标度:55
        System.out.println("double值2的标度:" + doubleValue2.scale()); // 输出:double值2的标度:55
    }
}

小贴士:

  1. 输出的结果中,整数类型的标度都是0,而使用 BigDecimal(double) 构造方法创建的 BigDecimal 对象的标度都是55,因为它们的近似值都是含有55位小数。

BigDecimal(double)和BigDecimal(String)有什么区别?

BigDecimal(double)BigDecimal(String) 构造方法之间有着明显的区别。

  1. BigDecimal(double) 构造方法:

由于 double 类型是不精确的,当我们使用 BigDecimal(double) 构造方法来创建 BigDecimal 对象时,得到的结果也是不精确的。这是因为 double 只能表示一个近似值,而不能精确表示某个具体的数字。

例如,当我们使用 new BigDecimal(0.1) 创建一个 BigDecimal 对象时,实际上创建出来的值并不等于精确的0.1。而是近似值:0.1000000000000000055511151231257827021181583404541015625。这种近似值是由 double 类型本身决定的。

  1. BigDecimal(String) 构造方法:

与使用 BigDecimal(double) 构造方法不同,当我们使用 BigDecimal(String) 构造方法来创建 BigDecimal 对象时,我们可以确切地指定要表示的数值。这是因为使用字符串来表示数值时不会引入浮点数的近似问题,字符串中的每个字符都会被精确地转换为相应的数字。

例如,当我们使用 new BigDecimal("0.1") 创建一个 BigDecimal 对象时,创建出来的值正好等于精确的0.1。

现在,让我们来通过代码示例来验证上述描述:

import java.math.BigDecimal;

public class BigDecimalComparisonExample {
    public static void main(String[] args) {
        // 使用BigDecimal(double)构造方法
        BigDecimal doubleValue = new BigDecimal(0.1);
        System.out.println("BigDecimal(double)值:" + doubleValue); // 输出:BigDecimal(double)值:0.1000000000000000055511151231257827021181583404541015625

        // 使用BigDecimal(String)构造方法
        BigDecimal stringValue = new BigDecimal("0.1");
        System.out.println("BigDecimal(String)值:" + stringValue); // 输出:BigDecimal(String)值:0.1
    }
}

在以上代码示例中,我们使用了 BigDecimal(double)BigDecimal(String) 构造方法分别创建了两个 BigDecimal 对象,并打印出它们的值。可以看到,使用 BigDecimal(double) 构造方法创建的值是近似值,而使用 BigDecimal(String) 构造方法创建的值是精确的。


BigDecimal 确实是通过一个 "无标度值" 和一个 "标度" 来表示一个数的。无标度值表示数字的实际值,而标度表示小数部分的位数。

BigDecimal 中,无标度值的表示方式取决于实际情况:

  1. 当无标度值超过阈值(默认为 Long.MAX_VALUE)时,使用 BigInteger 类型的 intVal 字段存储无标度值,并且 intCompact 字段存储 Long.MIN_VALUE。这是因为无标度值过大无法用 long 类型完整表示,所以使用 BigInteger 进行存储。

  2. 否则,当无标度值在可用的 long 范围内时,对无标度值进行压缩存储在 long 类型的 intCompact 字段中,用于后续计算,而 intVal 字段为空。

下面是对你提供的 BigDecimal 类的简化代码片段,用于说明 intValscaleintCompact 字段:

import java.math.BigDecimal;
import java.math.BigInteger;

public class BigDecimalExplanation {
    public static void main(String[] args) {
        // 示例:使用BigDecimal表示数值
        BigInteger intVal = new BigInteger("12345678901234567890");
        int scale = 6;
        long intCompact = Long.MIN_VALUE;

        BigDecimal bigDecimalWithBigInt = new BigDecimal(intVal, scale);
        BigDecimal bigDecimalWithCompact = new BigDecimal(intCompact, scale);

        System.out.println("BigDecimal with BigInteger: " + bigDecimalWithBigInt);
        System.out.println("BigDecimal with long (compact): " + bigDecimalWithCompact);
    }
}

在上面的代码示例中,我们创建了两个不同的 BigDecimal 对象:一个使用 BigIntegerintVal 字段表示无标度值,另一个使用 long 类型的 intCompact 字段表示无标度值。两个 BigDecimal 对象都具有相同的标度 scale

请注意,为了简化代码示例,我们直接创建了 BigIntegerlong 类型的值,实际情况中,这些值通常是由其他计算过程或者输入得到的。


BigDecimal(double)有什么问题

使用 BigDecimal(double) 构造方法来创建 BigDecimal 对象时,由于双精度浮点数的不精确性,会导致精度损失的问题。为了解决这个问题,我们应该使用 BigDecimal(String) 构造方法来确保精确表示。

下面是一个示例代码,演示了使用 BigDecimal(String) 构造方法来避免精度损失的情况:

import java.math.BigDecimal;

public class BigDecimalDoubleIssue {
    public static void main(String[] args) {
        double doubleValue = 0.1;
        String stringValue = "0.1";

        BigDecimal bdFromDouble = new BigDecimal(doubleValue);
        BigDecimal bdFromString = new BigDecimal(stringValue);

        System.out.println("BigDecimal from double: " + bdFromDouble);
        System.out.println("BigDecimal from String: " + bdFromString);
    }
}

在上述代码中,我们分别使用 BigDecimal(double)BigDecimal(String) 构造方法创建了两个 BigDecimal 对象,并打印出了它们的值。请注意,当使用 BigDecimal(double) 构造方法时,精度损失会导致结果不准确,而使用 BigDecimal(String) 构造方法则可以确保精确表示。

输出结果为:

BigDecimal from double: 0.1000000000000000055511151231257827021181583404541015625
BigDecimal from String: 0.1

如你所见,使用 BigDecimal(String) 构造方法创建的 BigDecimal 对象确保了精确表示,而使用 BigDecimal(double) 构造方法则引起了精度损失。

为了避免精度损失,我们应该始终使用 BigDecimal(String) 构造方法,将双精度浮点数转换为字符串后再进行构造。


使用BigDecimal(String)创建

对于 BigDecimal(String) 构造方法,我们可以通过传入一个字符串来精确地表示小数。例如,使用 new BigDecimal("0.1") 就可以创建一个精确表示为0.1的 BigDecimal,其标度为1。

然而,需要注意的是,new BigDecimal("0.10000")new BigDecimal("0.1") 这两个数的标度分别是5和1。如果我们使用 BigDecimalequals 方法进行比较,得到的结果是 false,因为标度不同。

为了创建一个精确表示0.1的 BigDecimal,我们可以使用以下两种方式:

  1. 使用 BigDecimal(String) 构造方法:
BigDecimal recommend1 = new BigDecimal("0.1");
  1. 使用 BigDecimal.valueOf(double) 方法:
BigDecimal recommend2 = BigDecimal.valueOf(0.1);

现在,让我们来解释一下 BigDecimal.valueOf(0.1) 是如何保证精确性的。

解释:

BigDecimal.valueOf(double) 方法是 BigDecimal 类的一个静态工厂方法。当我们调用 BigDecimal.valueOf(0.1) 时,它会将 double 类型的参数转换为字符串,然后再使用 BigDecimal(String) 构造方法来创建 BigDecimal 对象。

在这个过程中,有一个关键点是:BigDecimalBigDecimal(String) 构造方法能够确保精确地表示字符串中的小数。尽管传入的参数是一个 double 类型的近似值,但在转换为字符串时,它会保留足够的位数,从而确保精确性。

以下是可执行的完整代码示例来展示 BigDecimal.valueOf(0.1) 是如何保证精确性的:

import java.math.BigDecimal;

public class BigDecimalExample {
    public static void main(String[] args) {
        // 使用 BigDecimal(String) 构造方法创建精确表示0.1的 BigDecimal
        BigDecimal recommend1 = new BigDecimal("0.1");

        // 使用 BigDecimal.valueOf(double) 方法创建精确表示0.1的 BigDecimal
        BigDecimal recommend2 = BigDecimal.valueOf(0.1);

        // 打印结果
        System.out.println("Recommended 1: " + recommend1);
        System.out.println("Recommended 2: " + recommend2);

        // 使用 equals 方法进行比较
        System.out.println("Equals: " + recommend1.equals(recommend2));
    }
}

输出结果为:

Recommended 1: 0.1
Recommended 2: 0.1
Equals: true

如你所见,通过 BigDecimal.valueOf(0.1) 方法创建的 BigDecimal 精确地表示了0.1,并且与使用 BigDecimal(String) 构造方法创建的 BigDecimal 是相等的。


总结:

计算机采用二进制表示数据,而很多十进制小数(如0.1)在二进制中是无限循环小数,因此无法精确表示。为了处理这个问题,计算机引入了单精度浮点数(float)和双精度浮点数(double)等近似表示方法。

这样的近似表示导致了精度损失,因此在进行计算时,得到的结果也可能不是完全准确的。

为了避免精度损失,我们应该使用 BigDecimal(String) 构造方法来创建 BigDecimal 对象,从而确保精确表示小数。

下面是一个可执行的完整代码示例,展示了如何使用 BigDecimal(String) 构造方法创建 BigDecimal 对象,并进行精确的计算:

import java.math.BigDecimal;

public class BigDecimalExample {
    public static void main(String[] args) {
        // 使用 BigDecimal(String) 构造方法创建 BigDecimal 对象
        BigDecimal decimalValue = new BigDecimal("0.1");
        BigDecimal otherValue = new BigDecimal("0.2");

        // 精确计算
        BigDecimal sum = decimalValue.add(otherValue);
        BigDecimal product = decimalValue.multiply(otherValue);

        // 打印结果
        System.out.println("BigDecimal 1: " + decimalValue);
        System.out.println("BigDecimal 2: " + otherValue);
        System.out.println("Sum: " + sum);
        System.out.println("Product: " + product);
    }
}

在上述代码中,我们使用 BigDecimal(String) 构造方法来创建两个 BigDecimal 对象,分别表示0.1和0.2。然后,我们使用 add 方法对它们进行相加,并使用 multiply 方法进行乘法运算。由于使用了 BigDecimal(String) 构造方法,保证了精确表示,因此得到的结果是准确的。

输出结果为:

BigDecimal 1: 0.1
BigDecimal 2: 0.2
Sum: 0.3
Product: 0.02
赞赏