| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 游戏开发 -> Java8 日期时间类 -> 正文阅读 |
|
[游戏开发]Java8 日期时间类 |
????????在 Java 8 之前,我们处理日期时间需求时,使用 Date、Calender 和 SimpleDateFormat,来声明时间戳、使用日历处理日期和格式化解析日期时间。但是,这些类的 API 的缺点比较明显,比如可读性差、易用性差、使用起来冗余繁琐,还有线程安全问题。 ????????因此,Java 8 推出了新的日期时间类。每一个类功能明确清晰、类之间协作简单、API 定义清晰不踩坑,API 功能强大无需借助外部工具类即可完成操作,并且线程安全。 1、初始化日期时间1.1 普通初始化????????我们先从日期时间的初始化看起。如果要初始化一个 2019 年 12 月 31 日 11 点 12 分 13 秒这样的时间,可以使用下面的两行代码吗?
可以看到,输出的时间是 3029 年 1 月 31 日 11 点 12 分 13 秒:
错误:年应该是和 1900 的差值,月应该是从 0 到 11 而不是从 1 到 12。
1.2 国际化初始化????????当有国际化需求时,需要使用 Calendar 类来初始化时间。使用 Calendar 改造之后,初始化时年参数直接使用当前年即可,不过月需要注意是从 0 到 11。当然,你也可以直接使用 Calendar.DECEMBER 来初始化月份,更不容易犯错。为了说明时区的问题,分别使用当前时区和纽约时区初始化了两次相同的日期:
????????输出显示了两个时间,说明时区产生了作用。但,我们更习惯年 / 月 / 日 时: 分: 秒这样的日期时间格式,对现在输出的日期格式还不满意:
那,时区的问题是怎么回事,又怎么格式化需要输出的日期时间呢? 1.3“恼人”的时区问题????????我们知道,全球有 24 个时区,同一个时刻不同时区(比如中国上海和美国纽约)的时间是不一样的。 ????????对于需要全球化的项目,如果初始化时间时没有提供时区,那就不是一个真正意义上的时间,只能认为是我看到的当前时间的一个表示。 关于 Date 类,我们要有两点认识: 一是,Date 并无时区问题,世界上任何一台计算机使用 new Date() 初始化得到的时间都一样。因为,Date 中保存的是 UTC 时间,UTC 是以原子钟为基础的统一时间,不以太阳参照计时,并无时区划分。 二是,Date 中保存的是一个时间戳,代表的是从 1970 年 1 月 1 日 0 点(Epoch 时间)到现在的毫秒数。尝试输出 Date(0):
我得到的是 1970 年 1 月 1 日 8 点。 因为我机器当前的时区是中国上海,相比 UTC 时差 +8 小时:
????????对于国际化(世界各国的人都在使用)的项目,处理好时间和时区问题首先就是要正确保存日期时间。 这里有两种保存方式: 方式一,以 UTC 保存,保存的时间没有时区属性,是不涉及时区时间差问题的世界统一时间。我们通常说的时间戳,或 Java 中的 Date 类就是用的这种方式,这也是推荐的方式。 方式二,以字面量保存,比如年 / 月 / 日 时: 分: 秒,一定要同时保存时区信息。只有有了时区信息,我们才能知道这个字面量时间真正的时间点,否则它只是一个给人看的时间表示,只在当前时区有意义。Calendar 是有时区概念的,所以我们通过不同的时区初始化 Calendar,得到了不同的时间。 ????????正确保存日期时间之后,就是正确展示,即我们要使用正确的时区,把时间点展示为符合当前时区的时间表示。到这里,我们就能理解为什么会有所谓的“时间错乱”问题了。 接下来,再通过实际案例分析一下,从字面量解析成时间和从时间格式化为字面量这两类问题。 第一类是,对于同一个时间表示,比如 2020-01-02 22:00:00,不同时区的人转换成 Date 会得到不同的时间(时间戳):
????????可以看到,把 2020-01-02 22:00:00 这样的时间表示,对于当前的上海时区和纽约时区,转化为 UTC 时间戳是不同的时间:
????????这正是 UTC 的意义,并不是时间错乱。对于同一个本地时间的表示,不同时区的人解析得到的 UTC 时间一定是不同的,反过来不同的本地时间可能对应同一个 UTC。第二类问题是,格式化后出现的错乱,即同一个 Date,在不同的时区下格式化得到不同的时间表示。比如,在我的当前时区和纽约时区格式化 2020-01-02 22:00:00:
????????输出如下,我当前时区的 Offset(时差)是 +8 小时,对于 -5 小时的纽约,晚上 10 点对应早上 9 点:
????????因此,有些时候数据库中相同的时间,由于服务器的时区设置不同,读取到的时间表示不同。这不是时间错乱,正是时区发挥了作用,因为 UTC 时间需要根据当前时区解析为正确的本地时间。 ????????所以,要正确处理时区,在于存进去和读出来两方面: ????????存的时候,需要使用正确的当前时区来保存,这样 UTC 时间才会正确; ????????读的时候,也只有正确设置本地时区,才能把 UTC 时间转换为正确的当地时间。 ????????Java 8 推出了新的时间日期类 ZoneId、ZoneOffset、LocalDateTime、ZonedDateTime 和 DateTimeFormatter,处理时区问题更简单清晰。我们再用这些类配合一个完整的例子,来理解一下时间的解析和展示: ????????首先初始化上海、纽约和东京三个时区。我们可以使用 ZoneId.of 来初始化一个标准的时区,也可以使用 ZoneOffset.ofHours 通过一个 offset,来初始化一个具有指定时间差的自定义时区。对于日期时间表示,LocalDateTime 不带有时区属性,所以命名为本地时区的日期时间;而 ZonedDateTime=LocalDateTime+ZoneId,具有时区属性。 ????????因此,LocalDateTime 只能认为是一个时间表示,ZonedDateTime 才是一个有效的时间。在这里我们把 2020-01-02 22:00:00 这个时间表示,使用东京时区来解析得到一个 ZonedDateTime。使用 DateTimeFormatter 格式化时间的时候,可以直接通过 withZone 方法直接设置格式化使用的时区。 ????????最后,分别以上海、纽约和东京三个时区来格式化这个时间输出:
????????可以看到,相同的时区,经过解析存进去和读出来的时间表示是一样的(比如最后一行);而对于不同的时区,比如上海和纽约,最后输出的本地时间不同。+9 小时时区的晚上 10 点,对于上海是 +8 小时,所以上海本地时间是晚上 9 点;而对于纽约是 -5 小时,差 14 小时,所以是早上 8 点:
????????要正确处理国际化时间问题,推荐使用 Java 8 的日期时间类,即使用 ZonedDateTime 保存时间,然后使用设置了 ZoneId 的 DateTimeFormatter 配合 ZonedDateTime 进行时间格式化得到本地时间表示。这样的划分十分清晰、细化,也不容易出错。 2、日期时间格式化和解析????????每到年底,就有很多开发同学踩时间格式化的坑,比如“这明明是一个 2019 年的日期,怎么使用 SimpleDateFormat 格式化后就提前跨年了”。我们来重现一下这个问题:初始化一个Calendar,设置日期时间为 2019 年 12 月 29 日,使用大写的 YYYY 来初始SimpleDateFormat:
?得到的输出却是 2020 年 12 月 29 日:
?????????出现这个问题的原因在于,这位同学混淆了 SimpleDateFormat 的各种格式化模式。JDK 的文档中有说明:小写 y 是年,而大写 Y 是 week year,也就是所在的周属于哪一年。一年第一周的判断方式是,从 getFirstDayOfWeek() 开始,完整的 7 天,并且包含那一年至少 getMinimalDaysInFirstWeek() 天。这个计算方式和区域相关,对于当前 zh_CN 区域来说,2020 年第一周的条件是,从周日开始的完整 7 天,2020 年包含 1 天即可。显然,2019 年 12 月 29 日周日到 2020 年 1 月 4 日周六是 2020 年第一周,得出的 week year 就是 2020 年。如果把区域改为法国:
????????那么 week yeay 就还是 2019 年,因为一周的第一天从周一开始算,2020 年的第一周是 2019 年 12 月 30 日周一开始,29 日还是属于去年:
这个案例告诉我们,没有特殊需求,针对年份的日期格式化,应该一律使用 “y” 而非 “Y”。 除了格式化表达式容易踩坑外,SimpleDateFormat 还有两个著名的坑。 第一个坑是,定义的 static 的 SimpleDateFormat 可能会出现线程安全问题。 比如像这样,使用一个 100 线程的线程池,循环 20 次把时间格式化任务提交到线程池处理,每个任务中又循环 10 次解析 2020-01-01 11:12:13 这样一个时间表示:
????????运行程序后大量报错,且没有报错的输出结果也不正常,比如 2020 年解析成了 1212 年:SimpleDateFormat 的作用是定义解析和格式化日期时间的模式。这,看起来这是一次性的工作,应该复用,但它的解析和格式化操作是非线程安全的。我们来分析一下相关源码:SimpleDateFormat 继承了 DateFormat,DateFormat 有一个字段 Calendar;SimpleDateFormat 的 parse 方法调用 CalendarBuilder 的 establish 方法,来构建 Calendar;establish 方法内部先清空 Calendar 再构建 Calendar,整个操作没有加锁。显然,如果多线程池调用 parse 方法,也就意味着多线程在并发操作一个 Calendar,可能会产生一个线程还没来得及处理 Calendar 就被另一个线程清空了的情况:
format 方法也类似,可以自己分析。 ????????因此只能在同一个线程复用 SimpleDateFormat,比较好的解决方式是,通过 ThreadLocal 来存放 SimpleDateFormat:
????????第二个坑是,当需要解析的字符串和格式不匹配的时候,SimpleDateFormat 表现得很宽容,还是能得到结果。 比如,我们期望使用 yyyyMM 来解析 20160901 字符串:
居然输出了 2091 年 1 月 1 日,原因是把 0901 当成了月份,相当于 75 年:
对于 SimpleDateFormat 的这些坑,我们使用 Java 8 中的 DateTimeFormatter 就可以避过去。首先,使用 DateTimeFormatterBuilder 来定义格式化字符串,不用去记忆使用大写的 Y 还是小写的 Y,大写的 M 还是小写的 m:
????????其次,DateTimeFormatter 是线程安全的,可以定义为 static 使用; ????????最后,DateTimeFormatter 的解析比较严格,需要解析的字符串和格式不匹配时,会直接报错,而不会把 0901 解析为月份。 我们测试一下:
输出日志如下: 2
????????到这里我们可以发现,使用 Java 8 中的 DateTimeFormatter 进行日期时间的格式化和解析,显然更让人放心。那么,对于日期时间的运算,使用 Java 8 中的日期时间类会不会更简单呢? 3、日期时间的计算????????关于日期时间的计算,先和你说一个常踩的坑。 ????????有些同学喜欢直接使用时间戳进行时间计算,比如希望得到当前时间之后 30 天的时间,会这么写代码:直接把 new Date().getTime 方法得到的时间戳加 30 天对应的毫秒数,也就是 30 天 *1000 毫秒 *3600 秒 *24 小时:
得到的日期居然比当前日期还要早,根本不是晚 30 天的时间:
出现这个问题,其实是因为 int 发生了溢出。修复方式就是把 30 改为 30L,让其成为一个 long:
这样就可以得到正确结果了:
不难发现,手动在时间戳上进行计算操作的方式非常容易出错。 对于 Java 8 之前的代码,我更建议使用 Calendar:
使用 Java 8 的日期时间类型,可以直接进行各种计算,更加简洁和方便:
并且,对日期时间做计算操作,Java 8 日期时间 API 会比 Calendar 功能强大很多。 第一,可以使用各种 minus 和 plus 方法直接对日期进行加减操作,比如如下代码实现了减一天和加一天,以及减一个月和加一个月:
可以得到:
第二,还可以通过 with 方法进行快捷时间调节,比如: 使用 TemporalAdjusters.firstDayOfMonth 得到当前月的第一天; 使用 TemporalAdjusters.firstDayOfYear() 得到当前年的第一天; 使用 TemporalAdjusters.previous(DayOfWeek.SATURDAY) 得到上一个周六; 使用 TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY) 得到本月最后一个周五。
输出如下:
第三,可以直接使用 lambda 表达式进行自定义的时间调整。比如,为当前时间增加 100 天以内的随机天数:
得到:
除了计算外,还可以判断日期是否符合某个条件。比如,自定义函数,判断指定日期是否是家庭成员的生日:
然后,使用 query 方法查询是否匹配条件:
使用 Java 8 操作和计算日期时间虽然方便,但计算两个日期差时可能会踩坑: Java 8 中有一个专门的类 Period 定义了日期间隔,通过 Period.between 得到了两个 LocalDate 的差,返回的是两个日期差几年零几月零几天。 如果希望得知两个日期之间差几天,直接调用 Period 的 getDays() 方法得到的只是最后的“零几天”,而不是算总的间隔天数。 比如,计算 2019 年 12 月 12 日和 2019 年 10 月 1 日的日期间隔,很明显日期差是 2 个月零 11 天,但获取 getDays 方法得到的结果只是 11 天,而不是 72 天:
可以使用 ChronoUnit.DAYS.between 解决这个问题:
从日期时间的时区到格式化再到计算,你是不是体会到 Java 8 日期时间类的强大了呢? Tips:这里有个误区是,认为 java.util.Date 类似于新 API 中的 LocalDateTime。 ????????其实不是,虽然它们都没有时区概念,但 java.util.Date 类是因为使用 UTC 表示,所以没有时区概念,其本质是时间戳;而 LocalDateTime,严格上可以认为是一个日期时间的表示,而不是一个时间点。因此,在把 Date 转换为 LocalDateTime 的时候,需要通过 Date 的 toInstant 方法得到一个 UTC 时间戳进行转换,并需要提供当前的时区,这样才能把 UTC 时间转换为本地日期时间(的表示)。反过来,把 LocalDateTime 的时间表示转换为 Date 时,也需要提供时区,用于指定是哪个时区的时间表示,也就是先通过 atZone 方法把 LocalDateTime 转换为 ZonedDateTime,然后才能获得 UTC 时间戳:
很多同学说使用新 API 很麻烦,还需要考虑时区的概念,一点都不简洁。 通过这篇文章要和你说的是,并不是因为 API 需要设计得这么繁琐,而是 UTC 时间要变为当地时间,必须考虑时区。 ? |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/23 20:30:59- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |