9.3 在Java 8中处理日期和时间
几乎所有商业应用软件都具有一些日期和时间的概念。建模真实世界的事件或活动时,知道事件什么时候发生可以对后续报告和域对象的比较都很重要。Java 8 完全改变了开发者处理日期和时间的方式。本节介绍 Java 8 引入的新概念。在之前的版本中,只能通过 java.util.Date 类处理日期和时间,而且这个类没有建模这些概念。使用旧 API 的代码应该尽早转用新 API。
9.3.1 介绍Java 8的日期和时间API
Java 8 引入了一个新包 java.time,包含了多数开发者都会用到的核心类。这个包分为四个子包。
java.time.chrono
开发者使用的历法不符合 ISO 标准时,需要与之交互的其他纪年法。例如日本历法。
java.time.format
这个包中的 DateTimeFormatter 类用于把日期和时间对象转换成字符串,以及把字符串解析成日期和时间对象。
java.time.temporal
包含日期和时间核心类所需的接口,还抽象了一些日期方面的高级操作(例如查询和调节器)。
java.time.zone
底层时区规则使用的类;多数开发者都用不到这个包。
表示时间时,最重要的概念之一是,某个实体时间轴上的瞬时点。既然这个概念在狭义相对论等理论中已经有了完善的定义,那么在计算机中表示时间就要做些假设。Java 8 使用一个 Instant 对象表示一个时间点,而且做了下述关键假设:
表示的秒数不能超出
long类型的取值范围;表示的时间不能比纳秒还精细。
因此,能表示的时间受到当前计算机系统的能力所限制。不过,还有一个基本概念需要介绍。
Instant 对象是时空中的单一事件。可是,程序员经常要处理的却是两个事件之间的时间间隔,所以 Java 8 还引入了 java.time.Duration 类。这个类会忽略可能出现的日历效应(例如夏令时)。了解瞬时和事件持续时间的基本概念之后,我们来看一下瞬时的具体表现。
1. 时间戳的组成
图 9-1 展示了使用不同方式分解时间戳得到的各个部分。

