BigDecimal只要涉及到浮点数运算都会用到BigDecimal,并且面试的时候经常会问到,那么BigDecimal使用的时候需要注意什么?
public static void main(String[] args) {
double a = 7.22 + 7.0;
double b = 0.09 + 0.01;
float c = 7.22f + 7.0f;
System.out.println(a);
System.out.println(b);
System.out.println(c);
}
运行结果:
丢失精度的原因:因为不是所有的小数都能用二进制表示,所以,为了解决这个问题,IEEE 754提出了一种使用近似值表示小数的方式,并且引入了精度的概念。这就是我们所熟知的浮点数。
所以,浮点数只是近似值,并不是精确值,所以不能用来表示金额,否则会有精度丢失。
十进制的小数转换为二进制小数,可以采用乘2取整法,主要是利用小数部分乘2,取整数部分,直至小数点后为0。
下面以十进制的0.625为例,将它转化成二进制:
0.2转化二进制:
0.2*2=0.4,整数位为0
0.4*2=0.8,整数位为0
0.8*2=1.6,整数位为1,去掉整数位得0.6
0.6*2=1.2,整数位为1,去掉整数位得0.2
0.2*2=0.4,整数位为0
0.4*2=0.8.整数位为0
就这样推下去!小数*2整,一直下去就行
这个数整不断
0.0011001
十进制数0.2要用二进制数来表示的话,是一个循环小数,无法精确表达。只能根据精度需要,截取小数点后若干位来表示了。因此就会出现丢失精度的问题。
计算机都是以二进制存储,而二进制都是0110,根本没有小数点,我们在电脑计算器当中,十进制转换二进制的时候小数点根本不让你按,为什么呢?因为无限循环小数根本没法使用二进制来精确表示,那遇到浮点数二进制只有0110的计算机是怎么来表示和存储的呢?这就不得不提一下IEEE 754
了。
提IEEE 754
之前我们先来回忆一下科学计数法,科学记数法是一种记数的方法。科学计数法的精妙之处在于,其将"量级"与"数值"两个信息拆分,让使用者对这两个信息更加明确。
数学常识:
^
代替,例如x^2就是x²的意思。a叫做底数,n叫做指数
。例如,2^3=2×2×2=8。那么数x叫做以a为底N的对数
, log a N \log_a{N} logaN 读作以a为底N的对数,其中a叫做对数的底数,N叫做真数。一般的计算器当中的log计算就是计算的指数,然后底数默认为10,输入的时候输入的是真数。因此计算这类对数时,直接点击计算机的“log”键,再打上数字就可以算出来指数。把一个数表示成a与10的n次幂相乘的形式(1≤|a|<10,a不为分数形式,n为整数),这种记数法叫做科学记数法。 例如:19971400000000=1.99714×10^13。计算器或电脑表达10的幂是一般是用E或e,也就是1.99714E13=19971400000000。
用科学记数法表示数时,不改变数的符号,只是改变数的书写形式而已,如:光的速度大约是300,000,000米/秒;这样的数,读、写都很不方便,我们可以免去写这么多重复的0,将其表现为这样的形式:300,000,000=3×10^8, 或者0.00001=1×10^-5,即绝对值小于1的数也可以用科学记数法表示为a乘10 的负n次方的形式。
为了解决部分小数无法使用二进制精确表示的问题,于是就有了IEEE 754规范。IEEE二进制浮点数算术标准(IEEE 754)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用。
IEEE 754规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度(43比特以上,很少使用)与延伸双精确度(79比特以上,通常以80位实现)。其中最常用的就是32位单精度浮点数和64位双精度浮点数。对应的Java当中的float和double。
(1)小数和浮点数的关系:
小数在内存中是以浮点数的形式存储的。浮点数并不是一种数值分类,它和整数、小数、实数等不是一个层面的概念。浮点数是数字(或者说数值)在内存中的一种存储格式,它和定点数是相对的。
Java中使用定点数格式来存储 byte、short、int、long 类型的整数,使用浮点数格式来存储 float、double 类型的小数。整数和小数在内存中的存储格式不一样。
在学习Java语言时,通常认为浮点数和小数是等价的,并没有严格区分它们的概念,这也并没有影响到我们的学习,原因就是浮点数和小数是绑定在一起的,只有小数才使用浮点格式来存储。
其实,整数和小数可以都使用定点格式来存储,也可以都使用浮点格式来存储,但实际情况却是,Java语言使用定点格式存储整数,使用浮点格式存储小数,这是在“数值范围”和“数值精度”两项重要指标之间追求平衡的结果,稍后我会给大家带来深入的剖析。
(2)定点数
所谓定点数,就是指小数点的位置是固定的,不会向前或者向后移动。假设我们用4个字节(32位)来存储无符号的定点数,并且约定,前16位表示整数部分,后16位表示小数部分
如此一来,小数点就永远在第16位之后,整数部分和小数部分一目了然,不管什么时候,整数部分始终占用16位(不足16位前置补0),小数部分也始终占用16位(不足16位后置补0)。
精度: 小数部分的最后一位可能是精确数字,也可能是近似数字(由四舍五入、向零舍入等不同方式得到);除此以外,剩余的31位都是精确数字。从二进制的角度看,这种定点格式的小数,最多有 32 位有效数字,但是能保证的是 31 位;也就是说,整体的精度为 31~32 位。
(3)为什么要有浮点数?
按照上面提到的16位存储整数,16位存储小数,将内存中的所有位(Bit)都置为 1,值最大为 2^16,换算成十进制为 65 536。很显然精度高,因为所有的位都用来存储有效数字了,缺点是取值范围太小。
太阳质量大约是2000000000000000000000000000000=2×10^30千克(地球的330,000倍),约占太阳系总质量的99.86%。如果使用定点数,这将需要很大的一块内存,大到需要几十个字节。
更加科学的方案是按照 =后面的指数形式(2×10^30)来存储,这样不但节省内存,也非常直观。 这种以指数的形式来存储小数的解决方案就叫做浮点数
。浮点数是对定点数的升级和优化,克服了定点数取值范围太小的缺点。
(4)小数以科学计数法表示
IEE754浮点数表示法跟科学计数法表示很像,但又存在一点差距,它属于在2进制的层面来表示。科学计数法是在10进制的层面表示、
将小数转为浮点格式后,小数点为位置发生了浮动,并且 浮动的位数和方向由 E 决定
(5)浮点数的存储
标准的ieee 754格式由三部分组成
仍以 19.625 为例,将它转换为科学计数法的浮点数格式:
如你所见,我们需要存储进内存的只有这三项。
Java语言中浮点型分为:
浮点数的内存分成了三部分,分别用来存储 符号「S」,指数「E」,尾数「M」
我以网上能找到的ieee 754 float32在线转换器为例,简单易懂。地址:https://www.h-schmidt.net/FloatConverter/IEEE754.html
(6)舍入模式
我们可以尝试着再来一个十进制转二进制无限循环的案例,比如0.2转换二进制0.00110011001100… 会1100的无限循环下去。
1
1
浮点数的尾数部分包含的二进制位有限,如果尾数过长,放入内存时必须将多余的位丢掉。
该如何来取这个近似值,IEEE 754 列出了如下四种舍入模式:
(7)总结
因此浮点数在精度方面损失不小,但是在取值范围方面增大很多。牺牲精度,换来取值范围,这就是浮点数的整体思想。
IEEE 754 标准其实还规定了浮点数的加减乘除运算,不过本文的重点是讲解浮点数的存储,所以对于浮点数的运算不再展开讨论。
有效数字保留的位数越多说明数值的精确度越高,相对误差较小。
在线浮点数转换1:https://www.binaryconvert.com/convert_float.html
在线浮点数转换2:https://www.h-schmidt.net/FloatConverter/IEEE754.html
要了解整数是怎么存储的,那就一定要搞清楚原码、反码、补码!
(1)前置概念
计算机底层存储数据时使用的是二进制数字,但是计算机在存储一个数字时并不是直接存储该数字对应的二进制数字,而是存储该数字对应二进制数字的补码
。所以接下来我们需要来了解一下原码、反码和补码。
那么再了解原码、反码、补码之前,我们要了解机器数和真值的概念:
机器数:一个数在计算机的存储形式是二进制数,我们称这些二进制数为机器数,机器数是有符号,在计算机中用机器数的最高位存放符号位,0表示正数,1表示负数
真值:因为机器数带有符号位,所以机器数的形式值不等于其真实表示的值(真值),以机器数1000 0001为例,其真正表示的值(首位为符号位)为-1,而形式值(首位就是代表1)为129;因此将带符号的机器数的真正表示的值称为机器数的真值。
(2)原码、反码、补码介绍
一般计算器中十进制转二进制都是返回的补码
(3)数据在计算机中的存储形式
计算机实际只存储补码,所以原码转换为补码的过程,也可以理解为数据存储到计算机内存中的过程:
在原、反、补码中,正数的表示是一模一样的,而负数的表示是不相同的,所以对于负数的补码来说,我们是不能直接用进制转换将其转换为十进制数值的,因为这样是得不到计算机真正存储的十进制数的,所以应该将其转换为原码后,再将转换得到的原码进行进制转换为十进制数(机器数包含符号位)
(4)为何使用原码、反码、补码
我们上面说过,原码、反码、补码的表示对于正数来说都是一样的,而对于负数来说,三种码的表示确是完全不同的,那大家是否会有个疑问:如果原码才是我们人类可以识别并用于直接计算的表示方式,那为什么还会有反码和补码?计算机直接存储原码不就完事了?
在解决这些问题前,我们先来了解计算机的底层概念,我们人脑可以很轻松的知道机器数的第一位是符号位,但对于计算机基础电路设计来说判别第一位是符号位是非常难和复杂的事情,为了让计算机底层设计更加简单,人们开始探索将符号位参与运算,并且采用只保留加法的方法,我们知道减去一个数,等于加上这个数的负数,即:1-1 = 1 + (-1) = 0,这样让计算机运算就更加简单了,并且也让符号位参与到运算中去
总结:减去一个数,等于加上这个数的负数,让符号位参与到加法运算中去
(5)原码、补码、反码演进的过程
提醒:前提是已经完全掌握上面的原码、反码、补码介绍
byte类型的取值范围由其二进制表示决定。在Java中,byte类型的二进制表示是由8个bit组成的。其中,最高位表示符号位,0表示正数,1表示负数。剩下的7个bit用于表示具体的数值。
以有符号的byte类型为例,最小值是-128,其二进制表示为10000000。最大值是127,其二进制表示为01111111。这样,byte类型的取值范围就确定了。
总结:补码是为了解决正负0的问题,因此就多了一个-128
(6)总结
初始化时通常不会出现精度问题,因为在初始化时可以明确指定初始值,而没有涉及运算。初始化时可以使用特定精度的浮点数值来表示初始值,从而避免舍入误差。
float存放的更大,虽然float占用了4个字节而long占用了8个字节,但是float存储结构不同。他采用了IEEE754的浮点数规范来存储的,也就是把32位分成了两部分,8位存放指数,23位存放尾数。
其中存放指数的8位以无符号形式存储的,因此指数的偏差为其可能值的一半。 对于 float 类型,偏差为 127;对于 double 类型,偏差为 1023。 可以通过将指数值减去偏差值来计算实际指数值。
long属于采用定点数存储,8个字节也就是64bit,符号位占一位,也就是能存储最大的数大概是2的63次方。
因此单从指数上就已经确定了是float存储的大。
只要涉及到小数之间运算、小数之间等值判断的一定要用BigDecimal。
有区别,而且区别很大。因为double是不精确的,所以使用一个不精确的数字来创建BigDeciaml,得到的数字也是不精确的。如0.1这个数字,double只能表示他的近似值。
public static void main(String[] args) {
BigDecimal doubles = new BigDecimal(0.1);
BigDecimal String = new BigDecimal("0.1");
BigDecimal bigDecimal = BigDecimal.valueOf(0.1);
System.out.println(doubles);
System.out.println(String);
System.out.println(bigDecimal);
}
运行结果:
阿里巴巴规范:
BigDecimal.valueOf()是调用Double.toString方法实现的,那么,既然double都是不精确,BigDecimal.valueOf(0.1)怎么保证精确呢?
BigDecimal.valueOf(double) 方法确实是通过将 double 转换为 String 然后再创建 BigDecimal 对象来实现的。这样做是为了避免使用 double 的二进制表示,从而减少精度损失。然而,需要注意的是,由于 double 本身是不精确的浮点数表示,因此在将其转换为 BigDecimal 时,并不能消除 double 的精度问题。
BigDecimal 是 Java 中用于表示精确的十进制数值的类。它可以处理比double和float更大范围的数值,并且可以保证精度不会丢失。它的构造原理主要基于内部的封装了任意精度的整数(unscaled value)和一个标度(scale)的方式来表示十进制数。
我们看一下BigDecimal构造原理:
package java.math;
public class BigDecimal {
//值的绝对long型表示
private final transient long intCompact;
//值的小数点后的位数,也称之为标度
private final int scale;
private final BigInteger intVal;
//值的有效位数,不包含正负符号,也称之为精度
private transient int precision;
private transient String stringCache;
//加、减、乘、除、绝对值
public BigDecimal add(BigDecimal augend) {}
public BigDecimal subtract(BigDecimal subtrahend) {}
public BigDecimal multiply(BigDecimal multiplicand) {}
public BigDecimal divide(BigDecimal divisor) {}
public BigDecimal abs() {}
}
以long型的intCompact和scale来存储精确的值。创建BigDecimal对象时,他会优先转换成String类型,比如double转BigDecimal也是先double转成String,再String转成BigDecimal。
在BigDecimal中,每个数值都有一个精度和一个标度。精度是指数值中有效数字的位数,而标度是指小数点后面的位数。例如,对于数值123.45,它的精度是5,标度是2
。在进行加、减、乘、除等运算时,BigDecimal会根据数值的精度和标度来进行计算。在计算过程中,它会自动调整数值的精度和标度,以保证计算结果的精度和正确性。
BigDecimal还提供了一些方法来进行数值的格式化和比较。例如,可以使用setScale方法来设置数值的标度,使用compareTo方法来比较两个数值的大小等等。
如果scale为零或正值,则该值表示这个数字小数点右侧的位数。如果scale为负数,则该数字的无标度值需要乘以10的该负数的绝对值的幂。例如,scale为-3,则这个数需要乘1000,即在末尾有3个0。如123.123,那么如果使用BigDecimal表示,那么他的无标度值为123123,他的标度为3。
BigDecimal 是不可变的,意味着一旦创建了一个 BigDecimal 对象,它的值不能被更改。并且发生加减乘除操作也都是生成新的对象,这种不可变性有几个重要的原因
总的来说,BigDecimal 之所以是不可变的,是为了确保精确性、线程安全性、可预测性和安全性。这些特性使其成为处理精确数值的理想选择,特别是在需要高精度的计算和金融领域。
BigDecimal 类提供了不同的方法来将 BigDecimal 对象转换为字符串表示。这些方法之间的区别在于它们的输出格式以及如何处理标度(scale)和小数点。
注意:toString()、toEngineeringString()方法在某些时候会使用科学计数法或工程计数法,不是所有情况都会使用科学计数法或工程计数法的
public static void main(String[] args) {
BigDecimal bg = new BigDecimal("1E11");
System.out.println(bg.toString()); // 1E+11
System.out.println(bg.toPlainString()); // 100000000000
System.out.println(bg.toEngineeringString()); // 100E+9
}
运行结果:
我们看一下源码中toString方法中给的example:
总结了两种toString()方法会以科学计数方式输出的场景:
场景一测试:
public static void main(String[] args) {
BigDecimal a = new BigDecimal("2340").setScale(-1);
System.out.println(a.toString());
System.out.println(a.toPlainString());
System.out.println(a.scale());
System.out.println(a.unscaledValue());
}
运行结果:
场景二测试:
public static void main(String[] args) {
//案例一
BigDecimal b1 = new BigDecimal("0.000000123").setScale(9);
System.out.println(b1.toString());
System.out.println(b1.toPlainString());
System.out.println(b1.scale());
System.out.println(b1.unscaledValue());
//输出结果为
1.23E-7
0.000000123
9
123
//案例二
BigDecimal b2 = new BigDecimal("0.000001234").setScale(9);
System.out.println(b2.toString());
System.out.println(b2.toPlainString());
System.out.println(b2.scale());
System.out.println(b2.unscaledValue());
//输出结果为
0.000001234
0.000001234
9
1234
//案例三
BigDecimal b3 = new BigDecimal("0.123000000").setScale(9);
System.out.println(b3.toString());
System.out.println(b3.toPlainString());
System.out.println(b3.scale());
System.out.println(b3.unscaledValue());
//输出结果为
0.123000000
0.123000000
9
123000000
//案例四
BigDecimal b4 = new BigDecimal("123000000");
System.out.println(b4.toString());
System.out.println(b4.toPlainString());
System.out.println(b4.scale());
System.out.println(b4.unscaledValue());
//输出结果为
123000000
123000000
0
123000000
//案例五
//Double d = 12345678d; Double d = 12345678.0; 效果一样
Double d = (double) 12345678;
BigDecimal b5 = BigDecimal.valueOf(d);
System.out.println(d);
System.out.println(b5.toString());
System.out.println(b5.toPlainString());
System.out.println(b5.scale());
System.out.println(b5.unscaledValue());
//输出结果为
1.2345678E7
12345678
12345678
0
12345678
}
如果一个BigDecimal类型的参数toString()是以指数形式返回,那么调用toEngineeringString()则以工程计数法返回,工程计数法返回的10的幂必须是3的倍数
因为equals比较的时候会比较标度。new BigDecimal(“0.10000”)和new BigDecimal(“0.1”)这两个数的标度分别是5和1,如果使用BigDecimal的equals方法比较,得到的结果是false。
阿里巴巴规范:
public static void main(String[] args) {
BigDecimal bigDecimal = new BigDecimal("0.10000");
BigDecimal bigDecimal1 = new BigDecimal("0.1");
System.out.println(bigDecimal1.equals(bigDecimal1));
System.out.println(bigDecimal.compareTo(bigDecimal1));
}
运行结果:
浮点数一旦发生运算就会丢失精度,正常来说不进行运算,只是简单的比较,==比较没有什么问题。
在某些情况下,使用 String.format 可能会引入精度丢失问题,特别是当你尝试格式化 BigDecimal 对象时。这是因为 String.format 本质上是基于浮点数格式化的,而浮点数本身存在精度问题。
public static void main(String[] args) {
BigDecimal price = new BigDecimal("999999.999");
// 错误示例
String format = String.format("%.2f", price);
System.out.println(format);
// 正确示例(这里就涉及到了小数点的舍去模式,BigDecimal一共提供了8种舍去模式)
String s = price.setScale(2, RoundingMode.DOWN).toPlainString();
System.out.println(s);
}
运行结果:
总的来说,BigDecimal 是处理高精度数值计算的重要工具,但需要小心处理精度、性能和错误处理等问题,以确保正确性和可靠性。