20 个案例教你在 Java 8 中如何处理日期和时间?

2,960 阅读13分钟

前言

前面一篇文章写了《SimpleDateFormat 如何安全的使用?》, 里面介绍了 SimpleDateFormat 如何处理日期/时间,以及如何保证线程安全,及其介绍了在 Java 8 中的处理时间/日期默认就线程安全的 DateTimeFormatter 类。那么 Java 8 中该怎么样处理生活中常见的一些日期/时间呢?比如:计算一周后的日期;计算一年前或一年后的日期;检查闰年等。

接下来创建了 20 个基于任务的实例来学习 Java 8 的新特性。从最简单创建当天的日期开始,然后创建时间及时区,接着模拟一个日期提醒应用中的任务——计算重要日期的到期天数,例如生日、纪念日、账单日、保费到期日、信用卡过期日等。

示例 1、在 Java 8 中获取今天的日期

Java 8 中的 LocalDate 用于表示当天日期。和 java.util.Date 不同,它只有日期,不包含时间。当你仅需要表示日期时就用这个类。

1LocalDate now = LocalDate.now();
2System.out.println(now);

结果是:

12018-06-20

上面的代码创建了当天的日期,不含时间信息。打印出的日期格式非常友好,不像老的 Date 类打印出一堆没有格式化的信息。

示例 2、在 Java 8 中获取年、月、日信息

LocalDate 类提供了获取年、月、日的快捷方法,其实例还包含很多其它的日期属性。通过调用这些方法就可以很方便的得到需要的日期信息,不用像以前一样需要依赖 java.util.Calendar 类了

1LocalDate now = LocalDate.now();
2int year = now.getYear();
3int monthValue = now.getMonthValue();
4int dayOfMonth = now.getDayOfMonth();
5System.out.printf("year = %d, month = %d, day = %d", year, monthValue, dayOfMonth);

结果是:

1year = 2018, month = 6, day = 20

示例 3、在 Java 8 中处理特定日期

在第一个例子里,我们通过静态工厂方法 now() 非常容易地创建了当天日期,你还可以调用另一个有用的工厂方法LocalDate.of() 创建任意日期, 该方法需要传入年、月、日做参数,返回对应的 LocalDate 实例。这个方法的好处是没再犯老 API 的设计错误,比如年度起始于 1900,月份是从 0 开始等等。日期所见即所得,就像下面这个例子表示了 6 月 20 日,没有任何隐藏机关。

1LocalDate date = LocalDate.of(2018, 06, 20);
2System.out.println(date);

可以看到创建的日期完全符合预期,与写入的 2018 年 6 月 20 日完全一致。

示例 4、在 Java 8 中判断两个日期是否相等

现实生活中有一类时间处理就是判断两个日期是否相等。你常常会检查今天是不是个特殊的日子,比如生日、纪念日或非交易日。这时就需要把指定的日期与某个特定日期做比较,例如判断这一天是否是假期。下面这个例子会帮助你用 Java 8 的方式去解决,你肯定已经想到了,LocalDate 重载了 equal 方法,请看下面的例子:

1LocalDate now = LocalDate.now();
2LocalDate date = LocalDate.of(2018, 06, 20);
3if (date.equals(now)) {
4    System.out.println("同一天");
5}

这个例子中我们比较的两个日期相同。注意,如果比较的日期是字符型的,需要先解析成日期对象再作判断。

示例 5、在 Java 8 中检查像生日这种周期性事件

Java 中另一个日期时间的处理就是检查类似每月账单、结婚纪念日、EMI日或保险缴费日这些周期性事件。如果你在电子商务网站工作,那么一定会有一个模块用来在圣诞节、感恩节这种节日时向客户发送问候邮件。Java 中如何检查这些节日或其它周期性事件呢?答案就是 MonthDay 类。这个类组合了月份和日,去掉了年,这意味着你可以用它判断每年都会发生事件。和这个类相似的还有一个 YearMonth 类。这些类也都是不可变并且线程安全的值类型。下面我们通过 MonthDay 来检查周期性事件:

1LocalDate now = LocalDate.now();
2LocalDate dateOfBirth = LocalDate.of(2018, 06, 20);
3MonthDay birthday = MonthDay.of(dateOfBirth.getMonth(), dateOfBirth.getDayOfMonth());
4MonthDay currentMonthDay = MonthDay.from(now);
5if (currentMonthDay.equals(birthday)) {
6    System.out.println("Happy Birthday");
7} else {
8    System.out.println("Sorry, today is not your birthday");
9}

