看山聊Java:介绍 Java8 中的时间类及常用 API-阿里云开发者社区

开发者社区> 看山灬> 正文

看山聊Java:介绍 Java8 中的时间类及常用 API

简介: 年龄大的 Java 程序员都有体会,Java8 之前,Java 提供了一组时间类:java.util.Date、java.util.Calendar及其子类和工具类等。功能比较全面,最大的缺点是难用。所以很多团队直接放弃原生时间类,使用第三方的时间类库。
+关注继续查看

image.png

该图片由Önder Örtel在Pixabay上发布


你好,我是看山。


年龄大的 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方法创建当前日期实例:


LocalDate localDate = LocalDate.now();

这里是使用机器时间创建的。


我们还可以使用of方法或parse方法获取指定日期的LocalDate实例,比如,我们想要时间为 2021 年 6 月 11 号:

LocalDate.of(2021, 6, 11);
LocalDate.parse("2021-06-11");

LocalDate还提供了各种实用方法来获取时间信息,接下来快速浏览一下这些 API 方法。


获取明天的日期,即当前日期加一天:

LocalDate tomorrow = LocalDate.now().plusDays(1);

获取上个月的今天,即当前日期并减去一个月(我们可以使用枚举单位操作数据):

LocalDate previousMonthSameDay = LocalDate.now().minus(1, ChronoUnit.MONTHS);`

解析日期“2021-06-11”,并获取周几(结果DayOfWeek是一个枚举类,设计很周到):

DayOfWeek friday = LocalDate.parse("2021-06-11").getDayOfWeek();


解析日期“2021-06-11”,并获取几号:

int eleven = LocalDate.parse("2021-06-11").getDayOfMonth();


检查一个日期的年份是否是闰年:

boolean leapYear = LocalDate.now().isLeapYear();

判断一个日期是否在另外一个日期之前:

boolean isBefore = LocalDate.parse("2021-06-11").isBefore(LocalDate.parse("2021-06-12"));

判断一个日期是否在另外一个日期之后:

boolean isAfter = LocalDate.parse("2021-06-12").isAfter(LocalDate.parse("2021-06-11"));

获取给定日期的当天开始时间,比如给定“2021-06-11”,想要获取“2021-06-11T00:00”(也就是当天的 0 点时间):

LocalDateTime beginningOfDay = LocalDate.parse("2021-06-11").atStartOfDay();

获取给定日期所在月份的的第一天,比如给定“2021-06-11”,想要获取“2021-06-01”:

LocalDate firstDayOfMonth = LocalDate.parse("2021-06-11").with(TemporalAdjusters.firstDayOfMonth());

LocalTime

LocalDate是 ISO 格式(yyyy-MM-dd)的日期,没有时分秒的时间数据。我们可以用它表示生日、放假等只关心日期的数据。通过now方法创建当前日期实例:


LocalTime提供的是没有日期数据的时间,只有时分秒数据。这个类很多方法与LocalDate类似,所以我们快速过一下这些 API 方法。


创建当前时间

LocalDate firstDayOfMonth = LocalDate.parse("2021-06-11").with(TemporalAdjusters.firstDayOfMonth());

解析给定的字符串时间,可以使用of和parse方法,比如下午 4:30:

LocalTime sixty30 = LocalTime.parse("16:30");
LocalTime sixty31 = LocalTime.of(16, 31);

解析给定字符串时间,并获取一小时后的时间实例:

LocalTime seventy30 = LocalTime.parse("16:30").plus(1, ChronoUnit.HOURS);

获取给定字符串时间的小时数:

int sixty = LocalTime.parse("16:30").getHour();

判断一个时间是否在另外一个时间之前:

boolean isBeforeTime = LocalTime.parse("16:30").isBefore(LocalTime.parse("17:30"));

判断一个时间是否在另外一个时间之后:

boolean isAfterTime = LocalTime.parse("17:30").isAfter(LocalTime.parse("16:30"));

常用的时间常量:

LocalTime.MIN;// 00:00
LocalTime.NOON;// 12:00
LocalTime.MAX;// 23:59:59.999999999
LocalTime.MIDNIGHT;// 00:00

LocalDateTime

顾名思义,LocalDateTime表示日期和时间的组合。有了前两个类做铺垫,这个类也是很类似的操作。


创建当前时间

LocalDateTime.now();

解析给定的字符串时间,可以使用of和parse方法,比如 2021 年 6 月 11 日 16 点 30 分:

LocalDateTime.of(2021, Month.JUNE, 11, 16, 30);
LocalDateTime.parse("2021-06-11T16:30:00");// 注意这个时间格式,这种写法属于 UTC 时间格式,后续再开文说一下时间格式的话题。

其他与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 个区域标识。


我们对比我朝与漂亮国的时间,获取当前时间:

ZonedDateTime.now();// 2021-06-11T17:49:53.400+08:00[Asia/Shanghai]
ZonedDateTime.now(ZoneId.systemDefault());// 2021-06-11T17:49:53.400+08:00[Asia/Shanghai]
ZonedDateTime.now(ZoneId.of("America/New_York"));// 2021-06-11T05:49:53.400-04:00[America/New_York]

可以看到,now方法不传参数与使用当前时区参数结果一致,使用漂亮国时区时,在小时上有区别,但是两个时间,都是指当前时间。我们可以通过指定ZoneId获取不同时区的结果:


final ZonedDateTime newyorkZonedDateTime = ZonedDateTime.now(ZoneId.of("America/New_York"));// 2021-06-11T05:57:00.655-04:00[America/New_York]
final ZonedDateTime shanghaiZonedDateTime = newyorkZonedDateTime.withZoneSameInstant(ZoneId.of("Asia/Shanghai"));// 2021-06-11T17:57:00.655+08:00[Asia/Shanghai]
final ZonedDateTime shanghaiZonedDateTime2 = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));// 2021-06-11T17:58:34.221+08:00[Asia/Shanghai]

可以看到,我们直接获取北京时间与通过漂亮国时间转换为北京时间,结果是一样的。


这里有一点需要提醒,这点是我朝程序员容易忽略的。那就是夏令时,我国没有夏令时,但是国外有些国家使用夏令时。所以在处理时间的时候,我们最好不要通过手动加减时区差来计算时间,这样很容易忽略夏令时。


Period、Duration

这两个类都是表示时间量,也就是时间段。不过,Period类以年、月、日这种比较大的单位表示时间量,Duration类以秒、纳秒这种相对较小的单位表示时间量。


Period

Period表示的单位是年或月或日这种相对大一些的单位。我们可以用它来增减时间,或者计算两个时间间的时间差。


比如,我们以 2021 年 6 月 15 日为基准,计算 5 天后的日期:


LocalDate initialDate = LocalDate.parse("2021-06-15");
LocalDate finalDate = initialDate.plus(Period.ofDays(5));// 2021-06-20

Period提供了ofYears、ofMonths、ofWeeks、ofDays、of等方法,可以随情况处理时间。


除了锚定的特定时间,Period还可以计算两个日期之间的时间差。比如:


LocalDate initialDate = LocalDate.parse("2021-06-15");
final LocalDate newDate = initialDate.plus(Period.of(1, 2, 3));
final Period period = Period.between(initialDate, newDate);
int years = period.getYears();// 1
int months = period.getMonths();// 2
int days = period.getDays();// 3

这里需要注意一下,这三个方法是两个单位同单位的差,不会进行换算。我们可以借助ChronoUnit实现单位换算状态下的结果:


long allYears = ChronoUnit.YEARS.between(initialDate, newDate);// 1
long allMonths = ChronoUnit.MONTHS.between(initialDate, newDate);// 14
long allDays = ChronoUnit.DAYS.between(initialDate, newDate);// 429

ChronoUnit是借助Duration实现的,所以最细粒度可以到纳秒。


Duration

Duration可以表示的单位是天、小时、分、秒、毫秒、纳秒,其内部结果是通过秒、纳秒进行存储的,其他可表示的单位,都是通过这两个单位组合实现的。比如,一分钟等于 3600 秒,那内部存储就是 3600 秒 0 纳秒;1 毫秒等于 1000000 纳秒,内部存储就是 0 秒 1000000 纳秒。


Duration用法与Period类似。比如,我们给 21 点 03 分 15 秒加 30 秒:

LocalTime initialTime = LocalTime.parse("21:02:15");
LocalTime finalTime = initialTime.plus(Duration.ofSeconds(30));// 21:02:45

Duration也可以计算两个时间之间的时间差,只是单位较小一些,比如:


LocalTime initialTime = LocalTime.parse("21:02:15");
LocalTime newTime = LocalTime.of(21, 02, 20);
Duration duration = Duration.between(initialTime, newTime);
long seconds = duration.getSeconds();// 5
int nano = duration.getNano();// 0

同样的,我们可以借助ChronoUnit实现更多单位的时间差,这里不做赘述。


从老时间 API 创建

Java8 提供了toInstant()方法,可以将老时间 API 的Date和Calendar转换为新的对象:


Date date = new Date();
LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
Calendar calendar = GregorianCalendar.getInstance();
LocalDateTime localDateTime1 = LocalDateTime.ofInstant(calendar.toInstant(), ZoneId.systemDefault());

时间格式化

Java8 提供了易于使用的时间格式化 API,这里简单说下。比如:


LocalDateTime localDateTime = LocalDateTime.of(2021, Month.JUNE, 15, 21, 23);
String localDateString = localDateTime.format(DateTimeFormatter.ISO_DATE);// 2021-06-15

与老时间 API 一样,我们可以指定格式:


localDateTime.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));

文末总结

从 Java7 到 Java8,提供了很多特性,除了 Lambda 表达式,时间 API 绝对也是良心功能了。而且从发布到现在已经过去这么多年,我们还是要与时俱进,逐渐使用优秀的 API 替换老 API 了。如果还在用 Java7 或者 Java6,然后还想使用 Java8 这种时间 API,这里推荐两个第三方库:threetenbp和joda-time。


参考

core-java-8-datetime

ZonedDateTime

推荐阅读

Java8 的时间库(1):介绍 Java8 中的时间类及常用 API

未完待续。…


版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
10088 0
Java工具集介绍2008年第二期
[align=center][b]Java工具集介绍2008年第二期[/b][/align]1、WebLEAF新版本:2.2和3.0WebLEAF是一个servlet框架,用于帮助建立Java的Web服务器端的应用程序。
826 0
Java工具集介绍2008年第七期
版权声明:本文为博主chszs的原创文章,未经博主允许不得转载。 https://blog.csdn.net/chszs/article/details/2224631 Java工具集介绍2008年第七期 原创文章,转载请保留作者名。
762 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
13891 0
Java工具集介绍2008年第一期
Java工具集介绍2008年第一期 1、RefactorIT RefactorIT是一个用于Java开发者的工具。开发者可以把任意大小(size)、任意复杂的代码重构成非常易读的代码,通过使用自动重构技术。
877 0
Java工具集介绍11_04
Java工具集介绍11_04 1、Tom 新版本:2.5.2 Tom是一个新的Java模式匹配编译器。它提供了一个匹配操作符,和类似的一些用于模式匹配的函数语言定义。
820 0
Java工具集介绍2008年第二期
Java工具集介绍2008年第二期 1、WebLEAF 新版本:2.2和3.0 WebLEAF是一个servlet框架,用于帮助建立Java的Web服务器端的应用程序。
797 0
Java工具集介绍2008年第一期
[align=center][b]Java工具集介绍2008年第一期[/b][/align]1、RefactorITRefactorIT是一个用于Java开发者的工具。
878 0
+关注
看山灬
专注后端开发、架构相关知识分享,个人网站 https://howardliu.cn/。
136
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载