大数据计算遇到的科学计数问题及运算精度丢失问题
在《Effective Java》这本书中就给出了一个解决方法。该书中也指出,float和double只能用来做科学计算或者是工程计算,在商业计算等精确计算中,我们要用java.math.BigDecimal
对于数据的计算可以用java.math.BigDecimal类的原生方法加减乘除都包括了
我这里有个工具类可以借鉴参考一下:
package com.dm.springboot.utils;
import java.math.BigDecimal;
import java.text.NumberFormat;
/**
*
* 计算
*
*/
public class MathUtils {
// 默认除法运算精度
private static final int DEF_DIV_SCALE = 10;
// 加
public static double add(double d1, double d2) {
if (Double.isNaN(d1) || Double.isNaN(d2)) {
return Double.NaN;
}
BigDecimal big1 = BigDecimal.valueOf(d1);
BigDecimal big2 = BigDecimal.valueOf(d2);
return big1.add(big2).doubleValue();
}
// 减
public static double subtract(double d1, double d2) {
if (Double.isNaN(d1) || Double.isNaN(d2)) {
return Double.NaN;
}
BigDecimal big1 = BigDecimal.valueOf(d1);
BigDecimal big2 = BigDecimal.valueOf(d2);
return big1.subtract(big2).doubleValue();
}
/**
* 减法运算 1.NaN - NaN = NaN 2.NaN - v = NaN 3.v - NaN = v 4.v1-v2 = v3
*
* @param v1
* 被减数
* @param v2
* 减数
* @param flag
* @return
*/
public static double subtract(double v1, double v2, boolean flag) {
if (Double.isNaN(v1)) {
return Double.NaN;
} else if (Double.isNaN(v2)) {
return v1;
}
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 double mul(double v1, double v2) {
if (Double.isNaN(v1) || Double.isNaN(v2)) {
return Double.NaN;
}
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.multiply(b2).doubleValue();
}
/**
*
* 提供精确的连乘运算。
*
* @param inputs
* 连乘参数
*
* @return 积
*
*/
public static double mul(double[] inputs) {
double res = 0.0;
for (int i = 0; i < inputs.length - 1; i++) {
double v1 = 0.0;
double v2 = 0.0;
if (i == 0)
v1 = inputs[i];
else
v1 = res;
v2 = inputs[i + 1];
if (Double.isNaN(v1) || Double.isNaN(v2)) {
return Double.NaN;
}
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
res = b1.multiply(b2).doubleValue();
}
return res;
}
/**
*
* 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
*
* 小数点以后10位,以后的数字四舍五入。
*
* @param v1
* 被除数
*
* @param v2
* 除数
*
* @return 两个参数的商
*
*/
public static double div(double v1, double v2) {
if (Double.isNaN(v1) || Double.isNaN(v2)) {
return Double.NaN;
}
return div(v1, v2, DEF_DIV_SCALE);
/*BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.divide(b2, BigDecimal.ROUND_HALF_UP).doubleValue();*/
}
/**
*
* 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
*
* 定精度,以后的数字四舍五入。
*
* @param v1
* 被除数
*
* @param v2
* 除数
*
* @param scale
* 表示表示需要精确到小数点以后几位。
*
* @return 两个参数的商
*
*/
public static double div(double v1, double v2, int scale) {
if (Double.isNaN(v1) || Double.isNaN(v2)) {
return Double.NaN;
}
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 inputs
* 连除参数,必须保证顺序
*
* @param scale
* 表示表示需要精确到小数点以后几位。
*
* @return 参数的商
*
* @author Add by Huang Ye
*
*/
public static double div(double[] inputs, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
double res = 0.0;
for (int i = 0; i < inputs.length - 1; i++) {
double v1 = 0.0;
double v2 = 0.0;
if (i == 0)
v1 = inputs[i];
else
v1 = res;
v2 = inputs[i + 1];
if (Double.isNaN(v1) || Double.isNaN(v2)) {
return Double.NaN;
}
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
res = b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}
return res;
}
/**
*
* 提供精确的小数位四舍五入处理。
*
* @param dValue
* 需要四舍五入的数字
*
* @param lScale
* 小数点后保留几位
*
* @return 四舍五入后的结果
*
*/
public static double round(double dValue, int lScale) {
// 负数,则装化为正数后进行四舍五入
boolean bFlag = false;
if (dValue < 0) {
bFlag = true;
dValue = -dValue;
}
long lPrecision = 10000; // 精度值,为了避免double型出现偏差,增加校验位
long l45Value = lPrecision / 2 - 1; // 四舍五入的判断值
long lLength = 1; // 乘的数字
for (int i = 0; i < lScale; i++) {
lLength = lLength * 10; // 比如保留2位,乘以100
}
long lValue = (long) dValue; // 值的整数部分
long lValue1 = (long) ((dValue - lValue) * lLength); // 乘以保留位数
long lValue2 = (long) ((dValue - lValue) * lLength * lPrecision); //
long lLastValue = lValue2 - (lValue2 / lPrecision) * lPrecision;
if (lLastValue >= l45Value) {
lValue1++;
}
dValue = lValue + (double) lValue1 / lLength; // 四舍五入后的值
if (bFlag) {
dValue = -dValue;
}
BigDecimal bd = BigDecimal.valueOf(dValue);
bd = bd.setScale(lScale, BigDecimal.ROUND_HALF_UP);
return bd.doubleValue();
}
/**
*
* 提供两个数的比较 by Huang Ye
*
* @param d1
* 需要比较的数字1
*
* @param d2
* 需要比较的数字2
*
* @param scale
* 与之比较的数字的小数位数
*
* @return 是否相等
*
*/
public static boolean equals(double d1, double d2, int scale) {
if (Double.isNaN(d1) || Double.isNaN(d2)) {
return false;
}
double difference = Math.abs(d1 - d2);
double minNum = Math.pow(10, (-1) * scale);
if (difference < minNum)
return true;
else
return false;
}
/**
* 四舍五入保留两位小数
*
* @param number
* @return
*/
public static Double to2dec(Number number) {
NumberFormat format = NumberFormat.getInstance();
format.setMaximumFractionDigits(2);
format.setGroupingUsed(false);
return Double.valueOf(format.format(number.doubleValue() + 1.0e-5));// 防止0.005被舍掉
}
/**
* 判断是否为数字或者小数
*
* @param str
* @return
*/
public static boolean isNumeric(String str){
/*String reg = "\\d+(\\.\\d+)?";
return str.matches(reg);*/
if(str.indexOf("E")>0){
//科学计数法或者字符串
try {
String bigdecimal= new BigDecimal(str).toString();
} catch (Exception e) {
return false;//异常 说明包含非数字。
}
return true;
}else{
return false;
}
}
}
double类型数据加减乘除操作时精度丢失的问题
比方说:
这就需要先把double转换为字符串然后在作为BigDecimal(String val)构造函数的参数。转换为BigDecimal对象之后再进行加减乘除操作,这样精度就不会出现问题了。这也是为什么有关金钱数据存储都使用BigDecimal。
例如:
public static double addDouble(double m1, double m2) {
BigDecimal p1 = new BigDecimal(Double.toString(m1));
BigDecimal p2 = new BigDecimal(Double.toString(m2));
return p1.add(p2).doubleValue();
}
举例:
我们可以看出不管是DecimalFormat,还是Double.toString(),还是String.valueOf(),都最多只是精确到小数点后21位。还是会出现精度的丢失,相比与小数点后59位最大数,还是会丢失一多半的精度。
所以,如果我们计算BigDecimal数据,最好还是使用BigDecimal原生的计算方法。
但是大多数情况,我们都是使用Double保存数据,所以计算时,要想最大限度的保留精度,最好指定精度为59。59位是BigDecimal能够保留的小数点后最大位数。
例如:
public static double div(double v1, double v2) { if (Double.isNaN(v1) || Double.isNaN(v2)) { return Double.NaN; } BigDecimal b1 = new BigDecimal(Double.toString(v1)); BigDecimal b2 = new BigDecimal(Double.toString(v2)); BigDecimal result=b1.divide(b2,59,BigDecimal.ROUND_HALF_UP); System.out.println("MathUtilPlus,result:"+result); return Double.parseDouble(String.valueOf(result)); }
避免出现科学计数法:
public class testNumber { public static void main(String[] args) { Double s =1.00000555555555555555567120378728734269518301718764834933811; System.out.println("第一种方法:"+NonScientificNotation1(s)); System.out.println("第二种方法:"+NonScientificNotation2(s,59)); } /** * @Description: 使用BigDecimal处理 * @Param: [num 要转的科学数字] */ public static String NonScientificNotation1(Double num) { String result=new BigDecimal(num.toString()).toString(); return result; } /** * @Description: 使用DecimalFormat处理 * @Param: [num 要转的科学数字, scale 要保留小数点后的位数] */ public static String NonScientificNotation2(Double num,int scale) { DecimalFormat df = new DecimalFormat("0.0"); //设置a的小数部分允许的最大数字数 df.setMaximumFractionDigits(scale); String result=df.format(num); return result; } }