结果:(注意:获取当前时间可能与你看的时候不对,所以这个结果可能和你看的时候运行结果不一样)

1Happy Birthday

只要当天的日期和生日匹配,无论是哪一年都会打印出祝贺信息。你可以把程序整合进系统时钟,看看生日时是否会受到提醒,或者写一个单元测试来检测代码是否运行正确。

示例 6、在 Java 8 中获取当前时间

与 Java 8 获取日期的例子很像,获取时间使用的是 LocalTime 类,一个只有时间没有日期的 LocalDate 近亲。可以调用静态工厂方法 now() 来获取当前时间。默认的格式是 hh:mm:ss:nnn。

1LocalTime localTime = LocalTime.now();
2System.out.println(localTime);

结果:

113:35:56.155

可以看到当前时间就只包含时间信息,没有日期。

示例 7、如何在现有的时间上增加小时

通过增加小时、分、秒来计算将来的时间很常见。Java 8 除了不变类型和线程安全的好处之外,还提供了更好的plusHours() 方法替换 add(),并且是兼容的。注意,这些方法返回一个全新的 LocalTime 实例,由于其不可变性,返回后一定要用变量赋值。

1LocalTime localTime = LocalTime.now();
2System.out.println(localTime);
3LocalTime localTime1 = localTime.plusHours(2);//增加2小时
4System.out.println(localTime1);

结果:

113:41:20.721
215:41:20.721

可以看到,新的时间在当前时间 13:41:20.721 的基础上增加了 2 个小时。

示例 8、如何计算一周后的日期

和上个例子计算两小时以后的时间类似,这个例子会计算一周后的日期。LocalDate 日期不包含时间信息,它的 plus()方法用来增加天、周、月,ChronoUnit 类声明了这些时间单位。由于 LocalDate 也是不变类型,返回后一定要用变量赋值。

1LocalDate now = LocalDate.now();
2LocalDate plusDate = now.plus(1, ChronoUnit.WEEKS);
3System.out.println(now);
4System.out.println(plusDate);

结果:

12018-06-20
22018-06-27

可以看到新日期离当天日期是 7 天,也就是一周。你可以用同样的方法增加 1 个月、1 年、1 小时、1 分钟甚至一个世纪,更多选项可以查看 Java 8 API 中的 ChronoUnit 类。

示例 9、计算一年前或一年后的日期

继续上面的例子,上个例子中我们通过 LocalDate 的 plus() 方法增加天数、周数或月数,这个例子我们利用 minus() 方法计算一年前的日期。

1LocalDate now = LocalDate.now();
2LocalDate minusDate = now.minus(1, ChronoUnit.YEARS);
3LocalDate plusDate1 = now.plus(1, ChronoUnit.YEARS);
4System.out.println(minusDate);
5System.out.println(plusDate1);

结果:

12017-06-20
22019-06-20

示例 10、使用 Java 8 的 Clock 时钟类

Java 8 增加了一个 Clock 时钟类用于获取当时的时间戳,或当前时区下的日期时间信息。以前用到System.currentTimeInMillis() 和 TimeZone.getDefault() 的地方都可用 Clock 替换。

1Clock clock = Clock.systemUTC();
2Clock clock1 = Clock.systemDefaultZone();
3System.out.println(clock);
4System.out.println(clock1);

结果:

1SystemClock[Z]
2SystemClock[Asia/Shanghai]

示例 11、如何用 Java 判断日期是早于还是晚于另一个日期

另一个工作中常见的操作就是如何判断给定的一个日期是大于某天还是小于某天?在 Java 8 中,LocalDate 类有两类方法 isBefore() 和 isAfter() 用于比较日期。调用 isBefore() 方法时,如果给定日期小于当前日期则返回 true。

1 LocalDate tomorrow = LocalDate.of(2018,6,20);
2 if(tomorrow.isAfter(now)){
3     System.out.println("Tomorrow comes after today");
4 }
5 LocalDate yesterday = now.minus(1, ChronoUnit.DAYS);
6 if(yesterday.isBefore(now)){
7     System.out.println("Yesterday is day before today");
8 }

