8.2 根据现有实例创建日期和时间
问题
用户希望修改 Date-Time API 中某个类的现有实例。
方案
如果需要进行简单的增减操作,使用 plus 或 minus 方法;对于其他操作,使用 with 方法。
讨论
Date-Time API 中的所有实例都是不可变的。一旦创建 LocalDate、LocalTime、LocalDateTime 或 ZonedDateTime,就无法修改它们。这对保持线程安全而言十分有利,不过如何根据现有实例创建新的实例呢?
以 LocalDate 类为例,它定义了多种对日期进行增减操作的方法,包括:
LocalDate plusDays(long daysToAdd)LocalDate plusWeeks(long weeksToAdd)LocalDate plusMonths(long monthsToAdd)LocalDate plusYears(long yearsToAdd)
上述方法均返回一个新的 LocalDate,它是当前日期的副本,并添加了指定的值。
LocalTime 类也定义了类似的方法:
LocalTime plusNanos(long nanosToAdd)LocalTime plusSeconds(long secondsToAdd)LocalTime plusMinutes(long minutesToAdd)LocalTime plusHours(long hoursToAdd)
类似地,每种方法均返回一个新的 LocalTime,它是当前时间的副本,并添加了指定的值。此外,LocalDateTime 类囊括了 LocalDate 和 LocalTime 类中用于处理日期和时间增减的所有方法。例 8-6 显示了各种 plus 方法在 LocalDate 和 LocalTime 类中的应用。
例 8-6
plus方法在LocalDate和LocalTime类中的应用
- @Test
- public void localDatePlus() throws Exception {
- DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
- LocalDate start = LocalDate.of(2017, Month.FEBRUARY, 2);
LocalDate end = start.plusDays(3);assertEquals("2017-02-05", end.format(formatter));end = start.plusWeeks(5);assertEquals("2017-03-09", end.format(formatter));end = start.plusMonths(7);assertEquals("2017-09-02", end.format(formatter));end = start.plusYears(2);assertEquals("2019-02-02", end.format(formatter));}
@Test
public void localTimePlus() throws Exception {
DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_TIME;
LocalTime start = LocalTime.of(11, 30, 0, 0);LocalTime end = start.plusNanos(1_000_000);assertEquals("11:30:00.001", end.format(formatter));end = start.plusSeconds(20);assertEquals("11:30:20", end.format(formatter));end = start.plusMinutes(45);assertEquals("12:15:00", end.format(formatter));end = start.plusHours(5);assertEquals("16:30:00", end.format(formatter));}
不少类还包括其他两种形式的 plus 和 minus 方法。以 LocalDateTime 类为例,plus 和 minus 方法的签名如下:
- LocalDateTime plus(long amountToAdd, TemporalUnit unit)
- LocalDateTime plus(TemporalAmount amountToAdd)
- LocalDateTime minus(long amountToSubtract, TemporalUnit unit)
- LocalDateTime minus(TemporalAmount amountToSubtract)
对于 LocalDate 和 LocalDate 类,plus 和 minus 方法的格式与 LocalDateTime 类相同,具有相应的返回类型。有趣的是,不妨将 minus 方法视为具有否定形式的 plus 方法。
对传入 TemporalAmount 的方法而言,参数通常为 Period 或 Duration,但也可以是任何实现 TemporalAmount 接口的类型。该接口定义了 addTo 和 subtractFrom 两种方法:
Temporal addTo(Temporal temporal)Temporal subtractFrom(Temporal temporal)
跟踪调用栈(call stack)可以看到,调用 minus 委托给带有否定参数的 plus,而 plus 委托给 TemporalAmount.addTo(Temporal),TemporalAmount.addTo(Temporal) 再回调 plus(long, TemporalUnit),它将执行实际的操作 4。
4老天,多么间接的操作!
例 8-7 显示了 plus 和 minus 方法的相关应用。
例 8-7
plus和minus方法的应用
- @Test
- public void plus_minus() throws Exception {
- Period period = Period.of(2, 3, 4); // 两年3个月零4天
- LocalDateTime start = LocalDateTime.of(2017, Month.FEBRUARY, 2, 11, 30);
- LocalDateTime end = start.plus(period);
assertEquals("2019-05-06T11:30:00",end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));end = start.plus(3, ChronoUnit.HALF_DAYS);assertEquals("2017-02-03T23:30:00",end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));end = start.minus(period);assertEquals("2014-10-29T11:30:00",end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));end = start.minus(2, ChronoUnit.CENTURIES);assertEquals("1817-02-02T11:30:00",end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));end = start.plus(3, ChronoUnit.MILLENNIA);assertEquals("5017-02-02T11:30:00",end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));}
当 API 调用
TemporalUnit时,提供的实现类为ChronoUnit,它定义了许多方便的枚举常量(enum constant)可供使用。
此外,每种类都定义了一系列 with 方法,可以一次修改一个字段。
with 方法用于处理常用的日期和时间,某些方法颇为有趣。以 LocalDateTime 类为例:
- LocalDateTime withNano(int nanoOfSecond)
- LocalDateTime withSecond(int second)
- LocalDateTime withMinute(int minute)
- LocalDateTime withHour(int hour)
- LocalDateTime withDayOfMonth(int dayOfMonth)
- LocalDateTime withDayOfYear(int dayOfYear)
- LocalDateTime withMonth(int month)
- LocalDateTime withYear(int year)
例 8-8 显示了各种 with 方法的应用。
例 8-8
with方法在LocalDateTime类中的应用
- @Test
- public void with() throws Exception {
- LocalDateTime start = LocalDateTime.of(2017, Month.FEBRUARY, 2, 11, 30);
- LocalDateTime end = start.withMinute(45);
- assertEquals("2017-02-02T11:45:00",
- end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
end = start.withHour(16);assertEquals("2017-02-02T16:30:00",end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));end = start.withDayOfMonth(28);assertEquals("2017-02-28T11:30:00",end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));end = start.withDayOfYear(300);assertEquals("2017-10-27T11:30:00",end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));end = start.withYear(2020);assertEquals("2020-02-02T11:30:00",end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));}
@Test(expected = DateTimeException.class)
public void withInvalidDate() throws Exception {
LocalDateTime start = LocalDateTime.of(2017, Month.FEBRUARY, 2, 11, 30);
start.withDayOfMonth(29);
}
由于 2017 年并非闰年,无法将日期设置为 2 月 29 日,第二个测试将抛出 DateTimeException。
with 方法也可以传入 TemporalAdjuster 或 TemporalField:
- LocalDateTime with(TemporalAdjuster adjuster)
- LocalDateTime with(TemporalField field, long newValue)
传入 TemporalField 的 with 方法允许字段解析日期以使其有效。如例 8-9 所示,程序传入 1 月的最后一天,并尝试将月份改为 2 月(“2 月 31 日”)。此时,根据 Javadoc 的描述,系统将选择前一个有效日期,即 2 月的最后一天(2 月 28 日)。
例 8-9 月份调整(无效)
- @Test
- public void temporalField() throws Exception {
- LocalDateTime start = LocalDateTime.of(2017, Month.JANUARY, 31, 11, 30);
- LocalDateTime end = start.with(ChronoField.MONTH_OF_YEAR, 2);
- assertEquals("2017-02-28T11:30:00",
- end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
- }
可想而知,日期和时间的处理涉及一些相当复杂的规则,不过 Javadoc 对此做了系统而详尽的描述。
范例 8.3 将讨论传入 TemporalAdjuster 的 with 方法。
另见
有关 TemporalAdjuster 和 TemporalQuery 的讨论请参见范例 8.3。
当 API 调用 