图 9-1:分解时间戳
关键是要知道,不同的地方适合使用不同的抽象方式。例如,有些商业应用主要处理的是 LocalDate 对象,此时需要的时间粒度是一个工作日。而有些应用需要亚秒级甚至是毫秒级精度。开发者要了解所需的业务逻辑,在应用中使用合适的表示方式。
2. 示例
日期和时间 API 不是一朝一夕就能完全掌握的。下面举个例子,这个示例定义了一个日志类,记录生日。如果碰巧你很容易忘记生日,那么这样的类(尤其是 getBirthdaysInNextMonth() 这样的方法)可以给你提供很大的帮助:
public class BirthdayDiary {private Map<String, LocalDate> birthdays;public BirthdayDiary() {birthdays = new HashMap<>();}public LocalDate addBirthday(String name, int day, int month,int year) {LocalDate birthday = LocalDate.of(year, month, day);birthdays.put(name, birthday);return birthday;}public LocalDate getBirthdayFor(String name) {return birthdays.get(name);}public int getAgeInYear(String name, int year) {Period period = Period.between(birthdays.get(name),birthdays.get(name).withYear(year));return period.getYears();}public Set<String> getFriendsOfAgeIn(int age, int year) {return birthdays.keySet().stream().filter(p -> getAgeInYear(p, year) == age).collect(Collectors.toSet());}public int getDaysUntilBirthday(String name) {Period period = Period.between(LocalDate.now(),birthdays.get(name));return period.getDays();}public Set<String> getBirthdaysIn(Month month) {return birthdays.entrySet().stream().filter(p -> p.getValue().getMonth() == month).map(p -> p.getKey()).collect(Collectors.toSet());}public Set<String> getBirthdaysInNextMonth() {return getBirthdaysIn(LocalDate.now().getMonth());}public int getTotalAgeInYears() {return birthdays.keySet().stream().mapToInt(p -> getAgeInYear(p,LocalDate.now().getYear())).sum();}}
这个类展示了如何使用低层 API 实现有用的功能。这个类还用到了一些新技术,例如 Java 的流 API,而且演示了如何把 LocalDate 类视作不可变的类使用,以及如何把日期当成值处理。
9.3.2 查询
很多情况下,我们要回答一些关于某个时间对象的问题,例如:
这个日期在 3 月 1 日之前吗?
这个日期所在的年份是闰年吗?
今天距我下一次生日还有多少天?
为了回答这些问题,可以使用 TemporalQuery 接口,其定义如下所示:
public interface TemporalQuery<R> {R queryFrom(TemporalAccessor temporal);}
queryFrom() 方法的参数不能为 null,不过,如果结果表示不存在的值,可以使用 null 作为返回值。
![]()
Predicate接口实现的查询可以理解为只能回答“是”或“否”的问题。而TemporalQuery接口实现的查询更普适,除了能回答“是”或“否”之外,还能回答“有多少”和“哪一个”等问题。
下面看一个查询的具体示例,这个查询回答的问题是:“这个日期在一年中的哪个季度?”Java 8 不直接支持季度,因此要使用类似下面的代码:
LocalDate today = LocalDate.now();Month currentMonth = today.getMonth();Month firstMonthofQuarter = currentMonth.firstMonthOfQuarter();
这样写没有把季度单独抽象出来,还需要编写专用的代码。下面我们稍微扩展一下 JDK,定义如下的枚举类型:
public enum Quarter {FIRST, SECOND, THIRD, FOURTH;}
现在,可以这样编写查询:
public class QuarterOfYearQuery implements TemporalQuery<Quarter> {@Overridepublic Quarter queryFrom(TemporalAccessor temporal) {LocalDate now = LocalDate.from(temporal);if(now.isBefore(now.with(Month.APRIL).withDayOfMonth(1))) {return Quarter.FIRST;} else if(now.isBefore(now.with(Month.JULY).withDayOfMonth(1))) {return Quarter.SECOND;} else if(now.isBefore(now.with(Month.NOVEMBER).withDayOfMonth(1))) {return Quarter.THIRD;} else {return Quarter.FOURTH;}}}
TemporalQuery 对象可以直接使用,也可以间接使用。下面各举一个例子:
QuarterOfYearQuery q = new QuarterOfYearQuery();// 直接使用Quarter quarter = q.queryFrom(LocalDate.now());System.out.println(quarter);// 间接使用quarter = LocalDate.now().query(q);System.out.println(quarter);
多数情况下,最好间接使用,即把查询对象作为参数传给 query() 方法,因为这样写出的代码更易于阅读。
9.3.3 调节器
调节器的作用是修改日期和时间对象。假如我们想获取某个时间戳所在季度的第一天:
public class FirstDayOfQuarter implements TemporalAdjuster {@Overridepublic Temporal adjustInto(Temporal temporal) {final int currentQuarter = YearMonth.from(temporal).get(IsoFields.QUARTER_OF_YEAR);switch (currentQuarter) {case 1:return LocalDate.from(temporal).with(TemporalAdjusters.firstDayOfYear());case 2:return LocalDate.from(temporal).withMonth(Month.APRIL.getValue()).with(TemporalAdjusters.firstDayOfMonth());case 3:return LocalDate.from(temporal).withMonth(Month.JULY.getValue()).with(TemporalAdjusters.firstDayOfMonth());case 4:return LocalDate.from(temporal).withMonth(Month.OCTOBER.getValue()).with(TemporalAdjusters.firstDayOfMonth());default:return null; // 肯定不会执行到这里}}}
下面举个例子,看看如何使用调节器:
LocalDate now = LocalDate.now();Temporal fdoq = now.with(new FirstDayOfQuarter());System.out.println(fdoq);
这里的关键是 with() 方法,这段代码先读取一个 Temporal 对象,然后返回修改后的另一个对象。在处理不可变对象的 API 中经常会见到这种方式。
9.3.4 过时的日期和时间API
可惜,很多应用还没有转用 Java 8 中优秀的日期和时间库。所以,为了完整性,本节简要介绍一下以前的 Java 版本对日期和时间的支持(以 java.util.Date 类为基础)。
在 Java 8 环境中,别在使用过时的日期和时间类,尤其是
java.util.Date类。
在较旧的 Java 版本中没有 java.time 包,开发者只能依赖 java.util.Date 类提供的基础支持。以前,这是表示时间戳的唯一方式。虽然这个类的名称是 Date,但其实它为日期和时间都提供了相应的组件,因此也为很多程序员带来了大量困扰。
Date 类提供的过时支持有很多问题。
Date类的实现方式不正确。它表示的其实不是日期,更像是时间戳。因此需要使用不同的方式表示日期、日期和时间,以及瞬时时间戳。Date对象是可变的。创建日期的引用后,再次指向这个对象时可以修改它的值。Date类不符合 ISO-8601 标准。这是全球通用的日期标准,规定什么是有效的日期。Date类中有相当多的弃用方法。
这个版本的 JDK 使用两个构造方法创建 Date 对象:一个构造方法不接受参数,用于创建当前时间;另一个构造方法接受一个参数,即距 Epoch 时间 5 的毫秒数。
5Epoch 时间是 1970-01-01 00:00:00 UTC。——译者注