在 Java 8 中比较日期非常方便,不需要使用额外的 Calendar 类来做这些基础工作了。

示例 12、在 Java 8 中处理时区

Java 8 不仅分离了日期和时间,也把时区分离出来了。现在有一系列单独的类如 ZoneId 来处理特定时区,ZoneDateTime 类来表示某时区下的时间。这在 Java 8 以前都是 GregorianCalendar 类来做的。

1ZoneId america = ZoneId.of("America/New_York");
2LocalDateTime localtDateAndTime = LocalDateTime.now();
3ZonedDateTime dateAndTimeInNewYork  = ZonedDateTime.of(localtDateAndTime, america );
4System.out.println(dateAndTimeInNewYork);

示例 13、如何表示信用卡到期这类固定日期,答案就在 YearMonth

与 MonthDay 检查重复事件的例子相似,YearMonth 是另一个组合类,用于表示信用卡到期日、FD 到期日、期货期权到期日等。还可以用这个类得到 当月共有多少天,YearMonth 实例的 lengthOfMonth() 方法可以返回当月的天数,在判断 2 月有 28 天还是 29 天时非常有用。

1YearMonth currentYearMonth = YearMonth.now();
2System.out.printf("Days in month year %s: %d%n", currentYearMonth, currentYearMonth.lengthOfMonth());
3YearMonth creditCardExpiry = YearMonth.of(2018, Month.FEBRUARY);
4System.out.printf("Your credit card expires on %s %n", creditCardExpiry);

结果:

1Days in month year 2018-06: 30
2Your credit card expires on 2018-02

示例 14、如何在 Java 8 中检查闰年

LocalDate 类有一个很实用的方法 isLeapYear() 判断该实例是否是一个闰年。

示例 15、计算两个日期之间的天数和月数

有一个常见日期操作是计算两个日期之间的天数、周数或月数。在 Java 8 中可以用 java.time.Period 类来做计算。下面这个例子中,我们计算了当天和将来某一天之间的月数。

1LocalDate date = LocalDate.of(2019, Month.MARCH, 20);
2Period period = Period.between(now, date);
3System.out.println("离下个时间还有" + period.getMonths() + " 个月");

示例 16、包含时差信息的日期和时间

在 Java 8 中,ZoneOffset 类用来表示时区,举例来说印度与 GMT 或 UTC 标准时区相差 +05:30,可以通过ZoneOffset.of() 静态方法来获取对应的时区。一旦得到了时差就可以通过传入 LocalDateTime 和 ZoneOffset 来创建一个 OffSetDateTime 对象。

1LocalDateTime datetime = LocalDateTime.of(2014, Month.JANUARY, 14,19,30);
2ZoneOffset offset = ZoneOffset.of("+05:30");
3OffsetDateTime date = OffsetDateTime.of(datetime, offset);  
4System.out.println("Date and Time with timezone offset in Java : " + date);

示例 17、在 Java 8 中获取当前的时间戳

如果你还记得 Java 8 以前是如何获得当前时间戳,那么现在你终于解脱了。Instant 类有一个静态工厂方法 now() 会返回当前的时间戳,如下所示:

1Instant timestamp = Instant.now();
2System.out.println(timestamp);

结果:

12018-06-20T06:35:24.881Z

时间戳信息里同时包含了日期和时间,这和 java.util.Date 很像。实际上 Instant 类确实等同于 Java 8 之前的 Date类,你可以使用 Date 类和 Instant 类各自的转换方法互相转换,例如:Date.from(Instant) 将 Instant 转换成java.util.Date,Date.toInstant() 则是将 Date 类转换成 Instant 类。

示例 18、在 Java 8 中如何使用预定义的格式化工具去解析或格式化日期

在 Java 8 以前的世界里,日期和时间的格式化非常诡异,唯一的帮助类 SimpleDateFormat 也是非线程安全的,而且用作局部变量解析和格式化日期时显得很笨重。幸好线程局部变量能使它在多线程环境中变得可用,不过这都是过去时了。Java 8 引入了全新的日期时间格式工具,线程安全而且使用方便。它自带了一些常用的内置格式化工具。

参见我上一篇文章: 《SimpleDateFormat 如何安全的使用?》

示例 19、如何在 Java 中使用自定义格式化工具解析日期

