Jtoss Jtoss
首页
  • 数据结构与算法

    • 数据结构与算法 - 概述
    • 数据结构与算法 - 复杂度分析
    • 数据结构 - 线性表
    • 算法 - 常见排序算法
  • 代码规范

    • 代码简洁之道
    • 阿里巴巴开发手册
    • 谷歌Java编程风格指南
  • 设计模式

    • 编写高质量代码概述
    • 面向对象
    • 设计原则
    • 设计模式-创建型
    • 设计模式-结构型
    • 设计模式-行为型(上)
    • 设计模式-行为型(下)
    • 浅析框架源码中的设计模式
    • 业务框架实战案例
  • MySQL 基础

    • MySQL - 数据库设计规范
    • MySQL - 必知必会
  • MySQL 进阶

    • MySQL - 基础架构
    • MySQL - InnoDB存储引擎
    • MySQL - InnoDB缓冲池
    • MySQL - 事务与锁
    • MySQL - 索引
    • MySQL - 查询执行计划
    • MySQL - 性能优化
  • Redis 系列

    • Redis入门 - 基础相关
    • Redis进阶 - 数据结构
    • Redis进阶 - 持久化RDB和AOF
    • Redis进阶 - 事件机制
    • Redis进阶 - 事务
    • Redis进阶 - 高可用高可扩展
    • Redis进阶 - 缓存问题
    • Redis进阶 - 性能调优
  • Java 基础

    • Java 基础 - 知识点
    • Java 基础 - 面向对象
    • Java 基础 - Q/A
  • Java 进阶 - 集合框架

    • Java 集合框架详解
  • Java 进阶 - 多线程与并发

    • Java 并发 - 理论基础
    • Java 并发 - 线程基础
    • Java 并发 - 各种锁
    • Java 并发 - 关键字 volatile
    • Java 并发 - 关键字 synchronized
    • JUC - CAS与原子操作
    • JUC - 锁核心类AQS
    • JUC - 锁接口和类简介
    • JUC - 并发容器简介
    • JUC - 通信工具类
    • JUC - Fork-Join框架
    • JUC - 线程池
  • Java 进阶 - JVM

    • JVM - 概述
    • JVM - 类加载机制
    • JVM - 内存结构
    • JVM - 垃圾回收机制
    • JVM - 性能调优
  • Maven系列

    • Maven基础知识
    • Maven项目构建
    • Maven多模块配置
  • Spring 框架

    • Spring 框架 - 框架介绍
    • Spring 框架 - IOC详解
    • Spring 框架 - AOP详解
    • Spring 框架 - SpringMVC详解
  • Spring Boot 系列

    • Spring Boot - 开发入门
    • Spring Boot - 接口相关
  • Spring Cloud 系列
  • Mybatis 系列

    • Mybatis - 总体框架设计
    • Mybatis - 初始化基本过程
    • Mybatis - sqlSession执行过程
    • Mybatis - 插件机制
    • Mybatis - 事务管理机制
    • Mybatis - 缓存机制
  • 业务常见问题

    • Java 业务开发常见错误(一)
    • Java 业务开发常见错误(二)
    • Java 业务开发常见错误(三)
    • Java 业务开发常见错误(四)
    • Java 业务开发常见错误(五)
    • Java 业务开发常见错误(六)
  • IDEA系列

    • IDEA 2021开发环境配置
    • IDEA 快捷键
  • Git系列

    • git status中文乱码
  • 其他

    • Typora+Picgo 自动上传图片
    • hsdis 和 jitwatch
  • 实用技巧
  • 收藏
  • 摄影
  • 学习
  • 标签
  • 归档

Jason Huang

