
Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数,但在实际应用中,可能需要对更大或者更小的数进行运算和处理。而float则处理的是单精度浮点数。
一般情况下,对于那些不需要准确计算精度的数字,我们可以直接使用Float和Double处理,但是Double.valueOf(String) 和Float.valueOf(String)会丢失精度。所以开发中,如果我们需要精确计算的结果,则必须使用BigDecimal类来操作。
BigDecimal所创建的是对象,故我们不能使用传统的+、-、*、/等算术运算符直接对其对象进行数学运算,而必须调用其相对应的方法。方法中的参数也必须是BigDecimal的对象。构造器是类的特殊方法,专门用来创建对象,特别是带有参数的对象。
在实际内存中,以Java为例 ,float是单精度类型,精度是8位有效数字,取值范围是10的-38次方到10的38次方,float占用4个字节的存储空间;double是双精度类型,精度是17位有效数字,取值范围是10的-308次方到10的308次方,double占用8个字节的存储空间。
如果要指定为float类型,需要在小数末尾加上f或F。例如:
float a=1.8; //编译错误 ,不能把double类型的数据直接赋值给float类型的变量
float a=1.8f; //合法
float a=1.8F; //合法
注意float是8位有效数字,第7位数字将会产生四舍五入。所以如果一个float变量 这样定义: float a=1.92344435f; 则第7位将产生四舍五入(5及5以下的都将舍去) 。变量a的实际取值为1.9234444。同理double的17位有效数字也是一样的道理 。
float有效位:8位;
double有效位:17位;
float数据在第8位进行四舍五入;
double数据在第17位四舍五入。
为解决数据精度丢失问题,Java提供了BigDecimal保证数据精度,并在计算时可以定制小数点后面精确位数。夸张的是,它提供了8种数据取舍原则:如下表所示:
通常我们见的最多的,大概应该是规则3, 4, 5。
做相关运算时,
BigDecimal进行加减乘计算时,默认保留了所有数据,小数点后有几位即保留几位
BigDecimal进行除法计算时,需要指定小数点后保留的有效数据位数和数据取舍的规则
代码片断:
BigDecimal num1 = new BigDecimal("0.01");
BigDecimal num2 = new BigDecimal("0.0031");
BigDecimal num3 = new BigDecimal("17.8929183");
logger.info("BigDecimal加法:" + num1.add(num2));
logger.info("BigDecimal减法:" + num1.subtract(num2));
logger.info("BigDecimal乘法:" + num2.multiply(num3));
logger.info("BigDecimal除法:" + num3.divide(num2, THREE, BigDecimal.ROUND_HALF_UP));
嗯,现在,都啰嗦了半天,还没有回到正题。
以上都是Java编程语言中的相关数据类型。对应到PostgreSQL数据库当中,该如何处理?
建议仔细看看datatype-numeric.html#DATATYPE-FLOAT
这里头,real -> float, double precision -> double。它们的精度与java中的float, double稍有出入。而上边的decimal和numeric是完全等价的,相当于是同义词,效果完全相同,并且同属于SQL标准。
我们看看PG对于浮点类型的相关介绍:
数据类型real和double precision是不精确的、精度可变的数字类型。在所有当前支持的平台上,这些类型都是IEEE标准754二进制浮点算术(分别为单精度和双精度)的实现,只要底层处理器、操作系统和编译器支持它。
不精确意味着某些值不能精确地转换为内部格式,而是作为近似值存储,因此存储和检索值可能会显示出轻微的差异。管理这些错误以及它们如何在计算中传播是数学和计算机科学的一个完整分支的主题,这里不讨论,除了以下几点:
如果需要精确的存储和计算(例如货币金额),请使用numeric类型。
如果您希望使用这些类型进行复杂的计算,特别是如果您依赖于边界情况下的某些行为(无限,下溢),则应该仔细评估实现。
比较两个浮点值是否相等可能并不总是如预期的那样有效。
在所有当前支持的平台上,real类型的范围约为1E-37至1E+37,精度至少为6位十进制数字。双精度类型的范围约为1E-307至1E+308,精度至少为15位。值过大或过小都会导致错误。如果输入数字的精度太高,可能会发生舍入。太接近零的数字不能表示为与零不同,将导致下溢错误。(这一块儿确实有点奇怪,为何不是8位和17位,给出的应该是保守值。)
默认情况下,浮点值以最短精确十进制表示的文本形式输出; 生成的十进制值更接近存储的真实二进制值,而不是以相同二进制精度表示的任何其他值。(然而,输出值目前从来不会恰好位于两个可表示值之间,以避免输入例程不正确遵守四舍五入规则的普遍错误。) 对于float8值,该值最多使用17位有效十进制数字,对于float4值,该值最多使用9位有效十进制数字。
为了与旧版本的PostgreSQL生成的输出兼容,并允许降低输出精度,可以使用extra_float_digits参数来选择四舍五入的十进制输出。将值设置为0将恢复先前的默认值,即将值四舍五入为6(对于float4)或15(对于float8)有效的十进制数字。设置负值将进一步减少位数;例如,-2将分别将输出四舍五入为4或13位数字。
任何大于0的extra_float_digits值都会选择最精确的最短格式。
mydb=# show extra_float_digits;
extra_float_digits
--------------------
1
(1 row)
mydb=# create table t3(id real, col2 float8);
CREATE TABLE
mydb=# insert into t3 values(1.012345678, 2.0123455555555555555555555);
INSERT 0 1
mydb=# insert into t3 values(22.012345678, null);
INSERT 0 1
mydb=# select * from t3;
id | col2
-----------+--------------------
1.0123457 | 2.0123455555555556
22.012346 |
(2 rows)
这样,也就能理解上边为何是, real是8位有效数字了, 而float8则是17位有效数字了。real有7位是精确的。当我们把extra_float_digits设为0的时候:
mydb=# alter system set extra_float_digits = 0;
ALTER SYSTEM
mydb=# show extra_float_digits;
extra_float_digits
--------------------
1
(1 row)
mydb=# select pg_reload_conf();
pg_reload_conf
----------------
t
(1 row)
mydb=# show extra_float_digits;
extra_float_digits
--------------------
0
(1 row)
mydb=# select * from t3;
id | col2
---------+------------------
1.01235 | 2.01234555555556
22.0123 |
(2 rows)
PG中的浮点值还有几个特殊值:
Infinity
-Infinity
NaN
它们分别代表IEEE 754的特殊值“无穷大”、“负无穷大”和“非数字”。当在SQL命令中将这些值写成常量时,必须在它们周围加上引号,例如UPDATE table SET x = '-Infinity'。在输入时,以不区分大小写的方式识别这些字符串。无限值也可以拼写为inf和-inf。
注意:IEEE 754规定NaN不应该与任何其他浮点值(包括NaN)相等。为了允许浮点值在基于树的索引中排序和使用,PostgreSQL将NaN值视为等于或大于所有非NaN值。
PostgreSQL还支持sql标准符号float和float(p)来指定不精确的数字类型。这里,p指定以二进制表示的最小可接受精度。PostgreSQL接受float(1)到float(24)选择实数类型,而float(25)到float(53)选择双精度。超出允许范围的p值会产生错误。没有指定精度的浮点数表示双精度。(意味着float表示的还是双精度。)
mydb=# insert into t4 values(2.155555555555555555555, 11.332456);
INSERT 0 1
mydb=# select * from t4;
id | col2
------------------+---------
2.15555555555556 | 11.3325
(1 row)
mydb=# create table t5(id float(15), col2 float(4));
CREATE TABLE
mydb=# insert into t5 values(2.155555555555555555555, 11.332456);
INSERT 0 1
mydb=# select * from t4;
id | col2
------------------+---------
2.15555555555556 | 11.3325
(1 row)
在extra_float_digits = 0的情况下,float与float(15)表示的含义相同。
另外还有float4, float8,它们表示的是4字节的单精,8字节的双精。应该分别是real和double precision的同义词或别名(至少可以这么理解)。
这种类型号称是任意精度的数值类型。
numeric类型可以存储具有非常大位数的数字。特别推荐用于存储货币数量和其他需要精确的数量。数值计算在可能的情况下产生精确的结果,例如加法、减法、乘法。但是,与整数类型或下一节中描述的浮点类型相比,数值值的计算速度非常慢。
一个数字的精度是该整数中有效数字的总数,即小数点两边的位数。数字的比例(scale)是小数点右边小数部分的小数位数。所以23.5141的精度是6,比例是4。整数可以被认为有一个0的scale。
可以配置数字列的最大精度和最大比例尺。要声明数值类型的列,使用以下语法:
NUMERIC(precision, scale)
精度必须为正,而刻度可以为正或负(见下文)。另外:
NUMERIC(precision)
这时scale比例值为0。如果两个值都不指定,在没有任何精度或比例的情况下,创建一个“不受约束的数值”列,其中可以存储任何长度的数值,但不受实现限制。这种类型的列不会将输入值强制转换为任何特定的比例,而具有声明比例的数字列将强制转换为该比例。(SQL标准要求默认比例为0,即强制转换为整数精度。我们发现这有点没用。如果您关心可移植性,请始终明确指定精度和比例。)
可以在数字类型声明中显式指定的最大精度为1000。如果都没有指定,则小数点前最多:131072,小数点后最多:16383位。
有点变态的是,从PostgreSQL 15开始,允许使用负刻度声明数字列。然后将值四舍五入到小数点左边。精度仍然表示非舍入数字的最大数目。因此,从PostgreSQL 15开始,允许声明一个负刻度的数字列。然后将值四舍五入到小数点左边。精度仍然表示非舍入数字的最大数目。因此,将列声明为:
NUMERIC(2, -3)
将值四舍五入到最接近的千位,并且可以存储介于-99000和99000之间的值。还允许声明比声明的精度更大的刻度。这样的列只能保存小数值,并且它要求小数点右边的零位数至少是声明的比例减去声明的精度。例如,声明为的列:
NUMERIC(3, 5)
将值四舍五入到小数点后5位,并可以存储-0.00999和0.00999之间的值。
PostgreSQL允许数字类型声明中的刻度是-1000到1000范围内的任何值。但是,SQL标准要求刻度范围在0到precision之间。使用超出该范围的刻度可能无法移植到其他数据库系统。(实际使用时,需要仔细考虑可移值性)。
数值在物理上存储时没有任何额外的前导零或尾随零。因此,列的声明精度和比例是最大值,而不是固定分配。(从这个意义上说,数字类型更类似于varchar(n)而不是char(n)。) 实际的存储需求是每组4个十进制数字2个字节,再加上3到8个字节的开销。
在“非数字”概念的大多数实现中,NaN不被认为等于任何其他数值(包括NaN)。为了允许数值在基于树的索引中排序和使用,PostgreSQL将NaN值视为等于或大于所有非NaN值。
很自然地,在使用Numric(n, m)来描述有精度要求的数值时,特别是描述与货币相关的值时,很自然地会映射到 java中的BigDecimal类型,在使用这个类型的时候,有好几个坑,足以让你犯致命的错误:
虽然可以用double值来构造,但是会有精度损失。结果有一定的不可预知性。有人可能认为在Java中写入new BigDecimal(0.1)所创建的BigDecimal正好等于 0.1(非标度值 1,其标度为 1),但是它实际上等于0.1000000000000000055511151231257827021181583404541015625。
String 构造方法是完全可预知的:写入 newBigDecimal(“0.1”) 将创建一个 BigDecimal,它正好等于预期的 0.1。因此,比较而言, 通常建议优先使用String构造方法。
当double必须用作BigDecimal的源时,请注意,此构造方法提供了一个准确转换;它不提供与以下操作相同的结果:先使用Double.toString(double)方法,然后使用BigDecimal(String)构造方法,将double转换为String。要获取该结果,请使用static valueOf(double)方法。
java中对BigDecimal比较大小使用的必须是bigdemical的compareTo方法。
0.2和0.20,在你看来是相等的。new BigDecimal(0.2).euqals(new BigDecimal(0.20)), 却是false。因为scale不同。但是使用compareTo却可以得到0,表示两者等值。这种错误犯了一个,就足以让致命 。
不要简单的使用toString(),而是要使用toPlainString()。也可以用专用的格式化方法,如:
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 formatToNumber(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();
}
}
}
在需要精确的小数计算时再使用BigDecimal,BigDecimal的性能比double和float差,在处理庞大,复杂的运算时尤为明显。故一般精度的计算没必要使用BigDecimal。尽量使用参数类型为String的构造函数。
BigDecimal都是不可变的(immutable)的, 在进行每一次四则运算时,都会产生一个新的对象 ,所以在做加减乘除运算时要记得要保存操作后的值。
在将BigDecimal与数据库中的Numeric/Decimal类型相配接时,两边都需要经过良好的匹配。Java中的BigDecimal因为是封闭的对象类型,在使用的时候,很多地方不是按照原生类型一样的模式进行处理,比较的时候不能简单的equals, >, <。构造时也不能简单的通过double硬塞值,格式化时不能想当然的toString()。
将real, double precision (float, float4, float8….), numeric/decimal等一块儿进行对比了解,就基本上能了解整个浮点数在PostgreSQL中大致的实现及使用方法。坑也就不致于那么多了。
https://www.zhihu.com/question/46432979
https://blog.csdn.net/Xin_101/article/details/116781620
https://www.postgresql.org/docs/15/datatype-numeric.html#DATATYPE-FLOAT

原文作者:数据库杂记;如有侵权,请联系管理员删除。