写点什么

大厂都是怎么用 Java8 代替 SimpleDateFormat?

发布于: 2020 年 11 月 25 日

1 SimpleDateFormat 之坑

1.1 格式化

1.1.1 案例

初始化一个 Calendar,设置日期 2020 年 12 月 29 日



日志



这是由于混淆 SimpleDateFormat 的各种格式化模式:

  • 小写 y 是年

  • 大写 Y 是 week year,即所在的周属于哪一年

一年第一周的判断方式

getFirstDayOfWeek()开始,完整的 7 天,并且包含那一年至少getMinimalDaysInFirstWeek()天。

该计算方式和区域相关,对zh_CN区域,2020 年第一周条件:从周日开始的完整 7 天,2020 年包含 1 天即 可。显然,2019 年 12 月 27 日周日到 2020 年 1 月 2 日周六是 2020 年第一周,得出的 week year 就是 2021 年。

若把区域改为法国

Locale.setDefault(Locale.FRANCE);
复制代码


则 week yeay 就还是 2020 年,因为一周的第一天从周一开始算,2020 年的第一周是 2019 年 12 月 28 日周一开始,27 日还是属于去年:



小结

无特殊需求,针对年份的日期格式化,应该一律使用 “y” 而非 “Y”。

线程安全问题

使用一个 100 线程的线程池,循环 20 次把时间格式化任务提交到线程池处理,每个任务中又循环 10 次解析 2020-01-01 11:12:13 这样一个时间表示:

运行程序后大量报错,即使没有报错的输出结果也不正常,比如 2020 年解析成 57728 年



SimpleDateFormat 用于定义解析和格式化日期时间的模式。看起来是一次性工作,应该复用,但它的解析和格式化操作都非线程安全。

分析源码



SimpleDateFormat 继承自 DateFormat,DateFormat 有字段 Calendar;

SimpleDateFormat#parse调用CalendarBuilder#establish构建 Calendar



establish 方法内部先清空 Calendar 再构建 Calendar,整个操作没有加锁。



显然,若使用线程池调用 parse,即多线程并发操作一个 Calendar,就可能会产生一个线程还没来得及处理 Calendar 就被另一个线程清空。format 方法同理,不再赘述。因此只能在同一个线程复用 SimpleDateFormat,

解决方案

通过 ThreadLocal 来存放 SimpleDateFormat:

  • 日志输出全部正确



1.2 当需要解析的字符串和格式不匹配,SimpleDateFormat 还是能得到结果

案例

使用 yyyyMM 解析 20160901 字符串:



  • 居然输出 2112 年,这是因为把 1111 当成月份



对于 SimpleDateFormat 的这些坑,使用 Java 8 中的 DateTimeFormatter 即可避免。

2 Java 8 中的 DateTimeFormatter

2.1 格式化字符串

首先,使用 DateTimeFormatterBuilder 定义格式化字符串,无需死记大写 Y 还是小写 y,大写 M 还是小写 m:



2.2 线程安全

可定义为 static 使用

2.3 待解析字符串和格式不匹配时就报错



  • 日志

2020/11/11 11:11:11.789Exception in thread "main" java.time.format.DateTimeParseException: Text '20201111' could not be parsed at index 0	at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1949)	at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1777)	at org.javaedge.time.commonmistakes.datetime.dateformat.CommonMistakesApplication.better(CommonMistakesApplication.java:96)	at org.javaedge.time.commonmistakes.datetime.dateformat.CommonMistakesApplication.main(CommonMistakesApplication.java:47)
复制代码


3 Java8 计算日期时间

有人喜欢使用时间戳进行计算,比如希望得到当前时间后 30 天:把new Date().getTime得到的时间戳加 30 天对应毫秒数



得到的日期居然比当前日期还要早,根本不是后 30 天



因为 int 发生了溢出!。

应将 30 改为 30L,使其为 long:



正确输出



Java 8 前代码,建议使用 Calendar:



使用 Java 8 的日期时间类型,可以直接进行各种计算,更加简洁和方便:



对日期时间做计算操作,日期时间 API 会比 Calendar 功能强大很多。

3.1 minus/plus 直接对日期加减



3.2 with 快捷时间调节

  • TemporalAdjusters.firstDayOfMonth 得到当前月的第一天

  • TemporalAdjusters.firstDayOfYear()得到当前年的第一天

  • TemporalAdjusters.previous(DayOfWeek.SATURDAY)得到上一个周六

  • TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY)得到本月最后一个周五



3.3 使用 lambda 自定义的时间调整

为当前时间增加 100 天以内的随机天数:



判断日期是否符合某个条件



query 查询是否匹配条件



使用 Java 8 操作和计算日期时间虽然方便,但计算两个日期差时可能会踩坑:Java 8 中有一个专门的类 Period 定义了日期间隔,通过 Period.between 得到了两个 LocalDate 的差,返回的是两个日期差几年零几月零几天。

如果希望得知两个日期之间差几天,直接调用 Period 的 getDays()方法得到的只是最后的“零几天”,而不是算总的间隔天数。

比如,计算 2020 年 12 月 12 日和 2020 年 10 月 1 日的日期间隔,很明显日期差是 2 个月零 11 天,但获取 getDays 方法得到的结果只是 11 天,而不是 72 天:



可使用 ChronoUnit.DAYS.between 解决这个问题:



点击并拖拽以移动

4 总结

也许你认为java.util.Date类似于新 API 中的LocalDateTime。其实不是,虽然它们都没时区概念

  • java.util.Date 类是因为使用 UTC 表示,所以没有时区概念,其本质是时间戳

  • LocalDateTime,严格上可以认为是一个日期时间的表示,而不是一个时间点

因此,在把 Date 转换为 LocalDateTime 的时候,需要通过 Date 的 toInstant 方法得到一个 UTC 时间戳进行转换,并需要提供当前的时区,这样才能把 UTC 时间转换为本地日期时间(的表示)。反过来,把 LocalDateTime 的时间表示转换为 Date 时,也需要提供时区,用于指定是哪个时区的时间表示,也就是先通过 atZone 方法把 LocalDateTime 转换为 ZonedDateTime,然后才能获得 UTC 时间戳:

Date in = new Date();LocalDateTime ldt = LocalDateTime.ofInstant(in.toInstant(), ZoneId.systemDefault());Date out = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());
复制代码


点击并拖拽以移动

有人说新 API 很麻烦,还需要考虑时区,真麻烦。但并非因为 API 强行设计繁琐,而是 UTC 时间要变为当地时间,必须考虑时区!


---------------------

为什么阿里巴巴的程序员成长速度这么快?

霸榜GitHub的Offer来了原理篇+框架篇,开放分享;

50W年薪程序员需要的技术栈分析

看完三件事❤️

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

关注公众号 『 Java 斗帝 』,不定期分享原创知识。

同时可以期待后续文章 ing🚀


用户头像

还未添加个人签名 2020.09.07 加入

还未添加个人简介

评论

发布
暂无评论
大厂都是怎么用Java8代替SimpleDateFormat?