后端程序猿
首页
  • 数据结构与算法

    • 数据结构与算法 - 概述
    • 数据结构与算法 - 复杂度分析
    • 数据结构 - 线性表
    • 算法 - 常见排序算法
  • 代码规范

    • 代码简洁之道
    • 阿里巴巴开发手册
    • 谷歌Java编程风格指南
  • 设计模式

    • 编写高质量代码概述
    • 面向对象
    • 设计原则
    • 设计模式-创建型
    • 设计模式-结构型
    • 设计模式-行为型(上)
    • 设计模式-行为型(下)
    • 浅析框架源码中的设计模式
    • 业务框架实战案例
  • MySQL 基础

    • MySQL - 数据库设计规范
    • MySQL - 必知必会
  • MySQL 进阶

    • MySQL - 基础架构
    • MySQL - InnoDB存储引擎
    • MySQL - InnoDB缓冲池
    • MySQL - 事务与锁
    • MySQL - 索引
    • MySQL - 查询执行计划
    • MySQL - 性能优化
  • Redis 系列

    • Redis入门 - 基础相关
    • Redis进阶 - 数据结构
    • Redis进阶 - 持久化RDB和AOF
    • Redis进阶 - 事件机制
    • Redis进阶 - 事务
    • Redis进阶 - 高可用高可扩展
    • Redis进阶 - 缓存问题
    • Redis进阶 - 性能调优
  • Java 基础

    • Java 基础 - 知识点
    • Java 基础 - 面向对象
    • Java 基础 - Q/A
  • Java 进阶 - 集合框架

    • Java 集合框架详解
  • Java 进阶 - 多线程与并发

    • Java 并发 - 理论基础
    • Java 并发 - 线程基础
    • Java 并发 - 各种锁
    • Java 并发 - 关键字 volatile
    • Java 并发 - 关键字 synchronized
    • JUC - CAS与原子操作
    • JUC - 锁核心类AQS
    • JUC - 锁接口和类简介
    • JUC - 并发容器简介
    • JUC - 通信工具类
    • JUC - Fork-Join框架
    • JUC - 线程池
  • Java 进阶 - JVM

    • JVM - 概述
    • JVM - 类加载机制
    • JVM - 内存结构
    • JVM - 垃圾回收机制
    • JVM - 性能调优
  • Maven系列

    • Maven基础知识
    • Maven项目构建
    • Maven多模块配置
  • Spring 框架

    • Spring 框架 - 框架介绍
    • Spring 框架 - IOC详解
    • Spring 框架 - AOP详解
    • Spring 框架 - SpringMVC详解
  • Spring Boot 系列

    • Spring Boot - 开发入门
    • Spring Boot - 接口相关
  • Spring Cloud 系列
  • Mybatis 系列

    • Mybatis - 总体框架设计
    • Mybatis - 初始化基本过程
    • Mybatis - sqlSession执行过程
    • Mybatis - 插件机制
    • Mybatis - 事务管理机制
    • Mybatis - 缓存机制
  • 业务常见问题

    • Java 业务开发常见错误(一)
    • Java 业务开发常见错误(二)
    • Java 业务开发常见错误(三)
    • Java 业务开发常见错误(四)
    • Java 业务开发常见错误(五)
    • Java 业务开发常见错误(六)
  • IDEA系列

    • IDEA 2021开发环境配置
    • IDEA 快捷键
  • Git系列

    • git status中文乱码
  • 其他

    • Typora+Picgo 自动上传图片
    • hsdis 和 jitwatch
  • 实用技巧
  • 收藏
  • 摄影
  • 学习
  • 标签
  • 归档
  • Java 基础

    • Java 基础 - 谈谈 Java 平台
    • Java 基础 - 知识点
    • Java 基础 - 面向对象
    • Java 基础 - Q/A
    • 扩展 - int 和 Integer
    • 扩展 - final、finally、finalize
    • 扩展 - String、StringBuilder、StringBuffer
    • 扩展 - Exception 和 Error
    • 扩展 - 引用
    • 扩展 - 数值计算问题
      • 1. “危险”的 Double
      • 2. 使用 BigDecimal 类型代替 Double
      • 3. 浮点数舍入和格式化的方式
      • 4. 用equals做判等,就一定对吗?
    • 扩展 - 反射和动态代理
  • Java 进阶 - 集合框架

  • Java 进阶 - 多线程与并发

  • Java 进阶 - JVM

  • Java 进阶 - 版本特性

  • Java
  • Java 基础
Jason
目录

扩展 - 数值计算问题

# 数值计算问题

注意精度、舍入和溢出问题

