你有没有掉进去过这些 BigDecimal 和 DateFormatter 的“陷阱“

简介: 你有没有掉进去过这些 BigDecimal 和 DateFormatter 的“陷阱“

一、BigDecimal

使用 IDEA 创建一个 Maven 项目 calculate-date-traps 并导入 Junit 依赖。

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
复制代码

在进行计费时使用 Double 和 Float 类型计算经常会出现丢失精度的情况,在 test 包下新建一个测试类 ScaleLostTest。

public class ScaleLostTest {
    @Test
    public void testDoubleLostScale(){
        double alpha = 1;
        double bravo = 20.2;
        double charlie = 400.03;
        System.out.println(alpha + bravo + charlie);
    }
}
复制代码

执行上述代码,输出结果如下

image.png

使用 Double 类型进行精确运算出现了精度问题。

代码中所使用的数最终都会转换成二进制,而浮点类型的数转换成二进制并不是精确地二进制,只能是最接近的二进制,这是应为浮点数是由指数和尾数两部分组成,所以在浮点数计算的过程中会出现丢失精度的问题。

如果恰巧计算结果的二进制能和十进制准确转换那么自然也就不会出现丢失精度的问题了。

浮点数并不适合进行精确计算而更适合科学计算。而 BigDecimal 类型的核心就是精度

在 test 包下新建一个测试类 BigDecimalTest

public class BigDecimalTest {
    @Test
    public void testScaleException(){
        BigDecimal bigDecimal = new BigDecimal("12138.121");
        BigDecimal res = bigDecimal.setScale(2);
        System.out.println(res);
    }
}
复制代码

执行上述代码,输出结果如下:

image.png

设置的精度既小数点的位数比原来小会报错。设置为5,会自动补上0,再次执行测试输出结果如下:

image.png

BigDecimal支持的舍入方式有很多中,向上取整,向下取整,四舍五入等

@Test
public void testChangeScale(){
    BigDecimal bigDecimal = new BigDecimal("12138.121");
    BigDecimal res = bigDecimal.setScale(2, BigDecimal.ROUND_HALF_UP);
    System.out.println(res);
}
复制代码

image.png

12138.128

image.png

测试其他舍入方式

除法运算,除不尽出现异常问题

除不尽,既无限循环的问题

@Test
public void testDivideException(){
    BigDecimal d1 = new BigDecimal(10);
    BigDecimal d2 = new BigDecimal(3);
    System.out.println(d1.divide(d2));
}
复制代码

image.png

@Test
public void testSolveDivideException(){
    BigDecimal d1 = new BigDecimal(10);
    BigDecimal d2 = new BigDecimal(3);
    System.out.println(d1.divide(d2, 2, BigDecimal.ROUND_HALF_UP));
}
复制代码

image.png

指定精度和舍入方式

总结,使用BigDecimal一定要指定保留小数点的位数和指定的舍入方式

精度问题导致结果比较不一致

@Test
public void testCompare(){
    BigDecimal d1 = new BigDecimal("0");
    BigDecimal d2 = new BigDecimal("0.0");
    System.out.println(d1.equals(d2));
    System.out.println(d1.compareTo(d2));
}
复制代码

image.png

equals() 方法,精度不同直接返回 false

二、DateFormatter

SimpleDateFormat 是一个以与语言环境有关的方式来格式化和解析日期的具体类。它允许进行格式化(日期 -> 文本)、解析(文本 -> 日期)和规范化。使用SimpleDateFormat的format方法,将一个Date类型转化成String类型,并且可以指定输出格式。

在使用 SimpleDateFormat 时可以解析大于或者等于定义的时间精度,但不能解析小于它定义的时间精度,并且 SimpleDateFormat 是线程不安全的,在多线程环境下操作会抛出异常。

SimpleDateFormat 只能解析大于或者等于定义的时间精度

新增一个测试类 DateFormatterTest,新增测试方法 testFormatterException 测试 SimpleDateFormat 解析小于定义的时间精度会出现什么问题。

public class DateFormatterTest {
    @Test
    public void testFormatterException() throws ParseException {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
        String date_01 = "2022-06-09 23:51:00";
        String date_02 = "2022-06";
        System.out.println(simpleDateFormat.parse(date_01));
        System.out.println((simpleDateFormat).parse(date_02));
    }
}
复制代码

