8.3 调节器与查询
问题
给定一个时态值(temporal value),用户希望根据自定义逻辑对其进行调整,或检索给定值的相关信息。
方案
创建 TemporalAdjuster 或规划 TemporalQuery 接口。
讨论
TemporalAdjuster 和 TemporalQuery 接口中的类不仅提供使用 Date-Time API 中各种类的有趣方式,也提供有用的内置方法以实现用户自定义的方法。本范例将对此做讨论。
TemporalAdjuster的应用
TemporalAdjuster 接口定义了一个名为 adjustInto 的方法,它传入 Temporal 值作为参数,并返回调整后的值。而 TemporalAdjusters 类包括一系列用作静态方法的调节器(adjuster),或许能为开发带来一定便利。
以 LocalDateTime 类为例,我们可以通过时态对象(temporal object)上的 with 方法使用 TemporalAdjuster:
LocalDateTime with(TemporalAdjuster adjuster)
虽然也可以使用 TemporalAdjuster 接口定义的 adjustInto 方法,但 with 方法应作为首选。
我们首先讨论 TemporalAdjusters 类,它定义了多种便利的方法:
- static TemporalAdjuster firstDayOfNextMonth()
- static TemporalAdjuster firstDayOfNextYear()
- static TemporalAdjuster firstDayOfYear()
- static TemporalAdjuster firstInMonth(DayOfWeek dayOfWeek)
- static TemporalAdjuster lastDayOfMonth()
- static TemporalAdjuster lastDayOfYear()
- static TemporalAdjuster lastInMonth(DayOfWeek dayOfWeek)
- static TemporalAdjuster next(DayOfWeek dayOfWeek)
- static TemporalAdjuster nextOrSame(DayOfWeek dayOfWeek)
- static TemporalAdjuster previous(DayOfWeek dayOfWeek)
- static TemporalAdjuster previousOrSame(DayOfWeek dayOfWeek)
例 8-10 的用例显示了上述方法在实际开发中的应用。
例 8-10
TemporalAdjusters类定义的部分静态方法
- @Test
- public void adjusters() throws Exception {
- LocalDateTime start = LocalDateTime.of(2017, Month.FEBRUARY, 2, 11, 30);
- LocalDateTime end = start.with(TemporalAdjusters.firstDayOfNextMonth());
- assertEquals("2017-03-01T11:30", end.toString());
- end = start.with(TemporalAdjusters.next(DayOfWeek.THURSDAY));
- assertEquals("2017-02-09T11:30", end.toString());
- end = start.with(TemporalAdjusters.previousOrSame(DayOfWeek.THURSDAY));
- assertEquals("2017-02-02T11:30", end.toString());
- }
有趣之处在于编写自定义调节器。TemporalAdjuster 是一个函数式接口,所包含的单一抽象方法为:
Temporal adjustInto(Temporal temporal)
在讨论时态调节器(temporal adjuster)时,Java 官方教程以 PaydayAdjuster 类为例演示了自定义调节器的应用:假设员工在一个月中领取两次工资,且发薪日是每月 15 日和最后一天;如果某个发薪日为周末,则提前到周五。
为便于参考,例 8-11 完整复制了这个示例的代码。请注意,adjustInto 方法已被添加到实现 TemporalAdjuster 接口的 PaydayAdjuster 类中。
例 8-11
PaydayAdjuster类(取自 Java 官方教程)
- import java.time.DayOfWeek;
- import java.time.LocalDate;
- import java.time.temporal.Temporal;
- import java.time.temporal.TemporalAdjuster;
- import java.time.temporal.TemporalAdjusters;
- public class PaydayAdjuster implements TemporalAdjuster {
- public Temporal adjustInto(Temporal input) {
- LocalDate date = LocalDate.from(input); ➊
- int day;
- if (date.getDayOfMonth() < 15) {
- day = 15;
- } else {
- day = date.with(TemporalAdjusters.lastDayOfMonth())
- .getDayOfMonth();
- }
- date = date.withDayOfMonth(day);
- if (date.getDayOfWeek() == DayOfWeek.SATURDAY ||
- date.getDayOfWeek() == DayOfWeek.SUNDAY) {
- date = date.with(TemporalAdjusters.previous(DayOfWeek.FRIDAY));
- }
- return input.with(date);
- }
- }
➊ from 方法可以将任何时态对象转换为 LocalDate
以 2017 年 7 月为例运行程序,其中 7 月 15 日是周六,7 月 31 日是周一。例 8-12 的测试显示,调节器可以正确处理 2017 年 7 月的发薪日。
例 8-12 测试调节器
- @Test
- public void payDay() throws Exception {
- TemporalAdjuster adjuster = new PaydayAdjuster();
- IntStream.rangeClosed(1, 14)
- .mapToObj(day -> LocalDate.of(2017, Month.JULY, day))
- .forEach(date ->
- assertEquals(14, date.with(adjuster).getDayOfMonth()));
- IntStream.rangeClosed(15, 31)
- .mapToObj(day -> LocalDate.of(2017, Month.JULY, day))
- .forEach(date ->
- assertEquals(31, date.with(adjuster).getDayOfMonth()));
- }
虽然上述程序可以运行,但仍然存在改进的空间。首先,在 Java 8 之前,如果不使用其他机制就无法创建日期流(例如,本例需要计算天数)。这种情况在 Java 9 中得以改变。Java 9 新增了一种返回日期流的方法 datesUntil,详细讨论请参见范例 10.7。
其次,为实现 TemporalAdjuster 接口,程序创建了 PaydayAdjuster 类。由于 TemporalAdjuster 属于函数式接口,不妨改为提供 lambda 表达式或方法引用作为实现。
如例 8-13 所示,我们创建一个名为 Adjusters 的工具类,它包括进行各种操作所需的静态方法。
例 8-13 工具类 Adjusters
- public class Adjusters { ➊
- public static Temporal adjustInto(Temporal input) { ➋
- LocalDate date = LocalDate.from(input);
- // 与例8-11的实现相同
- return input.with(date);
- }
- }
❶ 不实现 TemporalAdjuster 接口
❷ 静态方法无须实例化
重写后的测试如例 8-14 所示。
例 8-14 测试调节器(使用方法引用)
- @Test
- public void payDayWithMethodRef() throws Exception {
- IntStream.rangeClosed(1, 14)
- .mapToObj(day -> LocalDate.of(2017, Month.JULY, day))
- .forEach(date ->
- assertEquals(14,
- date.with(Adjusters::adjustInto).getDayOfMonth())); ➊
- IntStream.rangeClosed(15, 31)
- .mapToObj(day -> LocalDate.of(2017, Month.JULY, day))
- .forEach(date ->
- assertEquals(31,
- date.with(Adjusters::adjustInto).getDayOfMonth()));
- }
➊ adjustInto 的方法引用
如果存在多个时态调节器,上述方案可能更为通用。
TemporalQuery的应用
TemporalQuery 接口用作时态对象中 query 方法的参数。以 LocalDate 类为例,query 方法的签名如下:
<R> R query(TemporalQuery<R> query)
query 方法调用 TemporalQuery.queryFrom(TemporalAccessor) 方法(传入 this 作为参数),并返回所需的查询。TemporalAccessor 接口定义的所有方法均可用于查询操作。
Date-Time API 还包括一个名为 TemporalQueries 的类,它定义了许多常见查询的常量:
- static TemporalQuery<Chronology> chronology()
- static TemporalQuery<LocalDate> localDate()
- static TemporalQuery<LocalTime> localTime()
- static TemporalQuery<ZoneOffset> offset()
- static TemporalQuery<TemporalUnit> precision()
- static TemporalQuery<ZoneId> zone()
- static TemporalQuery<ZoneId> zoneId()
例 8-15 的简单测试展示了部分方法的应用。
例 8-15
TemporalQueries类定义的部分方法
- @Test
- public void queries() throws Exception {
- assertEquals(ChronoUnit.DAYS,
- LocalDate.now().query(TemporalQueries.precision()));
- assertEquals(ChronoUnit.NANOS,
- LocalTime.now().query(TemporalQueries.precision()));
- assertEquals(ZoneId.systemDefault(),
- ZonedDateTime.now().query(TemporalQueries.zone()));
- assertEquals(ZoneId.systemDefault(),
- ZonedDateTime.now().query(TemporalQueries.zoneId()));
- }
与 TemporalAdjuster 接口类似,有趣之处在于编写自定义查询。TemporalQuery 接口包含的单一抽象方法为:
R queryFrom(TemporalAccessor temporal)
如果给定参数 TemporalAccessor,我们可以编写一个名为 daysUntilPirateDay 的方法,以计算指定日期与国际海盗模仿日(International Talk Like A Pirate Day,9 月 19 日)5 之间的天数,如例 8-16 所示。
例 8-16 计算指定日期与国际海盗模仿日之间的天数
- private long daysUntilPirateDay(TemporalAccessor temporal) {
- int day = temporal.get(ChronoField.DAY_OF_MONTH);
- int month = temporal.get(ChronoField.MONTH_OF_YEAR);
- int year = temporal.get(ChronoField.YEAR);
- LocalDate date = LocalDate.of(year, month, day);
- LocalDate tlapd = LocalDate.of(year, Month.SEPTEMBER, 19);
- if (date.isAfter(tlapd)) {
- tlapd = tlapd.plusYears(1);
- }
- return ChronoUnit.DAYS.between(date, tlapd);
- }
由于 daysUntilPirateDay 方法的签名与 TemporalQuery 接口包含的单一抽象方法 queryFrom 相互兼容,可以通过方法引用来调用,如例 8-17 所示。
例 8-17 通过方法引用使用
TemporalQuery
- @Test
- public void pirateDay() throws Exception {
- IntStream.range(10, 19)
- .mapToObj(n -> LocalDate.of(2017, Month.SEPTEMBER, n))
- .forEach(date ->
- assertTrue(date.query(this::daysUntilPirateDay) <= 9));
- IntStream.rangeClosed(20, 30)
- .mapToObj(n -> LocalDate.of(2017, Month.SEPTEMBER, n))
- .forEach(date -> {
- Long days = date.query(this::daysUntilPirateDay);
- assertTrue(days >= 354 && days < 365);
- });
- }
上述方案也可用于创建自定义查询。
5例如,“喂,伙计!我打算把你加入我的 LinkedIn 社交网络。”
