【小家java】Java数值运算 [加减乘除] 精度丢失原因分析,提供保证精度的MathHelper工具类(上)

简介: 【小家java】Java数值运算 [加减乘除] 精度丢失原因分析,提供保证精度的MathHelper工具类(上)

啥都不说,先看一个例子


    public static void main(String[] args) {
        //加减乘除都出现了对应的精度问题
        System.out.println(0.05 + 0.01); //0.060000000000000005
        System.out.println(1.0 - 0.42); //0.5800000000000001
        System.out.println(4.015 * 100); //401.49999999999994
        System.out.println(123.3 / 100); //1.2329999999999999
    }


有没有一种触目惊心的感觉,感觉回去检查检查自己的代码,有没有一些数值运算吧,哈哈。这个问题相当严重,比如你有9.999999999999元,你的计算机是不会认为你可以购买10元的商品的。在有的编程语言中提供了专门的货币类型来处理这种情况,但是Java没有。


下面会解释原因以及提出解决方案。但结论可以先给大家:


Java中的简单浮点数类型float和double不能够进行运算。


问题分析


我们的第一个反应是做四舍五入。Math类中的round方法不能设置保留几位小数,我们只能象这样(保留两位):


public double round(double value){
    return Math.round(value*100)/100.0;
}


非常不幸,上面的代码并不能正常工作,给这个方法传入4.015它将返回4.01而不是4.02,如我们在上面看到的4.015*100=401.49999999999994


因此如果我们要做到精确的四舍五入,不能利用简单类型做任何运算

java.text.DecimalFormat也不能解决这个问题:

System.out.println(new java.text.DecimalFormat("0.00").format(4.025)); //4.02

现在我们已经可以解决这个问题了,原则是使用BigDecimal并且一定要用String来构造。否则见下面例子


    public static void main(String[] args) {
        double g = 12.35;
        BigDecimal bigG = new BigDecimal(g).setScale(1, BigDecimal.ROUND_HALF_UP);
        System.out.println(bigG.doubleValue()); //期望得到12.4  但实际输出:12.3
    }

大概原因:定义double g= 12.35; 而在计算机中二进制表示可能这是样:定义了一个g=12.34444444444444449,

new BigDecimal(g) g还是12.34444444444444449

new BigDecimal(g).setScale(1, BigDecimal.ROUND_HALF_UP); 得到12.3


正确的定义方式是使用字符串构造函数:

new BigDecimal("12.35").setScale(1, BigDecimal.ROUND_HALF_UP)


解决方案


上面也已经提到过了,我们可以借助BigDecimal来解决这个问题。因此此处我提供一共工具类,**以后大家java中的数值运算都采用此工具类处理,就绝对不会有精度问题了:MathHelper **


import java.math.BigDecimal;
/**
 * 精确的加减乘除的工具类
 *
 * @author fangshixiang
 * @description //
 * @date 2019/1/16 16:34
 */
public abstract class MathHelper {
    //默认10才能进位
    private static final int DEF_DIV_SCALE = 10;
    /**
     * 提供精确的加法运算。
     *
     * @param v1 被加数
     * @param v2 加数
     * @return 两个参数的和 double
     */
    public static BigDecimal add(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.add(b2);
    }
    /**
     * 提供精确的减法运算。
     *
     * @param v1 被减数
     * @param v2 减数
     * @return 两个参数的差 double
     */
    public static BigDecimal sub(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.subtract(b2);
    }
    /**
     * 提供精确的乘法运算。
     *
     * @param v1 被乘数
     * @param v2 乘数
     * @return 两个参数的积 double
     */
    public static BigDecimal mul(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.multiply(b2);
    }
    /**
     * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到 小数点以后10位,以后的数字四舍五入。
     *
     * @param v1 被除数
     * @param v2 除数
     * @return 两个参数的商 double
     */
    public static BigDecimal div(double v1, double v2) {
        return div(v1, v2, DEF_DIV_SCALE);
    }
    /**
     * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指 定精度,以后的数字四舍五入。
     *
     * @param v1    被除数
     * @param v2    除数
     * @param scale 表示表示需要精确到小数点以后几位。
     * @return 两个参数的商 double
     */
    public static BigDecimal 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);
    }
    /**
     * 提供精确的小数位四舍五入处理。
     *
     * @param v     需要四舍五入的数字
     * @param scale 小数点后保留几位
     * @return 四舍五入后的结果 double
     */
    public static BigDecimal 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));
        BigDecimal one = new BigDecimal("1");
        return b.divide(one, scale, BigDecimal.ROUND_HALF_UP);
    }
}


提供加减乘除、四舍五入的方法。保证精度。返回值类型为保证精度的BigDecimal类型,根据业务需要请转换为自己需要的类型。


下面我们利用此工具类把上面例子再跑一次:


    public static void main(String[] args) {
        System.out.println(add(0.05, 0.01)); //0.06
        System.out.println(sub(1.0, 0.42)); //0.58
        System.out.println(mul(4.015, 100)); //401.5000
        System.out.println(div(123.3, 100)); //1.2330000000
        //返回值用double展示  可以保证精度
        System.out.println(add(0.05, 0.01).doubleValue()); //0.06
        System.out.println(sub(1.0, 0.42).doubleValue()); //0.58
        System.out.println(mul(4.015, 100).doubleValue()); //401.5
        System.out.println(div(123.3, 100).doubleValue()); //1.233
    }
    public static void main(String[] args) {
        Integer i = 1;
        Long l = 1L;
        Float f = 1.0f;
        //这样转换成double类型 或者直接强转
        System.out.println(i.doubleValue()); //1.0
        System.out.println(l.doubleValue()); //1.0
        System.out.println(f.doubleValue()); //1.0
    }