执行上述代码,输出结果如下:

image.png

在解析精度较小的时间时出现了报错。

SimpleDateFormat 线程不安全

SimpleDateFormat 是线程不安全的,这是因为维护了一个 全局的 Calandar 对象的存在,Calandar 中存储的值会被共享,导致线程不安全。

新增一个测试方法 testFormatterThreadSafety,测试在多线程情况下,对一个时间格式的字符串转换为时间后再转换为字符串,并比较这两个字符串是否相等,可以验证线程是否安全。

@Test
public void testFormatterThreadSafety(){
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    // 定义一个线程池
    ThreadPoolExecutor executor = new ThreadPoolExecutor(5,50,
            60, TimeUnit.SECONDS, new LinkedBlockingDeque<>(100));
    while (true){
        executor.execute(() -> {
            String date_01 = "2022-06-09 23:51:00";
            try {
                // 转换为 date 类型
                Date praseDate = simpleDateFormat.parse(date_01 );
                // 在转换为 string 类型
                String dateStr = simpleDateFormat.format(praseDate);
                // 比较
                System.out.println(date_01.equals(dateStr));
            } catch (Exception e){
                e.printStackTrace();
            }
        });
    }
复制代码

执行上述代码,输出结果如下:

image.png

控制台中输出 false,说明在多线程的转换下,字符串已经变化了。

解决 SimpleDateFormat 是线程不安全的方式有:

  • 定义为一个局部变量,局部变量不会受多线程的影响
  • 使用 ThreadLocal 可以保存各自线程中共独立的数据,互相不会收到干扰,但是需要维护 ThreadLocal,,不推荐使用
  • 使用关键字 synchronizez,使用锁来保证独立性,资源开销大,不推荐使用


相关文章
|
人工智能 自然语言处理 安全
AIGC是引领政府治理革新的强大引擎
【1月更文挑战第4天】AIGC是引领政府治理革新的强大引擎
360 1
AIGC是引领政府治理革新的强大引擎
|
人工智能
虚拟键盘AI
本文提供了一个虚拟键盘AI项目的详细代码实现,包括链接摄像头、手势识别、绘制键盘、确定选中字母以及使用`pynput`库模拟真实键盘输入的步骤,并附有环境配置指南。
虚拟键盘AI
|
存储 缓存 监控
一种基于动态代理的通用研发提效解决方案
作为一名研发人员,除了业务开发之外,研发提效是一个永恒的话题,而女娲正是这一话题下进行的一次全面的剖析和实践。
110087 26
|
11月前
|
数据采集 供应链 API
Python爬虫与1688图片搜索API接口:深度解析与显著收益
在电子商务领域,数据是驱动业务决策的核心。阿里巴巴旗下的1688平台作为全球领先的B2B市场,提供了丰富的API接口,特别是图片搜索API(`item_search_img`),允许开发者通过上传图片搜索相似商品。本文介绍如何结合Python爬虫技术高效利用该接口,提升搜索效率和用户体验,助力企业实现自动化商品搜索、库存管理优化、竞品监控与定价策略调整等,显著提高运营效率和市场竞争力。
502 3
|
机器学习/深度学习 算法 数据挖掘
【Python机器学习专栏】关联规则学习:Apriori算法详解
【4月更文挑战第30天】Apriori算法是一种用于关联规则学习的经典算法,尤其适用于购物篮分析,以发现商品间的购买关联。该算法基于支持度和置信度指标,通过迭代生成频繁项集并提取满足阈值的规则。Python中可借助mlxtend库实现Apriori,例如处理购物篮数据,设置支持度和置信度阈值,找出相关规则。
669 2
|
Web App开发 存储 缓存
三、《图解HTTP》- 报文内的 HTTP信息
三、《图解HTTP》- 报文内的 HTTP信息
293 0
三、《图解HTTP》- 报文内的 HTTP信息
【qt】QListWidget 组件2
【qt】QListWidget 组件
167 0
|
算法 数据挖掘
R语言使用混合模型GMM进行聚类
R语言使用混合模型GMM进行聚类
|
编解码 计算机视觉
【开源视频联动物联网平台】帧率、码率和分辨率
【开源视频联动物联网平台】帧率、码率和分辨率
791 0