一、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); } } 复制代码
执行上述代码,输出结果如下
使用 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); } } 复制代码
执行上述代码,输出结果如下:
设置的精度既小数点的位数比原来小会报错。设置为5,会自动补上0,再次执行测试输出结果如下:
BigDecimal支持的舍入方式有很多中,向上取整,向下取整,四舍五入等
@Test public void testChangeScale(){ BigDecimal bigDecimal = new BigDecimal("12138.121"); BigDecimal res = bigDecimal.setScale(2, BigDecimal.ROUND_HALF_UP); System.out.println(res); } 复制代码
12138.128
测试其他舍入方式
除法运算,除不尽出现异常问题
除不尽,既无限循环的问题
@Test public void testDivideException(){ BigDecimal d1 = new BigDecimal(10); BigDecimal d2 = new BigDecimal(3); System.out.println(d1.divide(d2)); } 复制代码
@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)); } 复制代码
指定精度和舍入方式
总结,使用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)); } 复制代码
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)); } } 复制代码
执行上述代码,输出结果如下:
在解析精度较小的时间时出现了报错。
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(); } }); } 复制代码
执行上述代码,输出结果如下:
控制台中输出 false,说明在多线程的转换下,字符串已经变化了。
解决 SimpleDateFormat 是线程不安全的方式有:
- 定义为一个局部变量,局部变量不会受多线程的影响
- 使用 ThreadLocal 可以保存各自线程中共独立的数据,互相不会收到干扰,但是需要维护 ThreadLocal,,不推荐使用
- 使用关键字 synchronizez,使用锁来保证独立性,资源开销大,不推荐使用