如上,若返回值不知道是整数还是小数,请使用double接收。因为double强转为int是会失去精度的


    public static void main(String[] args) {
        System.out.println(add(0.05, 0.01).intValue()); //0
        System.out.println(sub(1.0, 0.42).intValue()); //0
        System.out.println(mul(4.015, 100).intValue()); //401
        System.out.println(div(123.3, 100).intValue()); //1
        //double强转为int、long问题   它会直接舍弃小数部分保留整数部分  需要注意
        System.out.println((int) 3.41); //3
        System.out.println((int) 3.81); //3
    }



相关文章
|
11月前
|
Java Go 开发工具
【Java】(9)抽象类、接口、内部的运用与作用分析,枚举类型的使用
抽象类必须使用abstract修饰符来修饰,抽象方法也必须使用abstract修饰符来修饰,抽象方法不能有方法体。抽象类不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例。抽象类可以包含成员变量、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类(接 口、枚举)5种成分。抽象类的构造器不能用于创建实例,主要是用于被其子类调用。抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类abstract static不能同时修饰一个方法。
399 1
|
11月前
|
存储 Java Go
【Java】(3)8种基本数据类型的分析、数据类型转换规则、转义字符的列举
牢记类型转换规则在脑海中将编译和运行两个阶段分开,这是两个不同的阶段,不要弄混!
422 2
|
12月前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
|
安全 Java 编译器
new出来的对象,不一定在堆上?聊聊Java虚拟机的优化技术:逃逸分析
逃逸分析是一种静态程序分析技术,用于判断对象的可见性与生命周期。它帮助即时编译器优化内存使用、降低同步开销。根据对象是否逃逸出方法或线程,分析结果分为未逃逸、方法逃逸和线程逃逸三种。基于分析结果,编译器可进行同步锁消除、标量替换和栈上分配等优化,从而提升程序性能。尽管逃逸分析计算复杂度较高,但其在热点代码中的应用为Java虚拟机带来了显著的优化效果。
392 4
|
机器学习/深度学习 安全 Java
Java 大视界 -- Java 大数据在智能金融反洗钱监测与交易异常分析中的应用(224)
本文探讨 Java 大数据在智能金融反洗钱监测与交易异常分析中的应用,介绍其在数据处理、机器学习建模、实战案例及安全隐私等方面的技术方案与挑战,展现 Java 在金融风控中的强大能力。
Java 大视界 -- Java 大数据在智能家居能源消耗模式分析与节能策略制定中的应用(198)
简介:本文探讨Java大数据技术在智能家居能源消耗分析与节能策略中的应用。通过数据采集、存储与智能分析,构建能耗模型,挖掘用电模式,制定设备调度策略,实现节能目标。结合实际案例,展示Java大数据在智能家居节能中的关键作用。
|
数据采集 搜索推荐 算法
Java 大视界 -- Java 大数据在智能教育学习社区用户互动分析与社区活跃度提升中的应用(274)
本文系统阐述 Java 大数据技术在智能教育学习社区中的深度应用,涵盖数据采集架构、核心分析算法、活跃度提升策略及前沿技术探索,为教育数字化转型提供完整技术解决方案。
|
Java 数据库连接 API
互联网大厂校招 JAVA 工程师笔试题解析及常见考点分析
本文深入解析互联网大厂校招Java工程师笔试题,涵盖基础知识(数据类型、流程控制)、面向对象编程(类与对象、继承与多态)、数据结构与算法(数组、链表、排序算法)、异常处理、集合框架、Java 8+新特性(Lambda表达式、Stream API)、多线程与并发、IO与NIO、数据库操作(JDBC、ORM框架MyBatis)及Spring框架基础(IoC、DI、AOP)。通过技术方案讲解与实例演示,助你掌握核心考点,提升解题能力。
591 2
|
传感器 分布式计算 安全
Java 大视界 -- Java 大数据在智能安防入侵检测系统中的多源数据融合与分析技术(171)
本文围绕 Java 大数据在智能安防入侵检测系统中的应用展开,剖析系统现状与挑战,阐释多源数据融合及分析技术,结合案例与代码给出实操方案,提升入侵检测效能。
|
缓存 安全 Java
【高薪程序员必看】万字长文拆解Java并发编程!(3-1):并发共享问题的解决与分析
活锁:多个线程相互影响对方退出同步代码块的条件而导致线程一直运行的情况。例如,线程1的退出条件是count=5,而线程2和线程3在其代码块中不断地是count进行自增自减的操作,导致线程1永远运行。内存一致性问题:由于JIT即时编译器对缓存的优化和指令重排等造成的内存可见性和有序性问题,可以通过synchronized,volatile,并发集合类等机制来解决。这里的线程安全是指,多个线程调用它们同一个实例的方法时,是线程安全的,但仅仅能保证当前调用的方法是线程安全的,不同方法之间是线程不安全的。
289 0