尽管内置格式化工具很好用,有时还是需要定义特定的日期格式。可以调用 DateTimeFormatter 的 ofPattern() 静态方法并传入任意格式返回其实例,格式中的字符和以前代表的一样,M 代表月,m 代表分。如果格式不规范会抛出 DateTimeParseException 异常,不过如果只是把 M 写成 m 这种逻辑错误是不会抛异常的。

参见我上一篇文章: 《SimpleDateFormat 如何安全的使用?》

示例 20、在 Java 8 中如何把日期转换成字符串

上两个主要是从字符串解析日期。现在我们反过来,把 LocalDateTime 日期实例转换成特定格式的字符串。这是迄今为止 Java 日期转字符串最为简单的方式了。下面的例子将返回一个代表日期的格式化字符串。和前面类似,还是需要创建 DateTimeFormatter 实例并传入格式,但这回调用的是 format() 方法,而非 parse() 方法。这个方法会把传入的日期转化成指定格式的字符串。

1LocalDateTime arrivalDate  = LocalDateTime.now();
2try {
3    DateTimeFormatter format = DateTimeFormatter.ofPattern("MMMdd yyyy  hh:mm a");
4    String landing = arrivalDate.format(format);
5    System.out.printf("Arriving at :  %s %n", landing);
6}catch (DateTimeException ex) {
7    System.out.printf("%s can't be formatted!%n", arrivalDate);
8    ex.printStackTrace();
9}

Java 8 日期时间 API 的重点

通过这些例子,你肯定已经掌握了 Java 8 日期时间 API 的新知识点。现在来回顾一下这个优雅 API 的使用要点:

1)提供了 javax.time.ZoneId 获取时区。

2)提供了 LocalDate 和 LocalTime 类。

3)Java 8 的所有日期和时间 API 都是不可变类并且线程安全,而现有的 Date 和 Calendar API 中的 java.util.Date 和SimpleDateFormat 是非线程安全的。

4)主包是 java.time, 包含了表示日期、时间、时间间隔的一些类。里面有两个子包 java.time.format 用于格式化, java.time.temporal 用于更底层的操作。

5)时区代表了地球上某个区域内普遍使用的标准时间。每个时区都有一个代号,格式通常由区域/城市构成(Asia/Tokyo),在加上与格林威治或 UTC 的时差。例如:东京的时差是 +09:00。

6)OffsetDateTime 类实际上组合了 LocalDateTime 类和 ZoneOffset 类。用来表示包含和格林威治或 UTC 时差的完整日期(年、月、日)和时间(时、分、秒、纳秒)信息。

7)DateTimeFormatter 类用来格式化和解析时间。与 SimpleDateFormat 不同,这个类不可变并且线程安全,需要时可以给静态常量赋值。DateTimeFormatter 类提供了大量的内置格式化工具,同时也允许你自定义。在转换方面也提供了 parse() 将字符串解析成日期,如果解析出错会抛出 DateTimeParseException。DateTimeFormatter 类同时还有format() 用来格式化日期,如果出错会抛出 DateTimeException异常。

8)再补充一点,日期格式“MMM d yyyy”和“MMM dd yyyy”有一些微妙的不同,第一个格式可以解析“Jan 2 2014”和“Jan 14 2014”,而第二个在解析“Jan 2 2014”就会抛异常,因为第二个格式里要求日必须是两位的。如果想修正,你必须在日期只有个位数时在前面补零,就是说“Jan 2 2014”应该写成 “Jan 02 2014”。

推荐阅读:

《深入理解 Java 内存模型》读书笔记

面试-基础篇

Spring Boot 2.0 迁移指南

SpringBoot使用Docker快速部署项目

为什么选择 Spring 作为 Java 框架?

SpringBoot RocketMQ 整合使用和监控

Spring Boot 面试的十个问题

使用 Spring Framework 时常犯的十大错误

SpringBoot Admin 使用指南

SpringBoot Kafka 整合使用

SpringBoot RabbitMQ 整合使用

Elasticsearch索引增量统计及定时邮件实现

Elasticsearch实战 | 必要的时候,还得空间换时间!干货 |《从Lucene到Elasticsearch全文检索实战》拆解实践

上篇好文:

JVM面试问题系列:JVM 配置常用参数和常用 GC 调优策略