Java8 的时间库(1):介绍 Java8 中的时间类及常用 API
你好,我是看山。
年龄大的 Java 程序员都有体会,Java8 之前,Java 提供了一组时间类:java.util.Date
、java.util.Calendar
及其子类和工具类等。功能比较全面,最大的缺点是难用。所以很多团队直接放弃原生时间类,使用第三方的时间类库。后来,Java8 吸收了 joda-time 的优秀设计,提供了一组新的时间处理 APIjava.time.
。
本文作为这个系列的第一篇,扒一扒Date
和Calendar
存在的问题,说说 Java8 提供的时间库是怎么解决这些问题的。后续再说一下 Java8 中java.time.
包中的一些核心类,例如LocalDate
、LocalTime
、LocalDateTime
、ZonedDateTime
、Period
、Duration
。
老时间 API 存在的问题
线程安全性:老的时间 API 是非线程安全的,而我们的代码都是运行在并发环境下,这样就不得不处理难以调试的并发问题,而且还需要额外的代码处理线程安全。一不小心,就会碰到一些比较诡异的错误,本地还不容易复现,比如定义了一个公用的
SimpleDateFormt
去操作时间,偶尔并发时会出错,只能通过迂回的办法实现(比如借助ThreadLocal
)。在 Java8 中引入的新时间 API 是不可变对象,天然保证了线程安全。API 设计和可理解性:老的时间 API 在这设计上没有一致的模型,而且日常操作功能不全。而且有一些比较让人诟病的设计,比如
Date
类的构造方法public Date(int year, int month, int date)
,其中month
字段取值是0-11
,也就是按照计算机的 0 作为第一个数,但是day
没有这种情况,取值是1-31
,很容易搞错。新 API 以 ISO 为中心,遵循日期、时间、持续时间、时间间隔等一致的域模型,而且为时间 API 增加很多实用的工具方法。时区处理:老的时间 API 在处理时区逻辑时比较繁琐,但是在新的 API 中,只需要通过
Local*
或Zoned*
等类处理时区即可。
下面就开始实际上手 Java8 的时间 API 了。
LocalDate、LocalTime、LocalDateTime
这三个是最常用的几个时间类了,根据名字可以知道,这三个类是默认使用当前机器上的时区作为参考系的时间对象。也就是说,在不需要显示指定时区时,我们就可以使用这几个类。
LocalDate
LocalDate
是 ISO 格式(yyyy-MM-dd)的日期,没有时分秒的时间数据。我们可以用它表示生日、放假等只关心日期的数据。通过now
方法创建当前日期实例:
这里是使用机器时间创建的。
我们还可以使用of
方法或parse
方法获取指定日期的LocalDate
实例,比如,我们想要时间为 2021 年 6 月 11 号:
LocalDate
还提供了各种实用方法来获取时间信息,接下来快速浏览一下这些 API 方法。
获取明天的日期,即当前日期加一天:
获取上个月的今天,即当前日期并减去一个月(我们可以使用枚举单位操作数据):
解析日期“2021-06-11”,并获取周几(结果
DayOfWeek
是一个枚举类,设计很周到):
解析日期“2021-06-11”,并获取几号:
检查一个日期的年份是否是闰年:
判断一个日期是否在另外一个日期之前:
判断一个日期是否在另外一个日期之后:
获取给定日期的当天开始时间,比如给定“2021-06-11”,想要获取“2021-06-11T00:00”(也就是当天的 0 点时间):
获取给定日期所在月份的的第一天,比如给定“2021-06-11”,想要获取“2021-06-01”:
LocalTime
LocalDate
是 ISO 格式(yyyy-MM-dd)的日期,没有时分秒的时间数据。我们可以用它表示生日、放假等只关心日期的数据。通过now
方法创建当前日期实例:
LocalTime
提供的是没有日期数据的时间,只有时分秒数据。这个类很多方法与LocalDate
类似,所以我们快速过一下这些 API 方法。
创建当前时间
解析给定的字符串时间,可以使用
of
和parse
方法,比如下午 4:30:
解析给定字符串时间,并获取一小时后的时间实例:
获取给定字符串时间的小时数:
判断一个时间是否在另外一个时间之前:
判断一个时间是否在另外一个时间之后:
常用的时间常量:
LocalDateTime
顾名思义,LocalDateTime
表示日期和时间的组合。有了前两个类做铺垫,这个类也是很类似的操作。
创建当前时间
解析给定的字符串时间,可以使用
of
和parse
方法,比如 2021 年 6 月 11 日 16 点 30 分:
其他与LocalDate
和LocalTime
类似的 API,比如plusDays
、MinsHours
、getMonth
等。我们可以把LocalDateTime
理解为LocalDate
和LocalTime
的合体。
LocalDate
、LocalTime
、LocalDateTime
处理的都是当前系统所在时区的日期时间数据,有时候我们还需要处理特定时区的日期和时间,Java8 提供了ZonedDateTime
,接下来我们说说这个类。
ZonedDateTime
ZonedDateTime
的使用需要配合ZoneId
,ZoneId
表示不同区域的标识符,在${JAVA_HOME}/lib/tzdb.dat
文件中存放了默认的区域标识符,如果没有特别定义,需要是文件中指定的数据才能获取到ZoneId
实例。
我们来创建我天朝的区域:ZoneId zoneId = ZoneId.of("Asia/Shanghai");
。
如果不知道有哪些区域,可以通过Set<String> allZoneIds = ZoneId.getAvailableZoneIds();
获取,我当前 jdk 版本是 jdk1.8.0_202,一共有 599 个区域标识。
我们对比我朝与漂亮国的时间,获取当前时间:
可以看到,now
方法不传参数与使用当前时区参数结果一致,使用漂亮国时区时,在小时上有区别,但是两个时间,都是指当前时间。我们可以通过指定ZoneId
获取不同时区的结果:
可以看到,我们直接获取北京时间与通过漂亮国时间转换为北京时间,结果是一样的。
这里有一点需要提醒,这点是我朝程序员容易忽略的。那就是夏令时,我国没有夏令时,但是国外有些国家使用夏令时。所以在处理时间的时候,我们最好不要通过手动加减时区差来计算时间,这样很容易忽略夏令时。
Period、Duration
这两个类都是表示时间量,也就是时间段。不过,Period
类以年、月、日这种比较大的单位表示时间量,Duration
类以秒、纳秒这种相对较小的单位表示时间量。
Period
Period
表示的单位是年或月或日这种相对大一些的单位。我们可以用它来增减时间,或者计算两个时间间的时间差。
比如,我们以 2021 年 6 月 15 日为基准,计算 5 天后的日期:
Period
提供了ofYears
、ofMonths
、ofWeeks
、ofDays
、of
等方法,可以随情况处理时间。
除了锚定的特定时间,Period
还可以计算两个日期之间的时间差。比如:
这里需要注意一下,这三个方法是两个单位同单位的差,不会进行换算。我们可以借助ChronoUnit
实现单位换算状态下的结果:
ChronoUnit
是借助Duration
实现的,所以最细粒度可以到纳秒。
Duration
Duration
可以表示的单位是天、小时、分、秒、毫秒、纳秒,其内部结果是通过秒、纳秒进行存储的,其他可表示的单位,都是通过这两个单位组合实现的。比如,一分钟等于 3600 秒,那内部存储就是 3600 秒 0 纳秒;1 毫秒等于 1000000 纳秒,内部存储就是 0 秒 1000000 纳秒。
Duration
用法与Period
类似。比如,我们给 21 点 03 分 15 秒加 30 秒:
Duration
也可以计算两个时间之间的时间差,只是单位较小一些,比如:
同样的,我们可以借助ChronoUnit
实现更多单位的时间差,这里不做赘述。
从老时间 API 创建
Java8 提供了toInstant()
方法,可以将老时间 API 的Date
和Calendar
转换为新的对象:
时间格式化
Java8 提供了易于使用的时间格式化 API,这里简单说下。比如:
与老时间 API 一样,我们可以指定格式:
文末总结
从 Java7 到 Java8,提供了很多特性,除了 Lambda 表达式,时间 API 绝对也是良心功能了。而且从发布到现在已经过去这么多年,我们还是要与时俱进,逐渐使用优秀的 API 替换老 API 了。如果还在用 Java7 或者 Java6,然后还想使用 Java8 这种时间 API,这里推荐两个第三方库:threetenbp
和joda-time
。
参考
推荐阅读
你好,我是看山,10 年老猿,开源贡献者。游于码界,戏享人生。关注公众号:看山的小屋,领取学习资料。
版权声明: 本文为 InfoQ 作者【看山】的原创文章。
原文链接:【http://xie.infoq.cn/article/07c5a38856b1812e0515477c7】。文章转载请联系作者。
评论