# 1. “危险”的 Double

public class DoubleTest {
    public static void main(String[] args) {
        System.out.println(0.1+0.2);    // 0.30000000000000004
        System.out.println(1.0-0.8);     // 0.19999999999999996
        System.out.println(4.015*100);   // 401.49999999999994
        System.out.println(123.3/100);   // 1.2329999999999999

        double amount1 = 2.15;
        double amount2 = 1.10;
        if (amount1 - amount2 == 1.05) {
            System.out.println("OK");
        } else {
            System.out.println("NO " + (amount1 - amount2)); // NO 1.0499999999999998
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

计算机是以二进制存储数值的。0.1 的二进制表示为 0.0 0011 0011 0011… (0011 无限循环),再转换为十进制就是 0.1000000000000000055511151231257827021181583404541015625。对于计算机而言,0.1 无法精确表达,这是浮点数计算造成精度损失的根源。

如果大量使用 double 来作大量的金钱计算,最终损失的精度就是大量的资金出入。比如,每天有一百万次交易,每次交易都差一分钱,一个月下来就差 30 万

# 2. 使用 BigDecimal 类型代替 Double

BigDecimal的几个坑

import java.math.BigDecimal;

public class DecimalTest {
    public static void main(String[] args) {
        System.out.println(new BigDecimal(0.1).add(new BigDecimal(0.2)));        // 0.3000000000000000166533453693773481063544750213623046875
        System.out.println(new BigDecimal(1.0).subtract(new BigDecimal(0.8))); // 0.1999999999999999555910790149937383830547332763671875
        System.out.println(new BigDecimal(4.015).multiply(new BigDecimal(100))); // 401.49999999999996802557689079549163579940795898437500
        System.out.println(new BigDecimal(123.3).divide(new BigDecimal(100))); // 1.232999999999999971578290569595992565155029296875
    }
}
1
2
3
4
5
6
7
8
9
10

运算结果还是不精确,只不过精度高了而已。

计算浮点数避坑第一原则:

  1. 使用BigDecimal表示和计算浮点数
  2. 务必使用字符串的构造方法来初始化BigDecimal
import java.math.BigDecimal;

public class DecimalTest {
    public static void main(String[] args) {
        System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2")));        // 0.3      
        System.out.println(new BigDecimal("1.0").subtract(new BigDecimal("0.8")));   // 0.2
        System.out.println(new BigDecimal("4.015").multiply(new BigDecimal("100"))); // 401.500
        System.out.println(new BigDecimal("123.3").divide(new BigDecimal("100")));   // 1.233
        
        System.out.println(BigDecimal.valueOf(0.1).add(BigDecimal.valueOf(0.2)));        // 0.3
        System.out.println(BigDecimal.valueOf(1.0).subtract(BigDecimal.valueOf(0.8)));   // 0.2
        System.out.println(BigDecimal.valueOf(4.015).multiply(BigDecimal.valueOf(100))); // 401.500
        System.out.println(BigDecimal.valueOf(123.3).divide(BigDecimal.valueOf(100)));   // 1.233
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

BigDecimal的scale 和 precision 概念:scale 表示小数点右边的位数;precision表示精度,即有效数字的长度

new BigDecimal(Double.toString(100)) 得到的 BigDecimal 的 scale=1、precision=4; new BigDecimal(“100”) 得到的 BigDecimal 的 scale=0、precision=3

对于 BigDecimal 乘法操作,返回值的 scale 是两个数的 scale 相加


private static void testScale() {
    BigDecimal bigDecimal1 = new BigDecimal("100");
    BigDecimal bigDecimal2 = new BigDecimal(String.valueOf(100d));
    BigDecimal bigDecimal3 = new BigDecimal(String.valueOf(100));
    BigDecimal bigDecimal4 = BigDecimal.valueOf(100d);
    BigDecimal bigDecimal5 = new BigDecimal(Double.toString(100));

    print(bigDecimal1); //scale 0 precision 3 result 401.500
    print(bigDecimal2); //scale 1 precision 4 result 401.5000
    print(bigDecimal3); //scale 0 precision 3 result 401.500
    print(bigDecimal4); //scale 1 precision 4 result 401.5000
    print(bigDecimal5); //scale 1 precision 4 result 401.5000
}

private static void print(BigDecimal bigDecimal) {
    log.info("scale {} precision {} result {}", bigDecimal.scale(), bigDecimal.precision(), bigDecimal.multiply(new BigDecimal("4.015")));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 3. 浮点数舍入和格式化的方式

double num1 = 3.35;  // 相当于:3.350000000000000088817841970012523233890533447265625
float num2 = 3.35f;  // 相当于:3.349999904632568359375
System.out.println(String.format("%.1f", num1));//四舍五入 3.4
System.out.println(String.format("%.1f", num2)); //       3.3
1
2
3
4

String.format 采用四舍五入的方式进行舍入,取 1 位小数,所以记过为3.4 和 3.3 String.format 采用HALF_UP模式

使用其他舍入方式格式化字符串,可以使用DecimalFormat


double num1 = 3.35; // 相当于:3.350000000000000088817841970012523233890533447265625
float num2 = 3.35f; // 相当于:3.349999904632568359375
DecimalFormat format = new DecimalFormat("#.##");
format.setRoundingMode(RoundingMode.DOWN);
System.out.println(format.format(num1));  // 3.35
format.setRoundingMode(RoundingMode.DOWN); 
System.out.println(format.format(num2));  // 3.34  期望是3.35
1
2
3
4
5
6
7
8

因此,即使通过 DecimalFormat 来精确控制舍入方式,double 和 float 的问题也可能产生意想不到的结果 所以浮点数避坑第二原则:浮点数的字符串格式化也要通过 BigDecimal 进行。

比如下面这段代码,使用 BigDecimal 来格式化数字 3.35,分别使用向下舍入和四舍五入方式取 1 位小数进行格式化

BigDecimal num1 = new BigDecimal("3.35");
BigDecimal num2 = num1.setScale(1, BigDecimal.ROUND_DOWN);  // 3.3
System.out.println(num2);
BigDecimal num3 = num1.setScale(1, BigDecimal.ROUND_HALF_UP); // 3.4
System.out.println(num3);
1
2
3
4
5

# 4. 用equals做判等,就一定对吗?

System.out.println(new BigDecimal("1.0").equals(new BigDecimal("1")))  // false
1

equals 比较的是 BigDecimal 的 value 和 scale,1.0 的 scale 是 1,1 的 scale 是 0,所以结果一定是 false 如果我们希望只比较 BigDecimal 的 value,可以使用 compareTo 方法

System.out.println(new BigDecimal("1.0").compareTo(new BigDecimal("1"))==0);
1
Set<BigDecimal> hashSet1 = new HashSet<>();
hashSet1.add(new BigDecimal("1.0"));
System.out.println(hashSet1.contains(new BigDecimal("1")));//返回false
1
2
3

解决方法1:使用 TreeSet 替换 HashSet。TreeSet 不使用 hashCode 方法,也不使用 equals 比较元素,而是使用 compareTo 方法

Set<BigDecimal> treeSet = new TreeSet<>();
treeSet.add(new BigDecimal("1.0"));
System.out.println(treeSet.contains(new BigDecimal("1")));//返回true
1
2
3

解决方法2:把 BigDecimal 存入 HashSet 或 HashMap 前,先使用 stripTrailingZeros 方法去掉尾部的零,比较的时候也去掉尾部的 0,确保 value 相同的 BigDecimal,scale 也是一致的

Set<BigDecimal> hashSet2 = new HashSet<>();
hashSet2.add(new BigDecimal("1.0").stripTrailingZeros());
System.out.println(hashSet2.contains(new BigDecimal("1.000").stripTrailingZeros()));//返回true
1
2
3
#Java
上次更新: 2022-12-29
扩展 - 引用
扩展 - 反射和动态代理

← 扩展 - 引用 扩展 - 反射和动态代理→

最近更新
01
开始
01-09
02
AI工具分享
01-09
03
AI 导读
01-07
更多文章>
Theme by Vdoing | Copyright © 2022-2025 Jason Huang | 闽ICP备2025088096号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式