看山聊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

未完待续。…


目录
相关文章
|
27天前
|
安全 IDE Java
Java记录类型(Record):简化数据载体类
Java记录类型(Record):简化数据载体类
273 120
|
27天前
|
存储 Java API
Java Stream API:现代数据处理之道
Java Stream API:现代数据处理之道
294 188
|
1月前
|
JSON Java API
【干货满满】分享京东API接口到手价,用Java语言实现
本示例使用 Java 调用京东开放平台商品价格及优惠信息 API,通过商品详情和促销接口获取到手价(含优惠券、满减等),包含签名生成、HTTP 请求及响应解析逻辑,适用于比价工具、电商系统集成等场景。
|
27天前
|
存储 Java API
Java Stream API:现代数据处理之道
Java Stream API:现代数据处理之道
219 92
|
1月前
|
API 开发工具 开发者
客流类API实测:门店到访客群画像数据
本文介绍了一个实用的API——“门店到访客群画像分布”,适用于线下实体门店进行客群画像分析。该API支持多种画像维度,如性别、年龄、职业、消费偏好等,帮助商家深入了解顾客特征,提升运营效率。文章详细说明了API的参数配置、响应数据、接入流程,并附有Python调用示例,便于开发者快速集成。适合零售、餐饮等行业从业者使用。
客流类API实测:门店到访客群画像数据
|
1月前
|
JSON Java API
【干货满满】分享拼多多API接口到手价,用Java语言实现
本方案基于 Java 实现调用拼多多开放平台商品详情 API,通过联盟接口获取商品到手价(含拼团折扣与优惠券),包含签名生成、HTTP 请求及响应解析逻辑,适用于电商比价、导购系统集成。
|
1月前
|
Java API 网络架构
java调用api接口自动判断节假日信息
java调用api接口自动判断节假日信息
522 0
|
1月前
|
JSON Java API
【干货满满】分享淘宝API接口到手价,用Java语言实现
本文介绍了如何使用 Java 调用淘宝开放平台 API 获取商品到手价,涵盖依赖配置、签名生成、HTTP 请求与响应解析等核心实现步骤。
|
2月前
|
Java API
深入解析Java API中Object类的功能
了解和合理运用 Object类的这些方法,对于编写可靠和高效的Java应用程序至关重要。它们构成了Java对象行为的基础,影响着对象的创建、识别、表达和并发控制。
61 0