Java8 时间 API
Java8 中最为人津津乐道的新改变恐怕当属函数式 API 的加入。但实际上,Java8 所加入的新功能远不止这个。本文将基于《Java SE8 for the Really Impatient》的第 5 章,归纳一下 Java8 加入的位于 java.time 包下的日期和时间 API。
时间点与时间间隔
在我们常说的四维空间体系中,时间轴往往作为除长宽高三轴以外的第四轴。时间轴由无穷多个时间点组成,而两个时间点之间的距离组成一个时间间隔。相较于我们常说的日期、时间,时间点本身所携带的信息是很少的,不会携带如时区等冗余的信息。作为时间轴上的一个点,我们可以将它称为绝对时间。
Java8 引入了 Instant 类(瞬时)来表示时间轴上的一个点。Instant 的构造方法是 private 的,我们只能通过调用它的静态工厂方法来产生一个 Instant 实例。其中最常用的是 Instant.now() 方法,返回当前的时间点。Instant 类也实现了 comparesTo 和 equals 方法来对比两个瞬时点。
通过调用 Duration.between() 方法我们便可以计算两个时间点之间的时间间隔:
| 1 | Instant start = Instant.now(); | 
between 方法返回一个 Duration 实例。Duration 内部以 long 成员来存储时间间隔信息,最小单位可去到纳秒,同时提供了如 toMillis 、 toSeconds 等方法。
Instant 和 Duration 类常用的方法包括如下:
| 方法 | 描述 | 
|---|---|
| plus、minus | 对当前 Instant或Duration增加或减少一段时间 | 
| plusNanos、plusMillis、plusSeconds、plusMinutes、plusHours、plusDays | 根据指定的时间单位,对当前 Instant或者Duration添加一段时间 | 
| minusNanos、minusMillis、minusSeconds、minusMinutes、minusHours、minusDays | 根据指定的时间单位,对当前 Instant或者Duration减少一段时间 | 
| multipliedBy、dividedBy、negated | 返回当前 Duration与指定long值相乘或相除得到的时间间隔 | 
| isZero、isNegative | 检查 Duration是否为 0 或负数 | 
注意:
Instant类和Duration类都是不可变的,上述方法都会返回一个新的实例。
本地日期
在新的时间 API 中,Java 提供了两种时间格式:不带时区信息的本地时间和带时区的时间。本地日期表示一个日期,而本地时间还包含(一天中的)时间,但它们都不包含任何有关时区的信息。例如,June 14, 1903 就是一个本地日期。由于日期不含一天中的时间,也不含时区信息,所以它无法与一个准确的瞬时点对应。相反,July 16, 1969, 09:32:00 EDT 就是一个带时区的时间,它表示了时间轴上准确的一点。但有很多计算是不需要考虑时区的,在某些情况下考虑时区甚至可能导致错误的结果。出于此原因,API 设计者们更推荐使用不带时区的时间,除非你真的需要这个时区信息。
LocalDate 就是一个不带时区的本地日期:它只带有年份、月份和当月的天数。你可以通过 LocalDate 的静态工厂方法 now 或 of 来创建一个实例:
| 1 | LocalDate alonzosBirthday = LocalDate.of(1903, 6, 14); | 
这里我们看到,静态工厂方法中指示月份的数字是以 1 开始的,因此 6 就代表着六月。如果你实在是太喜欢以 0 开始,无法接受这种设定,你也可以使用枚举类型 Month 来指定月份。
下表中列出了 LocalDate 对象的一些常用方法。详细的方法说明请参考 LocalDate 的 JavaDoc。
| 方法 | 描述 | 
|---|---|
| now、of | 静态工厂方法,可以根据当前时间或指定的年月日来创建一个 LocalDate对象 | 
| plusDays、plusWeeks、plusMonths、plusYears | 返回在当前 LocalDate的基础上加上几天、几周、几个月或者几年后的新的LocalDate对象,原有的LocalDate对象保持不变 | 
| minusDays、minusWeeks、minusMonths、minusYears | 返回在当前 LocalDate的基础上减去几天、几周、几个月或者几年后的新的LocalDate对象,原有的LocalDate对象保持不变 | 
| plus、minus | 返回在当前 LocalDate的基础上加上或减去一个Duration或者Period的新的LocalDate对象,原有的LocalDate对象保持不变 | 
| withDayOfMonth、withDayOfYear、withMonth、withYear | 返回一个月份天数、年份天数、月份、年份修改为指定的值的新的 LocalDate对象,原有的LocalDate对象保持不变 | 
| getDayOfMonth | 获取月份天数(在 $[1,31]$ 之间) | 
| getDayOfYear | 获取年份天数(在 $[1,366]$ 之间) | 
| getDayOfWeek | 获取星期几(返回一个 DayOfWeek枚举值) | 
| getMonth、getMonthValue | 获取月份,返回一个 Month枚举的值,或者是 $[1,12]$ 之间的一个数字 | 
| getYear | 获取年份,在 $[-999999999,999999999]$ 之间 | 
| until | 获取两个日期之间的 Period对象,或者以指定ChronoUnits为单位的数值 | 
| isBefore、isAfter | 比较两个 LocalDate | 
| isLeapYear | 是否为闰年 | 
注意:
LocalDate类是不可变的,上述方法都会返回一个新的实例。
在上一节中我们提到,两个瞬时点 Instant 之间的是一个持续时间 Duration。对于本地时间,对应的对象就是时段 Period,它表示一段逝去的年月日。
本地时间
LocalTime 代表一天中的某个时间,例如下午 3 点 30 分。同样,你可以通过 LocalTime 的静态工厂方法 now 和 of 来创建一个实例。
| 1 | LocalTime rightNow = LocalTime.now(); | 
下表中列出了 LocalTime 对象的一些常用方法。详细的方法说明请参考 LocalTime 的 JavaDoc。
| 方法 | 描述 | 
|---|---|
| now、of | 静态工厂方法,可以根据当前时间或指定的时分秒来创建一个 LocalTime对象 | 
| plusHours、plusMinutes、plusSeconds、plusNanos | 返回在当前 LocalTime的基础上加上几小时、几分钟、几秒或者几纳秒后的新的LocalTime对象,原有的LocalTime对象保持不变 | 
| minusHours、minusMinutes、minusSeconds、minusNanos | 返回在当前 LocalTime的基础上减去几小时、几分钟、几秒或者几纳秒后的新的LocalTime对象,原有的LocalTime对象保持不变 | 
| plus、minus | 返回在当前 LocalTime的基础上加上或减去一个Duration的新的LocalTime对象,原有的LocalTime对象保持不变 | 
| withHour、withMinute、withSecond、withNano | 返回一个小时数、分钟数、秒数、纳秒数修改为指定的值的新的 LocalTime对象,原有的LocalTime对象保持不变 | 
| getHour、getMinute、getSecond、getNano | 返回该 LocalTime的小时、分钟、秒钟及纳秒值 | 
| isBefore、isAfter | 比较两个 LocalTime | 
注意:
LocalTime类是不可变的,上述方法都会返回一个新的实例。
LocalDateTime 类则可看作是 LocalDate 和 LocalTime 的结合。它用于存储本地时区中的某个时间点,包含当前的年月日等日期信息,同时也包含了时钟、分钟、秒钟等时间信息。同样,LocalDateTime 也是不可变的。
详细的方法说明请参考 LocalDateTime 的 JavaDoc。
带时区的时间
Java8 的时间 API 当然也加入了对时区的支持。分别对应着 LocalDate、LocalTime 和 LocalDateTime,带时区的时间类为 ZonedDate、ZonedTime、ZonedDateTime。
Java 中的时区信息来自于 IANA(Internet Assigned Numbers Authority)的数据库,其中每个时区都有着对应的 ID,例如 America/New_York 或者 Europe/Berlin。调用 ZoneId.getAvailableIds 方法即可获取所有可用的时区信息。
你还可以使用 ZoneId.of(id) 方法,用指定的时区 ID 来获取对应的 ZoneId 对象。通过调用 local.atZone(zoneId) 方法,你可以将一个 LocalDateTime 转换成一个 ZonedDateTime 对象,或者通过调用静态方法 ZonedDateTime.of 来创建一个对象。
ZonedDateTime 的许多方法都与 LocalDateTime 一致。下表中列出了 ZonedDateTime 特有的常用方法,详细的方法说明请参考 ZonedDateTime 的 JavaDoc。
| 方法 | 描述 | 
|---|---|
| now、of、ofInstant | 根据当前时间或指定的年月日时分秒、纳秒和 ZoneId,或者一个Instant和一个ZoneId来创建一个ZonedDateTime对象 | 
| withZoneSameInstant、withZoneSameLocal | 返回时区失去中的一个新的 ZonedDateTime对象,它表示相同的瞬时点或本地时间 | 
| getOffset | 获得与 UTC 之间的时差,返回一个 ZoneOffset对象 | 
| toLocalDate、toLocalTime、toInstant | 返回对应的本地日期、本地时间或瞬时点 | 
除此之外,Java8 还提供了一个 OffsetDateTime 类,用来表示带有(与 UTC 相比的)偏移量的时间。这个类专门用于一些不需要时区规则的业务场景,比如某些网络协议。对于人类可读的时间,ZonedDateTime 是更好的选择。
详情请查阅 OffsetDateTime 的 JavaDoc。
日期校正器
有些时候,我们可以能需要得到类似“每月的第一个星期二”这样的日期。Java8 提供了 TemporalAdjuster 接口,用以实现自定义的日期校正逻辑。通过将创建好的 TemporalAdjuster 传递给日期时间类的 with 方法便可在原有日期时间对象的基础上产生出一个符合要求的日期时间。例如,你可以通过如下代码来计算下一个星期二:
| 1 | TemporalAdjuster NEXT_TUESDAY = (Temporal temporal) -> { | 
此处利用 Lambda 表达式快速实现了一个匿名的 TemporalAdjuster 对象。注意 Lambda 表达式的参数类型为 Temporal,某些 LocalDate 或者 LocalDateTime 之类的类特有的方法将不可用,在使用前必须进行强制转换。你可以通过 ofDateAdjuster 方法和一个 UnaryOperator<LocalDate> 来避免强制转换:
| 1 | TermporalAdjuster NEXT_WORKDAY = TemporalAdjusters.ofDateAdjuster((LocalDate w) -> { | 
上述代码中使用的 ofDateAdjuster 方法来自类 TemporalAdjusters。实际上这个类通过静态方法提供了大量的常用 TemporalAdjuster 实现。比如,我们也可以通过如下代码来计算下一个星期二:
| 1 | LocalDate nextTuesDay = LocalDate.now().with( | 
下表中列出了 TemporalAdjusters 的一些常用方法。详细的方法说明请参考 TemporalAdjusters 的 JavaDoc。
| 方法 | 描述 | 
|---|---|
| previous(dayOfWeek)、next(dayOfWeek) | 返回被校正日期之后或之前最近的指定星期几 | 
| previoursOrSame(dayOfWeek)、nextOrSame(dayOfWeek) | 返回从被校正日期开始,之前或之后的指定星期几。如果被校正日期已吻合条件,被校正的日期实例将被直接返回 | 
| dayOfWeekInMonth(n, dayOfWeek) | 返回该月中指定的第几个星期几 | 
| firstInMonth(dayOfWeek)、lastInMonth(dayOfWeek) | 返回该月第一个或最后一个星期几 | 
| firstDayOfMonth()、firstDayOfNextMonth()、firstDayOfNextYear()、lastDayOfMonth()、lastDayOfPreviousMonth()、lastDayOfYear() | 返回方法名所描述的日期 | 
格式化和解析
除了日期校正,日期与字符串之间的相互转换也是十分常见的操作。对于原有的 java.util.Date 等类,我们使用 java.text.DateFormat 来对日期进行格式化和解析。对于 Java8 新引入的日期时间类,我们使用 java.time.format.DateTimeFormatter 类。
DateTimeFormatter 类提供了三种格式化方法来打印日期时间:
- 预定义的标准格式
- 语言环境相关的格式
- 自定义的格式
下表中列出了所有预定义的 DateTimeFormatter。详细说明可参考 DateTimeFormatter 的 JavaDoc。
| 格式 | 示例 | 
|---|---|
| BASIC_ISO_DATE | 20111203 | 
| ISO_LOCAL_DATEISO_LOCAL_TIMEISO_LOCAL_DATE_TIME | 2011-12-0310:15:302011-12-03T10:15:30 | 
| ISO_OFFSET_DATEISO_OFFSET_TIMEISO_OFFSET_DATE_TIME | 2011-12-03+01:0010:15:30+01:002011-12-03T10:15:30+01:00 | 
| ISO_ZONED_DATE_TIME | 2011-12-03T10:15:30+01:00[Europe/Paris] | 
| ISO_INSTANT | 2011-12-03T10:15:30Z | 
| ISO_ORDINAL_DATE | 2012-337 | 
| ISO_WEEK_DATE | 2012-W48-6 | 
| ISO_DATEISO_TIMEISO_DATE_TIME | 2011-12-03+01:00;2011-12-0310:15:30+01:00;10:15:302011-12-03T10:15:30+01:00[Europe/Paris] | 
| RFC_1123_DATE_TIME | Tue, 3 Jun 2008 11:05:30 GMT | 
通过调用 DateTimeFormatter 类的 format 方法即可对日期进行格式化:
| 1 | String formatted = DateTimeFormatter.ISO_DATE_TIME.format(apollolllaunch); | 
标准格式主要用于机器可读的时间戳。为了产生人类可读的日期和时间,你需要使用语言环境相关的格式。下表中列出了 Java8 提供的 4 种风格:
| 风格 | 日期 | 时间 | 
|---|---|---|
| SHORT | 7/16/69 | 9:32 AM | 
| MEDIUM | Jul 16, 1969 | 9:32:00 AM | 
| LONG | July 16, 1969 | 9:32:00 AM EDT | 
| FULL | Wednesday, July 16, 1969 | 9:32:00 AM EDT | 
你可以通过静态方法 ofLocalizedDate 、 ofLocalizedTime 和 ofLocalizedDateTime 来创建这些格式:
| 1 | DateTimeFormatter formatter = | 
这些方法使用的都是默认的语言环境。通过使用 withLocale 方法可以更改为其他语言环境:
| 1 | String formatted = formatter.withLocale(Locale.FRENCH).format(apollolllaunch); | 
你可以通过调用 formatter.toFormat() 方法来获取一个等效的 java.util.DateFormat 对象。
最后,你可以通过指定的模式来自定义日期的格式。例如:
| 1 | formatter = DateTimeFormatter.ofPattern("E yyyy-MM-dd HH:mm"); | 
其中不同的符号对应着不同的含义。下表中列出了不同符号的具体含义和实例,详情可查阅 DateTimeFormatter 的 JavaDoc。
| 含义 | 符号 | 示例 | 
|---|---|---|
| 纪元 | GGGGGGGGGG | ADAnno DominiA | 
| 年份 | yyyyyy | 691969 | 
| 月份 | MMMMMMMMMMMMMMM | 707JulJulyJ | 
| 日份 | ddd | 606 | 
| 星期几 | eEEEEEEEEEE | 3WedWednesdayW | 
| 24 小时制时钟($[0,23]$) | HHH | 909 | 
| 12 小时制时钟($[0,11]$) | KKK | 909 | 
| AM/PM | a | AM | 
| 分钟 | mm | 02 | 
| 秒钟 | ss | 00 | 
| 时区 ID | VV | America/New_York | 
| 时区名称 | zzzzz | EDTEastern Daylight Time | 
| 时差 | xxxxxxXXX | -04-0400-04:00-Z4:ZZ | 
| 本地化的时差 | OOOOO | GMT-4GMT-04:00 | 
要从一个字符串中解析出日期时间,可以使用静态方法 parse 的各个重载方法。例如:
| 1 | LocalDate churchsBirthday = LocalDate.parse("1903-06-14"); | 
与遗留代码互操作
尽管使用全新的 API 可以获得更好的开发体验,但兼容遗留代码总是不可避免的。因此,熟知新的日期时间类和旧的日期时间类之间的转换方法也是我们必须学习的。
总体来讲,转换规则可以归纳为下表:
| 类 | To 遗留类 | From 遗留类 | 
|---|---|---|
| java.time.Instantjava.util.Date | Date.from(instant) | date.toInstant() | 
| java.time.Instantjava.sql.Timestamp | Timestamp.from(instant) | timestamp.toInstant() | 
| java.time.Instantjava.nio.file.attribute.FileTime | FileTime.from(instant) | fileTime.toInstant() | 
| java.time.ZonedDateTimejava.util.GregorianCalendar | GregorianCalendar.from(zonedDateTime) | cal.toZonedDateTime() | 
| java.time.LocalDatejava.sql.Time | Date.valueOf(localDate) | date.toLocalDate() | 
| java.time.LocalTimejava.sql.Time | Date.valueOf(localDate) | date.toLocalTime() | 
| java.time.LocalDateTimejava.sql.Timestamp | Timestamp.valueOf(localDateTime) | timestamp.toLocalDateTime() | 
| java.time.ZoneIdjava.util.TimeZone | Timezone.getTimeZone(id) | timeZone.toZoneId() | 
| java.time.format.DateTimeFormatterjava.text.DateFormat | formatter.toFormat() | 无 | 
Java8 时间 API

