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

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 年龄大的 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

未完待续。…


目录
相关文章
|
17天前
|
JSON Java Apache
Java基础-常用API-Object类
继承是面向对象编程的重要特性,允许从已有类派生新类。Java采用单继承机制,默认所有类继承自Object类。Object类提供了多个常用方法,如`clone()`用于复制对象,`equals()`判断对象是否相等,`hashCode()`计算哈希码,`toString()`返回对象的字符串表示,`wait()`、`notify()`和`notifyAll()`用于线程同步,`finalize()`在对象被垃圾回收时调用。掌握这些方法有助于更好地理解和使用Java中的对象行为。
|
1月前
|
算法 Java API
如何使用Java开发获得淘宝商品描述API接口?
本文详细介绍如何使用Java开发调用淘宝商品描述API接口,涵盖从注册淘宝开放平台账号、阅读平台规则、创建应用并申请接口权限,到安装开发工具、配置开发环境、获取访问令牌,以及具体的Java代码实现和注意事项。通过遵循这些步骤,开发者可以高效地获取商品详情、描述及图片等信息,为项目和业务增添价值。
67 10
|
1月前
|
存储 Java 数据挖掘
Java 8 新特性之 Stream API:函数式编程风格的数据处理范式
Java 8 引入的 Stream API 提供了一种新的数据处理方式,支持函数式编程风格,能够高效、简洁地处理集合数据,实现过滤、映射、聚合等操作。
65 6
|
1月前
|
Java API 开发者
Java中的Lambda表达式与Stream API的协同作用
在本文中,我们将探讨Java 8引入的Lambda表达式和Stream API如何改变我们处理集合和数组的方式。Lambda表达式提供了一种简洁的方法来表达代码块,而Stream API则允许我们对数据流进行高级操作,如过滤、映射和归约。通过结合使用这两种技术,我们可以以声明式的方式编写更简洁、更易于理解和维护的代码。本文将介绍Lambda表达式和Stream API的基本概念,并通过示例展示它们在实际项目中的应用。
|
2月前
|
安全 Java API
Java中的Lambda表达式与Stream API的高效结合####
探索Java编程中Lambda表达式与Stream API如何携手并进,提升数据处理效率,实现代码简洁性与功能性的双重飞跃。 ####
33 0
|
运维 监控 Java
Java版阿里云通信短信发送API接口实例(1)
Java版阿里云通信短信发送API接口实例(新)
1136 0
Java版阿里云通信短信发送API接口实例(1)
|
JSON Java API
Java版阿里云通信短信发送API接口实例(2)
Java版阿里云通信短信发送API接口实例(新)
1099 0
|
Java API 开发工具
Java版阿里云通信短信发送API接口实例(新)
版权声明:欢迎转载,请注明沉默王二原创。 https://blog.csdn.net/qing_gee/article/details/78751698 阿里云通信(原名阿里大于)的短信服务(Short Message Service)是阿里云为用户提供的一种通信服务的能力,支持快速发送短信验证码、短信通知等。
3577 1
|
4天前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
42 17
|
15天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
下一篇
开通oss服务