Java 编程问题:三、使用日期和时间

原文:Java Coding Problems

协议:CC BY-NC-SA 4.0

贡献者:飞龙

本文来自【ApacheCN Java 译文集】,自豪地采用谷歌翻译

本章包括 20 个涉及日期和时间的问题。这些问题通过DateCalendarLocalDateLocalTimeLocalDateTimeZoneDateTimeOffsetDateTimeOffsetTimeInstant等涵盖了广泛的主题(转换、格式化、加减、定义时段/持续时间、计算等)。到本章结束时,您将在确定日期和时间方面没有问题,同时符合您的应用的需要。本章介绍的基本问题将非常有助于了解日期-时间 API 的整体情况,并将像拼图中需要拼凑起来的部分一样解决涉及日期和时间的复杂挑战。

问题

使用以下问题来测试您的日期和时间编程能力。我强烈建议您在使用解决方案和下载示例程序之前,先尝试一下每个问题:

  1. 将字符串转换为日期和时间编写一个程序,演示字符串和日期/时间之间的转换。
  2. 格式化日期和时间:**解释日期和时间的格式模式。
  3. 获取当前日期/时间(不含日期/时间):编写程序,提取当前日期(不含时间或日期)。
  4. LocalDateLocalTimeLocalDateTime:编写一个程序,从LocalDate对象和LocalTime构建一个LocalDateTime。它将日期和时间组合在一个LocalDateTime对象中。
  5. 通过Instant类获取机器时间:解释并举例说明InstantAPI。
  6. 定义使用基于日期的值的时间段(Period)和使用基于时间的值的时间段(Duration):解释并举例说明PeriodDurationAPI 的用法。
  7. 获取日期和时间单位:编写一个程序,从表示日期时间的对象中提取日期和时间单位(例如,从日期中提取年、月、分钟等)。
  8. 对日期时间的加减:编写一个程序,对日期时间对象加减一定的时间(如年、日、分等)(如对日期加 1 小时,对LocalDateTime减 2 天等)。
  9. 获取 UTC 和 GMT 的所有时区:编写一个程序,显示 UTC 和 GMT 的所有可用时区。
  10. 获取所有可用时区的本地日期时间:编写一个程序,显示所有可用时区的本地时间。68. 显示航班日期时间信息:编写程序,显示 15 小时 30 分钟的航班时刻信息。更确切地说,是从澳大利亚珀斯飞往欧洲布加勒斯特的航班。
  11. 将 Unix 时间戳转换为日期时间:编写将 Unix 时间戳转换为java.util.Datejava.time.LocalDateTime的程序。
  12. 查找月份的第一天/最后一天:编写一个程序,通过 JDK8,TemporalAdjusters查找月份的第一天/最后一天。
  13. 定义/提取区域偏移:编写一个程序,展示定义和提取区域偏移的不同技术。
  14. DateTemporal之间的转换:编写DateInstantLocalDateLocalDateTime等之间的转换程序。
  15. 迭代一系列日期:编写一个程序,逐日(以一天的步长)迭代一系列给定日期。
  16. 计算年龄:编写一个计算一个人年龄的程序。
  17. 一天的开始和结束:编写一个程序,返回一天的开始和结束时间。
  18. 两个日期之间的差异:编写一个程序,计算两个日期之间的时间量(以天为单位)。
  19. 实现象棋时钟:编写实现象棋时钟的程序。

以下各节介绍上述问题的解决方案。记住,通常没有一个正确的方法来解决一个特定的问题。另外,请记住,这里显示的解释仅包括解决问题所需的最有趣和最重要的细节。下载示例解决方案以查看更多详细信息,并在这个页面中试用程序。

58 将字符串转换为日期和时间

String转换或解析为日期和时间可以通过一组parse()方法来完成。从日期和时间到String的转换可以通过toString()format()方法完成。

JDK8 之前

在 JDK8 之前,这个问题的典型解决方案依赖于抽象的DateFormat类的主扩展,名为SimpleDateFormat(这不是线程安全类)。在本书附带的代码中,有几个示例说明了如何使用此类。

从 JDK8 开始

从 JDK8 开始,SimpleDateFormat可以替换为一个新类—DateTimeFormatter。这是一个不可变(因此是线程安全的)类,用于打印和解析日期时间对象。这个类支持从预定义的格式化程序(表示为常量,如 ISO 本地时间2011-12-03,是ISO_LOCAL_DATE)到用户定义的格式化程序(依赖于一组用于编写自定义格式模式的符号)。

此外,除了Date类之外,JDK8 还提供了几个新类,它们专门用于处理日期和时间。其中一些类显示在下面的列表中(这些类也被称为临时类,因为它们实现了Temporal接口):

  • LocalDate(ISO-8601 日历系统中没有时区的日期)
  • LocalTime(ISO-8601 日历系统中无时区的时间)
  • LocalDateTime(ISO-8601 日历系统中无时区的日期时间)
  • ZonedDateTime(ISO-8601 日历系统中带时区的日期时间),依此类推
  • OffsetDateTime(在 ISO-8601 日历系统中,有 UTC/GMT 偏移的日期时间)
  • OffsetTime(在 ISO-8601 日历系统中与 UTC/GMT 有偏移的时间)

为了通过预定义的格式化程序将String转换为LocalDate,它应该遵循DateTimeFormatter.ISO_LOCAL_DATE模式,例如2020-06-01LocalDate提供了一种parse()方法,可以如下使用:

// 06 is the month, 01 is the day
LocalDate localDate = LocalDate.parse("2020-06-01");

类似地,在LocalTime的情况下,字符串应该遵循DateTimeFormatter.ISO_LOCAL_TIME模式;例如,10:15:30,如下面的代码片段所示:

LocalTime localTime = LocalTime.parse("12:23:44");

LocalDateTime的情况下,字符串应该遵循DateTimeFormatter.ISO_LOCAL_DATE_TIME模式,例如2020-06-01T11:20:15,如下代码片段所示:

LocalDateTime localDateTime 
  = LocalDateTime.parse("2020-06-01T11:20:15");

ZonedDateTime的情况下,字符串必须遵循DateTimeFormatter.ISO_ZONED_DATE_TIME模式,例如2020-06-01T10:15:30+09:00[Asia/Tokyo],如下代码片段所示:

ZonedDateTime zonedDateTime 
  = ZonedDateTime.parse("2020-06-01T10:15:30+09:00[Asia/Tokyo]");

OffsetDateTime的情况下,字符串必须遵循DateTimeFormatter.ISO_OFFSET_DATE_TIME模式,例如2007-12-03T10:15:30+01:00,如下代码片段所示:

OffsetDateTime offsetDateTime 
  = OffsetDateTime.parse("2007-12-03T10:15:30+01:00");

最后,在OffsetTime的情况下,字符串必须遵循DateTimeFormatter.ISO_OFFSET_TIME模式,例如10:15:30+01:00,如下代码片段所示:

OffsetTime offsetTime = OffsetTime.parse("10:15:30+01:00");

如果字符串不符合任何预定义的格式化程序,则是时候通过自定义格式模式使用用户定义的格式化程序了;例如,字符串01.06.2020表示需要用户定义格式化程序的日期,如下所示:

DateTimeFormatter dateFormatter 
  = DateTimeFormatter.ofPattern("dd.MM.yyyy");
LocalDate localDateFormatted 
  = LocalDate.parse("01.06.2020", dateFormatter);

但是,像12|23|44这样的字符串需要如下用户定义的格式化程序:

DateTimeFormatter timeFormatter 
  = DateTimeFormatter.ofPattern("HH|mm|ss");
LocalTime localTimeFormatted 
  = LocalTime.parse("12|23|44", timeFormatter);

01.06.2020, 11:20:15这样的字符串需要一个用户定义的格式化程序,如下所示:

DateTimeFormatter dateTimeFormatter 
  = DateTimeFormatter.ofPattern("dd.MM.yyyy, HH:mm:ss");
LocalDateTime localDateTimeFormatted 
  = LocalDateTime.parse("01.06.2020, 11:20:15", dateTimeFormatter);

01.06.2020, 11:20:15+09:00 [Asia/Tokyo]这样的字符串需要一个用户定义的格式化程序,如下所示:

DateTimeFormatter zonedDateTimeFormatter 
  = DateTimeFormatter.ofPattern("dd.MM.yyyy, HH:mm:ssXXXXX '['VV']'");
ZonedDateTime zonedDateTimeFormatted 
  = ZonedDateTime.parse("01.06.2020, 11:20:15+09:00 [Asia/Tokyo]", 
    zonedDateTimeFormatter);

2007.12.03, 10:15:30, +01:00这样的字符串需要一个用户定义的格式化程序,如下所示:

DateTimeFormatter offsetDateTimeFormatter 
  = DateTimeFormatter.ofPattern("yyyy.MM.dd, HH:mm:ss, XXXXX");
OffsetDateTime offsetDateTimeFormatted 
  = OffsetDateTime.parse("2007.12.03, 10:15:30, +01:00", 
    offsetDateTimeFormatter);

最后,像10 15 30 +01:00这样的字符串需要一个用户定义的格式化程序,如下所示:

DateTimeFormatter offsetTimeFormatter 
  = DateTimeFormatter.ofPattern("HH mm ss XXXXX");
OffsetTime offsetTimeFormatted 
  = OffsetTime.parse("10 15 30 +01:00", offsetTimeFormatter);

前面示例中的每个ofPattern()方法也支持Locale

LocalDateLocalDateTimeZonedDateTimeString的转换至少可以通过两种方式完成:

  • 依赖于LocalDateLocalDateTimeZonedDateTime.toString()方法(自动或显式)。请注意,依赖于toString()将始终通过相应的预定义格式化程序打印日期:
// 2020-06-01 results in ISO_LOCAL_DATE, 2020-06-01
String localDateAsString = localDate.toString();

// 01.06.2020 results in ISO_LOCAL_DATE, 2020-06-01
String localDateAsString = localDateFormatted.toString();

// 2020-06-01T11:20:15 results 
// in ISO_LOCAL_DATE_TIME, 2020-06-01T11:20:15
String localDateTimeAsString = localDateTime.toString();

// 01.06.2020, 11:20:15 results in 
// ISO_LOCAL_DATE_TIME, 2020-06-01T11:20:15
String localDateTimeAsString 
  = localDateTimeFormatted.toString();

// 2020-06-01T10:15:30+09:00[Asia/Tokyo] 
// results in ISO_ZONED_DATE_TIME,
// 2020-06-01T11:20:15+09:00[Asia/Tokyo]
String zonedDateTimeAsString = zonedDateTime.toString();

// 01.06.2020, 11:20:15+09:00 [Asia/Tokyo] 
// results in ISO_ZONED_DATE_TIME,
// 2020-06-01T11:20:15+09:00[Asia/Tokyo]
String zonedDateTimeAsString 
  = zonedDateTimeFormatted.toString();
  • 依靠DateTimeFormatter.format()方法。请注意,依赖于DateTimeFormatter.format()将始终使用指定的格式化程序打印日期/时间(默认情况下,时区将为null),如下所示:
// 01.06.2020
String localDateAsFormattedString 
  = dateFormatter.format(localDateFormatted);

// 01.06.2020, 11:20:15
String localDateTimeAsFormattedString 
  = dateTimeFormatter.format(localDateTimeFormatted);

// 01.06.2020, 11:20:15+09:00 [Asia/Tokyo]
String zonedDateTimeAsFormattedString 
  = zonedDateTimeFormatted.format(zonedDateTimeFormatter);

在讨论中添加一个明确的时区可以如下所示:

DateTimeFormatter zonedDateTimeFormatter 
  = DateTimeFormatter.ofPattern("dd.MM.yyyy, HH:mm:ssXXXXX '['VV']'")
    .withZone(ZoneId.of("Europe/Paris"));
ZonedDateTime zonedDateTimeFormatted 
  = ZonedDateTime.parse("01.06.2020, 11:20:15+09:00 [Asia/Tokyo]", 
    zonedDateTimeFormatter);

这次,字符串表示欧洲/巴黎时区中的日期/时间:

// 01.06.2020, 04:20:15+02:00 [Europe/Paris]
String zonedDateTimeAsFormattedString 
  = zonedDateTimeFormatted.format(zonedDateTimeFormatter);

59 格式化日期和时间

前面的问题包含一些通过SimpleDateFormat.format()DateTimeFormatter.format()格式化日期和时间的风格。为了定义格式模式,开发人员必须了解格式模式语法。换句话说,开发人员必须知道 Java 日期时间 API 使用的一组符号,以便识别有效的格式模式。

大多数符号与SimpleDateFormat(JDK8 之前)和DateTimeFormatter(从 JDK8 开始)通用。下表列出了 JDK 文档中提供的最常见符号的完整列表:

字母 含义 演示 示例
y 1994; 94
M 数字/文本 7; 07; Jul; July; J
W 每月的一周 数字 4
E 星期几 文本 Tue; Tuesday; T
d 日期 数字 15
H 小时 数字 22
m 分钟 数字 34
s 数字 55
S 秒的分数 数字 345
z 时区名称 时区名称 Pacific Standard Time; PST
Z 时区偏移 时区偏移 -0800
V 时区 ID(JDK8) 时区 ID America/Los_Angeles; Z; -08:30

下表提供了一些格式模式示例:

模式 示例
yyyy-MM-dd 2019-02-24
MM-dd-yyyy 02-24-2019
MMM-dd-yyyy Feb-24-2019
dd-MM-yy 24-02-19
dd.MM.yyyy 24.02.2019
yyyy-MM-dd HH:mm:ss 2019-02-24 11:26:26
yyyy-MM-dd HH:mm:ssSSS 2019-02-24 11:36:32743
yyyy-MM-dd HH:mm:ssZ 2019-02-24 11:40:35+0200
yyyy-MM-dd HH:mm:ss z 2019-02-24 11:45:03 EET
E MMM yyyy HH:mm:ss.SSSZ Sun Feb 2019 11:46:32.393+0200
yyyy-MM-dd HH:MM:ss VV(JDK8) 2019-02-24 11:45:41 Europe/Athens

在 JDK8 之前,可以通过SimpleDateFormat应用格式模式:

// yyyy-MM-dd
Date date = new Date();
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
String stringDate = formatter.format(date);

从 JDK8 开始,可以通过DateTimeFormatter应用格式模式:

  • 对于LocalDate(ISO-8601 日历系统中没有时区的日期):
// yyyy-MM-dd
LocalDate localDate = LocalDate.now();
DateTimeFormatter formatterLocalDate 
  = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String stringLD = formatterLocalDate.format(localDate);

// or shortly
String stringLD = LocalDate.now()
  .format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
  • 对于LocalTime(ISO-8601 日历系统中没有时区的时间):
// HH:mm:ss
LocalTime localTime = LocalTime.now();
DateTimeFormatter formatterLocalTime 
  = DateTimeFormatter.ofPattern("HH:mm:ss");
String stringLT 
  = formatterLocalTime.format(localTime);

// or shortly
String stringLT = LocalTime.now()
  .format(DateTimeFormatter.ofPattern("HH:mm:ss"));
  • 对于LocalDateTime(ISO-8601 日历系统中没有时区的日期时间):
// yyyy-MM-dd HH:mm:ss
LocalDateTime localDateTime = LocalDateTime.now();
DateTimeFormatter formatterLocalDateTime 
  = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String stringLDT 
  = formatterLocalDateTime.format(localDateTime);

// or shortly
String stringLDT = LocalDateTime.now()
  .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
  • 对于ZonedDateTime(ISO-8601 日历系统中带时区的日期时间):
// E MMM yyyy HH:mm:ss.SSSZ
ZonedDateTime zonedDateTime = ZonedDateTime.now();
DateTimeFormatter formatterZonedDateTime 
  = DateTimeFormatter.ofPattern("E MMM yyyy HH:mm:ss.SSSZ");
String stringZDT 
  = formatterZonedDateTime.format(zonedDateTime);

// or shortly
String stringZDT = ZonedDateTime.now()
  .format(DateTimeFormatter
    .ofPattern("E MMM yyyy HH:mm:ss.SSSZ"));
  • 对于OffsetDateTime(在 ISO-8601 日历系统中,与 UTC/GMT 有偏移的日期时间):
// E MMM yyyy HH:mm:ss.SSSZ
OffsetDateTime offsetDateTime = OffsetDateTime.now();
DateTimeFormatter formatterOffsetDateTime 
  = DateTimeFormatter.ofPattern("E MMM yyyy HH:mm:ss.SSSZ");
String odt1 = formatterOffsetDateTime.format(offsetDateTime);

// or shortly
String odt2 = OffsetDateTime.now()
  .format(DateTimeFormatter
    .ofPattern("E MMM yyyy HH:mm:ss.SSSZ"));
  • 对于OffsetTime(在 ISO-8601 日历系统中与 UTC/GMT 有偏移的时间):
// HH:mm:ss,Z
OffsetTime offsetTime = OffsetTime.now();
DateTimeFormatter formatterOffsetTime 
  = DateTimeFormatter.ofPattern("HH:mm:ss,Z");
String ot1 = formatterOffsetTime.format(offsetTime);

// or shortly
String ot2 = OffsetTime.now()
  .format(DateTimeFormatter.ofPattern("HH:mm:ss,Z"));

60 获取没有时间/日期的当前日期/时间

在 JDK8 之前,解决方案必须集中在java.util.Date类上。绑定到本书的代码包含此解决方案。

从 JDK8 开始,日期和时间可以通过专用类LocalDateLocalTimejava.time包中获得:

// 2019-02-24
LocalDate onlyDate = LocalDate.now();

// 12:53:28.812637300
LocalTime onlyTime = LocalTime.now();

61 LocalDateLocalTime中的LocalDateTime

LocalDateTime类公开了一系列of()方法,这些方法可用于获取LocalDateTime的不同类型的实例。例如,从年、月、日、时、分、秒或纳秒获得的LocalDateTime类如下所示:

LocalDateTime ldt = LocalDateTime.of​(2020, 4, 1, 12, 33, 21, 675);

因此,前面的代码将日期和时间组合为of()方法的参数。为了将日期和时间组合为对象,解决方案可以利用以下of()方法:

public static LocalDateTime of​(LocalDate date, LocalTime time)

这导致LocalDateLocalTime,如下所示:

LocalDate localDate = LocalDate.now(); // 2019-Feb-24
LocalTime localTime = LocalTime.now(); // 02:08:10 PM

它们可以组合在一个对象LocalDateTime中,如下所示:

LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime);

格式化LocalDateTime显示日期和时间如下:

// 2019-Feb-24 02:08:10 PM
String localDateTimeAsString = localDateTime
  .format(DateTimeFormatter.ofPattern("yyyy-MMM-dd hh:mm:ss a"));

62 通过Instant类的机器时间

JDK8 附带了一个新类,名为java.time.Instant。主要地,Instant类表示时间线上的一个瞬时点,从 1970 年 1 月 1 日(纪元)的第一秒开始,在 UTC 时区,分辨率为纳秒。

Java8Instant类在概念上类似于java.util.Date。两者都代表 UTC 时间线上的一个时刻。当Instant的分辨率高达纳秒时,java.util.Date的分辨率为毫秒。

这个类对于生成机器时间的时间戳非常方便。为了获得这样的时间戳,只需调用如下的now()方法:

// 2019-02-24T15:05:21.781049600Z
Instant timestamp = Instant.now();

使用以下代码段可以获得类似的输出:

OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);

或者,使用以下代码段:

Clock clock = Clock.systemUTC();

调用Instant.toString()产生一个输出,该输出遵循 ISO-8601 标准来表示日期和时间。

将字符串转换为Instant

遵循 ISO-8601 标准表示日期和时间的字符串可以通过Instant.parse()方法轻松转换为Instant,如下例所示:

// 2019-02-24T14:31:33.197021300Z
Instant timestampFromString =
  Instant.parse("2019-02-24T14:31:33.197021300Z");

Instant添加/减去时间

对于添加时间,Instant有一套方法。例如,向当前时间戳添加 2 小时可以如下完成:

Instant twoHourLater = Instant.now().plus(2, ChronoUnit.HOURS);

在减去时间方面,例如 10 分钟,请使用以下代码段:

Instant tenMinutesEarlier = Instant.now()
  .minus(10, ChronoUnit.MINUTES);

plus()方法外,Instant还包含plusNanos()plusMillis()plusSeconds()。此外,除了minus()方法外,Instant还包含minusNanos()minusMillis()minusSeconds()

比较Instant对象

比较两个Instant对象可以通过Instant.isAfter()Instant.isBefore()方法来完成。例如,让我们看看以下两个Instant对象:

Instant timestamp1 = Instant.now();
Instant timestamp2 = timestamp1.plusSeconds(10);

检查timestamp1是否在timestamp2之后:

boolean isAfter = timestamp1.isAfter(timestamp2); // false

检查timestamp1是否在timestamp2之前:

boolean isBefore = timestamp1.isBefore(timestamp2); // true

两个Instant对象之间的时差可以通过Instant.until()方法计算:

// 10 seconds
long difference = timestamp1.until(timestamp2, ChronoUnit.SECONDS);

InstantLocalDateTimeZonedDateTimeOffsetDateTime之间转换

这些常见的转换可以在以下示例中完成:

  • InstantLocalDateTime之间转换-因为LocalDateTime不知道时区,所以使用零偏移 UTC+0:
// 2019-02-24T15:27:13.990103700
LocalDateTime ldt = LocalDateTime.ofInstant(
  Instant.now(), ZoneOffset.UTC);

// 2019-02-24T17:27:14.013105Z
Instant instantLDT = LocalDateTime.now().toInstant(ZoneOffset.UTC);
  • InstantZonedDateTime之间转换—将InstantUTC+0 转换为巴黎ZonedDateTimeUTC+1:
// 2019-02-24T16:34:36.138393100+01:00[Europe/Paris]
ZonedDateTime zdt = Instant.now().atZone(ZoneId.of("Europe/Paris"));

// 2019-02-24T16:34:36.150393800Z
Instant instantZDT = LocalDateTime.now()
  .atZone(ZoneId.of("Europe/Paris")).toInstant();
  • InstantOffsetDateTime之间转换-指定 2 小时的偏移量:
// 2019-02-24T17:34:36.151393900+02:00
OffsetDateTime odt = Instant.now().atOffset(ZoneOffset.of("+02:00"));

// 2019-02-24T15:34:36.153394Z
Instant instantODT = LocalDateTime.now()
  .atOffset(ZoneOffset.of("+02:00")).toInstant();

63 使用基于日期的值定义时段,使用基于时间的值定义持续时间

JDK8 附带了两个新类,分别命名为java.time.Periodjava.time.Duration。让我们在下一节中详细了解它们。

使用基于日期的值的时间段

Period类意味着使用基于日期的值(年、月、周和天)来表示时间量。这段时间可以用不同的方法获得。例如,120 天的周期可以如下获得:

Period fromDays = Period.ofDays(120); // P120D

ofDays()方法旁边,Period类还有ofMonths()ofWeeks()ofYears()

或者,通过of()方法可以得到 2000 年 11 个月 24 天的期限,如下所示:

Period periodFromUnits = Period.of(2000, 11, 24); // P2000Y11M24D

Period也可以从LocalDate中得到:

LocalDate localDate = LocalDate.now();
Period periodFromLocalDate = Period.of(localDate.getYear(),
  localDate.getMonthValue(), localDate.getDayOfMonth());

最后,可以从遵循 ISO-8601 周期格式PnYnMnDPnWString对象获得Period。例如,P2019Y2M25D字符串表示 2019 年、2 个月和 25 天:

Period periodFromString = Period.parse("P2019Y2M25D");

调用Period.toString()将返回时间段,同时也遵循 ISO-8601 时间段格式,PnYnMnDPnW(例如P120DP2000Y11M24D)。

但是,当Period被用来表示两个日期之间的一段时间(例如LocalDate时,Period的真实力量就显现出来了。2018 年 3 月 12 日至 2019 年 7 月 20 日期间可表示为:

LocalDate startLocalDate = LocalDate.of(2018, 3, 12);
LocalDate endLocalDate = LocalDate.of(2019, 7, 20);
Period periodBetween = Period.between(startLocalDate, endLocalDate);

年、月、日的时间量可以通过Period.getYears()Period.getMonths()Period.getDays()获得。例如,以下辅助方法使用这些方法将时间量输出为字符串:

public static String periodToYMD(Period period) {

  StringBuilder sb = new StringBuilder();

  sb.append(period.getYears())
   .append("y:")
   .append(period.getMonths())
   .append("m:")
   .append(period.getDays())
   .append("d");

 return sb.toString();
}

我们将此方法称为periodBetween(差值为 1 年 4 个月 8 天):

periodToYMD(periodBetween); // 1y:4m:8d

当确定某个日期是否早于另一个日期时,Period类也很有用。有一个标志方法,名为isNegative()。有一个A周期和一个B周期,如果BA之前,应用Period.between(A, B)的结果可以是负的,如果AB之前,应用isNegative()的结果可以是正的,如果BA之前,falseA之前,则isNegative()返回true B,如我们的例子所示(基本上,如果年、月或日为负数,此方法返回false):

// returns false, since 12 March 2018 is earlier than 20 July 2019
periodBetween.isNegative();

最后,Period可以通过加上或减去一段时间来修改。方法有plusYears()plusMonths()plusDays()minusYears()minusMonths()minusDays()等。例如,在periodBetween上加 1 年可以如下操作:

Period periodBetweenPlus1Year = periodBetween.plusYears(1L);

添加两个Period类可以通过Period.plus()方法完成,如下所示:

Period p1 = Period.ofDays(5);
Period p2 = Period.ofDays(20);
Period p1p2 = p1.plus(p2); // P25D

使用基于时间的值的持续时间

Duration类意味着使用基于时间的值(小时、分钟、秒或纳秒)来表示时间量。这种持续时间可以通过不同的方式获得。例如,可以如下获得 10 小时的持续时间:

Duration fromHours = Duration.ofHours(10); // PT10H

ofHours()方法旁边,Duration类还有ofDays()ofMillis()ofMinutes()ofSeconds()ofNanos()

或者,可以通过of()方法获得 3 分钟的持续时间,如下所示:

Duration fromMinutes = Duration.of(3, ChronoUnit.MINUTES); // PT3M

Duration也可以从LocalDateTime中得到:

LocalDateTime localDateTime 
  = LocalDateTime.of(2018, 3, 12, 4, 14, 20, 670);

// PT14M
Duration fromLocalDateTime 
  = Duration.ofMinutes(localDateTime.getMinute());

也可从LocalTime中获得:

LocalTime localTime = LocalTime.of(4, 14, 20, 670);

// PT0.00000067S
Duration fromLocalTime = Duration.ofNanos(localTime.getNano());

最后,可以从遵循 ISO-8601 持续时间格式PnDTnHnMn.nSString对象获得Duration,其中天被认为正好是 24 小时。例如,P2DT3H4M字符串有 2 天 3 小时 4 分钟:

Duration durationFromString = Duration.parse("P2DT3H4M");

调用Duration.toString()将返回符合 ISO-8601 持续时间格式的持续时间PnDTnHnMn.nS(例如,PT10HPT3MPT51H4M)。

但是,与Period的情况一样,当Duration用于表示两次之间的时间段(例如,Instant时,揭示了它的真实功率。从 2015 年 11 月 3 日 12:11:30 到 2016 年 12 月 6 日 15:17:10 之间的持续时间可以表示为两个Instant类之间的差异,如下所示:

Instant startInstant = Instant.parse("2015-11-03T12:11:30.00Z");
Instant endInstant = Instant.parse("2016-12-06T15:17:10.00Z");

// PT10059H5M40S
Duration durationBetweenInstant 
  = Duration.between(startInstant, endInstant);

以秒为单位,可通过Duration.getSeconds()方法获得该差值:

durationBetweenInstant.getSeconds(); // 36212740 seconds

或者,从 2018 年 3 月 12 日 04:14:20.000000670 到 2019 年 7 月 20 日 06:10:10.000000720 之间的持续时间可以表示为两个LocalDateTime对象之间的差异,如下所示:

LocalDateTime startLocalDateTime 
  = LocalDateTime.of(2018, 3, 12, 4, 14, 20, 670);
LocalDateTime endLocalDateTime 
  = LocalDateTime.of(2019, 7, 20, 6, 10, 10, 720);
// PT11881H55M50.00000005S, or 42774950 seconds
Duration durationBetweenLDT 
  = Duration.between(startLocalDateTime, endLocalDateTime);

最后,04:14:20.000000670 和 06:10:10.000000720 之间的持续时间可以表示为两个LocalTime对象之间的差异,如下所示:

LocalTime startLocalTime = LocalTime.of(4, 14, 20, 670);
LocalTime endLocalTime = LocalTime.of(6, 10, 10, 720);

// PT1H55M50.00000005S, or 6950 seconds
Duration durationBetweenLT 
  = Duration.between(startLocalTime, endLocalTime);

在前面的例子中,Duration通过Duration.getSeconds()方法以秒表示,这是Duration类中的秒数。然而,Duration类包含一组方法,这些方法专用于通过toDays()以天为单位、通过toHours()以小时为单位、通过toMinutes()以分钟为单位、通过toMillis()以毫秒为单位、通过toNanos()以纳秒为单位来表达Duration

从一个时间单位转换到另一个时间单位可能会产生残余。例如,从秒转换为分钟可能导致秒的剩余(例如,65 秒是 1 分钟,5 秒是剩余)。残差可以通过以下一组方法获得:天残差通过toDaysPart(),小时残差通过toHoursPart(),分钟残差通过toMinutesPart()等等。

假设差异应该显示为天:小时:分:秒:纳秒(例如,9d:2h:15m:20s:230n)。将toFoo()toFooPart()方法的力结合在一个辅助方法中将产生以下代码:

public static String durationToDHMSN(Duration duration) {

  StringBuilder sb = new StringBuilder();
  sb.append(duration.toDays())
    .append("d:")
    .append(duration.toHoursPart())
    .append("h:")
    .append(duration.toMinutesPart())
    .append("m:")
    .append(duration.toSecondsPart())
    .append("s:")
    .append(duration.toNanosPart())
    .append("n");

  return sb.toString();
}

让我们调用这个方法durationBetweenLDT(差别是 495 天 1 小时 55 分 50 秒 50 纳秒):

// 495d:1h:55m:50s:50n
durationToDHMSN(durationBetweenLDT);

Period类相同,Duration类有一个名为isNegative()的标志方法。当确定某个特定时间是否早于另一个时间时,此方法很有用。有持续时间A和持续时间B,如果BA之前,应用Duration.between(A, B)的结果可以是负的,如果AB之前,应用Duration.between(A, B)的结果可以是正的,进一步逻辑,isNegative()如果BA之前,则返回true,如果AB之前,则返回false,如以下情况:

durationBetweenLT.isNegative(); // false

最后,Duration可以通过增加或减少持续时间来修改。有plusDays()plusHours()plusMinutes()plusMillis()plusNanos()minusDays()minusHours()minusMinutes()minusMillis()minusNanos()等方法来执行此操作。例如,向durationBetweenLT添加 5 小时可以如下所示:

Duration durationBetweenPlus5Hours = durationBetweenLT.plusHours(5);

添加两个Duration类可以通过Duration.plus()方法完成,如下所示:

Duration d1 = Duration.ofMinutes(20);
Duration d2 = Duration.ofHours(2);

Duration d1d2 = d1.plus(d2);

System.out.println(d1 + "+" + d2 + "=" + d1d2); // PT2H20M

64 获取日期和时间单位

对于Date对象,解决方案可能依赖于Calendar实例。绑定到本书的代码包含此解决方案。

对于 JDK8 类,Java 提供了专用的getFoo()方法和get​(TemporalField field)方法。例如,假设下面的LocalDateTime对象:

LocalDateTime ldt = LocalDateTime.now();

依靠getFoo()方法,我们得到如下代码:

int year = ldt.getYear();
int month = ldt.getMonthValue();
int day = ldt.getDayOfMonth();
int hour = ldt.getHour();
int minute = ldt.getMinute();
int second = ldt.getSecond();
int nano = ldt.getNano();

或者,依赖于get​(TemporalField field)结果如下:

int yearLDT = ldt.get(ChronoField.YEAR);
int monthLDT = ldt.get(ChronoField.MONTH_OF_YEAR);
int dayLDT = ldt.get(ChronoField.DAY_OF_MONTH);
int hourLDT = ldt.get(ChronoField.HOUR_OF_DAY);
int minuteLDT = ldt.get(ChronoField.MINUTE_OF_HOUR);
int secondLDT = ldt.get(ChronoField.SECOND_OF_MINUTE);
int nanoLDT = ldt.get(ChronoField.NANO_OF_SECOND);

请注意,月份是从 1 开始计算的,即 1 月。

例如,2019-02-25T12:58:13.109389100LocalDateTime对象可以被切割成日期时间单位,结果如下:

Year: 2019 Month: 2 Day: 25 Hour: 12 Minute: 58 Second: 13 Nano: 109389100

通过一点直觉和文档,很容易将此示例改编为LocalDateLocalTimeZonedDateTime和其他示例。

65 日期时间的加减

这个问题的解决方案依赖于专用于处理日期和时间的 Java API。让我们在下一节中看看它们。

使用Date

对于Date对象,解决方案可能依赖于Calendar实例。绑定到本书的代码包含此解决方案。

使用LocalDateTime

跳转到 JDK8,重点是LocalDateLocalTimeLocalDateTimeInstant等等。新的 Java 日期时间 API 提供了专门用于加减时间量的方法。LocalDateLocalTimeLocalDateTimeZonedDateTimeOffsetDateTimeInstantPeriodDuration以及许多其他方法,如plusFoo()minusFoo(),其中Foo可以用单位替换时间(例如,plusYears()plusMinutes()minusHours()minusSeconds()等等)。

假设如下LocalDateTime

// 2019-02-25T14:55:06.651155500
LocalDateTime ldt = LocalDateTime.now();

加 10 分钟和调用LocalDateTime.plusMinutes(long minutes)一样简单,减 10 分钟和调用LocalDateTime.minusMinutes(long minutes)一样简单:

LocalDateTime ldtAfterAddingMinutes = ldt.plusMinutes(10);
LocalDateTime ldtAfterSubtractingMinutes = ldt.minusMinutes(10);

输出将显示以下日期:

After adding 10 minutes: 2019-02-25T15:05:06.651155500
After subtracting 10 minutes: 2019-02-25T14:45:06.651155500

除了每个时间单位专用的方法外,这些类还支持plus/minus(TemporalAmount amountToAdd)plus/minus(long amountToAdd, TemporalUnit unit)

现在,让我们关注Instant类。除了plus/minusSeconds()plus/minusMillis()plus/minusNanos()之外,Instant类还提供了plus/minus(TemporalAmount amountToAdd)方法。

为了举例说明这个方法,我们假设如下Instant

// 2019-02-25T12:55:06.654155700Z
Instant timestamp = Instant.now();

现在,让我们加减 5 个小时:

Instant timestampAfterAddingHours 
  = timestamp.plus(5, ChronoUnit.HOURS);
Instant timestampAfterSubtractingHours 
  = timestamp.minus(5, ChronoUnit.HOURS);

输出将显示以下Instant

After adding 5 hours: 2019-02-25T17:55:06.654155700Z
After subtracting 5 hours: 2019-02-25T07:55:06.654155700Z

66 使用 UTC 和 GMT 获取所有时区

UTC 和 GMT 被认为是处理日期和时间的标准参考。今天,UTC 是首选的方法,但是 UTC 和 GMT 在大多数情况下应该返回相同的结果。

为了获得 UTC 和 GMT 的所有时区,解决方案应该关注 JDK8 前后的实现。所以,让我们从 JDK8 之前有用的解决方案开始。

JDK8 之前

解决方案需要提取可用的时区 ID(非洲/巴马科、欧洲/贝尔格莱德等)。此外,每个时区 ID 都应该用来创建一个TimeZone对象。最后,解决方案需要提取特定于每个时区的偏移量,并考虑到夏令时。绑定到本书的代码包含此解决方案。

从 JDK8 开始

新的 Java 日期时间 API 为解决这个问题提供了新的工具。

在第一步,可用的时区 id 可以通过ZoneId类获得,如下所示:

Set zoneIds = ZoneId.getAvailableZoneIds();

在第二步,每个时区 ID 都应该用来创建一个ZoneId实例。这可以通过ZoneId.of(String zoneId)方法实现:

ZoneId zoneid = ZoneId.of(current_zone_Id);

在第三步,每个ZoneId可用于获得特定于所识别区域的时间。这意味着需要一个“实验室老鼠”参考日期时间。此参考日期时间(无时区,LocalDateTime.now())通过LocalDateTime.atZone()与给定时区(ZoneId)组合,以获得ZoneDateTime(可识别时区的日期时间):

LocalDateTime now = LocalDateTime.now();
ZonedDateTime zdt = now.atZone(ZoneId.of(zone_id_instance));

atZone()方法尽可能地匹配日期时间,同时考虑时区规则,例如夏令时。

在第四步,代码可以利用ZonedDateTime来提取 UTC 偏移量(例如,对于欧洲/布加勒斯特,UTC 偏移量为+02:00):

String utcOffset = zdt.getOffset().getId().replace("Z", "+00:00");

getId()方法返回规范化区域偏移 ID,+00:00偏移作为Z字符返回;因此代码需要快速将Z替换为+00:00,以便与其他偏移对齐,这些偏移遵循+hh:mm+hh:mm:ss格式。

现在,让我们将这些步骤合并到一个辅助方法中:

public static List fetchTimeZones(OffsetType type) {

  List timezones = new ArrayList<>();
  Set zoneIds = ZoneId.getAvailableZoneIds();
  LocalDateTime now = LocalDateTime.now();

  zoneIds.forEach((zoneId) -> {
    timezones.add("(" + type + now.atZone(ZoneId.of(zoneId))
      .getOffset().getId().replace("Z", "+00:00") + ") " + zoneId);
  });

  return timezones;
}

假设此方法存在于DateTimes类中,则获得以下代码:

List timezones 
  = DateTimes.fetchTimeZones(DateTimes.OffsetType.GMT);
Collections.sort(timezones); // optional sort
timezones.forEach(System.out::println);

此外,还显示了一个输出快照,如下所示:

(GMT+00:00) Africa/Abidjan
(GMT+00:00) Africa/Accra
(GMT+00:00) Africa/Bamako
...
(GMT+11:00) Australia/Tasmania
(GMT+11:00) Australia/Victoria
...

67 获取所有可用时区中的本地日期时间

可通过以下步骤获得此问题的解决方案:

  1. 获取本地日期和时间。
  2. 获取可用时区。
  3. 在 JDK8 之前,使用SimpleDateFormatsetTimeZone()方法。
  4. 从 JDK8 开始,使用ZonedDateTime

JDK8 之前

在 JDK8 之前,获取当前本地日期时间的快速解决方案是调用Date空构造器。此外,还可以使用Date在所有可用的时区中显示,这些时区可以通过TimeZone类获得。绑定到本书的代码包含此解决方案。

从 JDK8 开始

从 JDK8 开始,获取默认时区中当前本地日期时间的一个方便解决方案是调用ZonedDateTime.now()方法:

ZonedDateTime zlt = ZonedDateTime.now();

所以,这是默认时区中的当前日期。此外,该日期应显示在通过ZoneId类获得的所有可用时区中:

Set zoneIds = ZoneId.getAvailableZoneIds();

最后,代码可以循环zoneIds,对于每个区域 ID,可以调用ZonedDateTime.withZoneSameInstant(ZoneId zone)方法。此方法返回具有不同时区的此日期时间的副本,并保留以下瞬间:

public static List localTimeToAllTimeZones() {

  List result = new ArrayList<>();
  Set zoneIds = ZoneId.getAvailableZoneIds();
  DateTimeFormatter formatter 
    = DateTimeFormatter.ofPattern("yyyy-MMM-dd'T'HH:mm:ss a Z");
  ZonedDateTime zlt = ZonedDateTime.now();

  zoneIds.forEach((zoneId) -> {
    result.add(zlt.format(formatter) + " in " + zoneId + " is "
      + zlt.withZoneSameInstant(ZoneId.of(zoneId))
        .format(formatter));
  });

  return result;
}

此方法的输出快照可以如下所示:

2019-Feb-26T14:26:30 PM +0200 in Africa/Nairobi 
  is 2019-Feb-26T15:26:30 PM +0300
2019-Feb-26T14:26:30 PM +0200 in America/Marigot 
  is 2019-Feb-26T08:26:30 AM -0400
...
2019-Feb-26T14:26:30 PM +0200 in Pacific/Samoa 
  is 2019-Feb-26T01:26:30 AM -1100

68 显示航班的日期时间信息

本节提供的解决方案将显示有关从澳大利亚珀斯到欧洲布加勒斯特的 15 小时 30 分钟航班的以下信息:

  • UTC 出发和到达日期时间
  • 离开珀斯的日期时间和到达布加勒斯特的日期时间
  • 离开和到达布加勒斯特的日期时间

假设从珀斯出发的参考日期时间为 2019 年 2 月 26 日 16:00(或下午 4:00):

LocalDateTime ldt = LocalDateTime.of(
  2019, Month.FEBRUARY, 26, 16, 00);

首先,让我们将这个日期时间与澳大利亚/珀斯(+08:00)的时区结合起来。这将产生一个特定于澳大利亚/珀斯的ZonedDateTime对象(这是出发时珀斯的时钟日期和时间):

// 04:00 PM, Feb 26, 2019 +0800 Australia/Perth
ZonedDateTime auPerthDepart 
  = ldt.atZone(ZoneId.of("Australia/Perth"));

此外,让我们在ZonedDateTime中加上 15 小时 30 分钟。结果ZonedDateTime表示珀斯的日期时间(这是抵达布加勒斯特时珀斯的时钟日期和时间):

// 07:30 AM, Feb 27, 2019 +0800 Australia/Perth
ZonedDateTime auPerthArrive 
  = auPerthDepart.plusHours(15).plusMinutes(30);

现在,让我们计算一下布加勒斯特的日期时间和珀斯的出发日期时间。基本上,以下代码表示从布加勒斯特时区的珀斯时区出发的日期和时间:

// 10:00 AM, Feb 26, 2019 +0200 Europe/Bucharest
ZonedDateTime euBucharestDepart 
  = auPerthDepart.withZoneSameInstant(ZoneId.of("Europe/Bucharest"));

最后,让我们计算一下到达布加勒斯特的日期和时间。以下代码表示布加勒斯特时区珀斯时区的到达日期时间:

// 01:30 AM, Feb 27, 2019 +0200 Europe/Bucharest
ZonedDateTime euBucharestArrive 
  = auPerthArrive.withZoneSameInstant(ZoneId.of("Europe/Bucharest"));

如下图所示,从珀斯出发的 UTC 时间是上午 8:00,而到达布加勒斯特的 UTC 时间是晚上 11:30:

这些时间可以很容易地提取为OffsetDateTime,如下所示:

// 08:00 AM, Feb 26, 2019
OffsetDateTime utcAtDepart = auPerthDepart.withZoneSameInstant(
  ZoneId.of("UTC")).toOffsetDateTime();

// 11:30 PM, Feb 26, 2019
OffsetDateTime utcAtArrive = auPerthArrive.withZoneSameInstant(
  ZoneId.of("UTC")).toOffsetDateTime();

69 将 Unix 时间戳转换为日期时间

对于这个解决方案,假设下面的 Unix 时间戳是 1573768800。此时间戳等效于以下内容:

  • 11/14/2019 @ 10:00pm (UTC)
  • ISO-8601 中的2019-11-14T22:00:00+00:00
  • Thu, 14 Nov 2019 22:00:00 +0000,RFC 822、1036、1123、2822
  • Thursday, 14-Nov-19 22:00:00 UTC,RFC 2822
  • 2019-11-14T22:00:00+00:00在 RFC 3339 中

为了将 Unix 时间戳转换为日期时间,必须知道 Unix 时间戳的分辨率以秒为单位,而java.util.Date需要毫秒。因此,从 Unix 时间戳获取Date对象的解决方案需要将 Unix 时间戳乘以 1000,从秒转换为毫秒,如下两个示例所示:

long unixTimestamp = 1573768800;

// Fri Nov 15 00:00:00 EET 2019 - in the default time zone
Date date = new Date(unixTimestamp * 1000L);

// Fri Nov 15 00:00:00 EET 2019 - in the default time zone
Date date = new Date(TimeUnit.MILLISECONDS
  .convert(unixTimestamp, TimeUnit.SECONDS));

从 JDK8 开始,Date类使用from(Instant instant)方法。此外,Instant类附带了ofEpochSecond(long epochSecond)方法,该方法使用1970-01-01T00:00:00Z的纪元的给定秒数返回Instant的实例:

// 2019-11-14T22:00:00Z in UTC
Instant instant = Instant.ofEpochSecond(unixTimestamp);

// Fri Nov 15 00:00:00 EET 2019 - in the default time zone
Date date = Date.from(instant);

上一示例中获得的瞬间可用于创建LocalDateTimeZonedDateTime,如下所示:

// 2019-11-15T06:00
LocalDateTime date = LocalDateTime
  .ofInstant(instant, ZoneId.of("Australia/Perth"));

// 2019-Nov-15 00:00:00 +0200 Europe/Bucharest
ZonedDateTime date = ZonedDateTime
  .ofInstant(instant, ZoneId.of("Europe/Bucharest"));

70 查找每月的第一天/最后一天

这个问题的正确解决将依赖于 JDK8、TemporalTemporalAdjuster接口。

Temporal接口位于日期和时间的表示后面。换句话说,表示日期和/或时间的类实现了这个接口。例如,以下类只是实现此接口的几个类:

  • LocalDate(ISO-8601 日历系统中没有时区的日期)
  • LocalTime(ISO-8601 日历系统中无时区的时间)
  • LocalDateTime(ISO-8601 日历系统中无时区的日期时间)
  • ZonedDateTime(ISO-8601 日历系统中带时区的日期时间),依此类推
  • OffsetDateTime(在 ISO-8601 日历系统中,从 UTC/格林威治时间偏移的日期时间)
  • HijrahDate(希吉拉历法系统中的日期)

TemporalAdjuster类是一个函数式接口,它定义了可用于调整Temporal对象的策略。除了可以定义自定义策略外,TemporalAdjuster类还提供了几个预定义的策略,如下所示(文档包含了整个列表,非常令人印象深刻):

  • firstDayOfMonth()(返回当月第一天)
  • lastDayOfMonth()(返回当月最后一天)
  • firstDayOfNextMonth()(次月 1 日返回)
  • firstDayOfNextYear()(次年第一天返回)

注意,前面列表中的前两个调整器正是这个问题所需要的。

考虑一个修正-LocalDate

LocalDate date = LocalDate.of(2019, Month.FEBRUARY, 27);

让我们看看二月的第一天/最后一天是什么时候:

// 2019-02-01
LocalDate firstDayOfFeb 
  = date.with(TemporalAdjusters.firstDayOfMonth());

// 2019-02-28
LocalDate lastDayOfFeb 
  = date.with(TemporalAdjusters.lastDayOfMonth());

看起来依赖预定义的策略非常简单。但是,假设问题要求您查找 2019 年 2 月 27 日之后的 21 天,也就是 2019 年 3 月 20 日。对于这个问题,没有预定义的策略,因此需要自定义策略。此问题的解决方案可以依赖 Lambda 表达式,如以下辅助方法中所示:

public static LocalDate getDayAfterDays(
    LocalDate startDate, int days) {

  Period period = Period.ofDays(days);
  TemporalAdjuster ta = p -> p.plus(period);
  LocalDate endDate = startDate.with(ta);

  return endDate;
}

如果此方法存在于名为DateTimes的类中,则以下调用将返回预期结果:

// 2019-03-20
LocalDate datePlus21Days = DateTimes.getDayAfterDays(date, 21);

遵循相同的技术,但依赖于static工厂方法ofDateAdjuster(),下面的代码片段定义了一个静态调整器,返回下一个星期六的日期:

static TemporalAdjuster NEXT_SATURDAY 
    = TemporalAdjusters.ofDateAdjuster(today -> {

  DayOfWeek dayOfWeek = today.getDayOfWeek();

  if (dayOfWeek == DayOfWeek.SATURDAY) {
    return today;
  }

  if (dayOfWeek == DayOfWeek.SUNDAY) {
    return today.plusDays(6);
  }

  return today.plusDays(6 - dayOfWeek.getValue());
});

我们将此方法称为 2019 年 2 月 27 日(下一个星期六是 2019 年 3 月 2 日):

// 2019-03-02
LocalDate nextSaturday = date.with(NEXT_SATURDAY);

最后,这个函数式接口定义了一个名为adjustInto()abstract方法。在自定义实现中,可以通过向该方法传递一个Temporal对象来覆盖该方法,如下所示:

public class NextSaturdayAdjuster implements TemporalAdjuster {

  @Override
  public Temporal adjustInto(Temporal temporal) {

    DayOfWeek dayOfWeek = DayOfWeek
      .of(temporal.get(ChronoField.DAY_OF_WEEK));

    if (dayOfWeek == DayOfWeek.SATURDAY) {
      return temporal;
    }

    if (dayOfWeek == DayOfWeek.SUNDAY) {
      return temporal.plus(6, ChronoUnit.DAYS);
    }

    return temporal.plus(6 - dayOfWeek.getValue(), ChronoUnit.DAYS);
  }
}

下面是用法示例:

NextSaturdayAdjuster nsa = new NextSaturdayAdjuster();

// 2019-03-02
LocalDate nextSaturday = date.with(nsa);

71 定义/提取区域偏移

通过区域偏移,我们了解需要从 GMT/UTC 时间中添加/减去的时间量,以便获得全球特定区域(例如,澳大利亚珀斯)的日期时间。通常,区域偏移以固定的小时和分钟数打印:+02:00-08:30+0400UTC+01:00,依此类推。

因此,简而言之,时区偏移量是指时区与 GMT/UTC 之间的时间差。

JDK8 之前

在 JDK8 之前,可以通过java.util.TimeZone定义一个时区,有了这个时区,代码就可以通过TimeZone.getRawOffset()方法得到时区偏移量(原始部分来源于这个方法不考虑夏令时)。绑定到本书的代码包含此解决方案。

从 JDK8 开始

从 JDK8 开始,有两个类负责处理时区表示。首先是java.time.ZoneId,表示欧洲雅典等时区;其次是java.time.ZoneOffset(扩展ZoneId),表示指定时区的固定时间(偏移量),以 GMT/UTC 表示。

新的 Java 日期时间 API 默认处理夏令时;因此,使用夏令时的夏-冬周期区域将有两个ZoneOffset类。

UTC 区域偏移量可以很容易地获得,如下所示(这是+00:00,在 Java 中用Z字符表示):

// Z
ZoneOffset zoneOffsetUTC = ZoneOffset.UTC;

系统默认时区也可以通过ZoneOffset类获取:

// Europe/Athens
ZoneId defaultZoneId = ZoneOffset.systemDefault();

为了使用夏令时进行分区偏移,代码需要将日期时间与其关联。例如,关联一个LocalDateTime类(也可以使用Instant),如下所示:

// by default it deals with the Daylight Saving Times
LocalDateTime ldt = LocalDateTime.of(2019, 6, 15, 0, 0);
ZoneId zoneId = ZoneId.of("Europe/Bucharest");

// +03:00
ZoneOffset zoneOffset = zoneId.getRules().getOffset(ldt);

区域偏移量也可以从字符串中获得。例如,以下代码获得+02:00的分区偏移:

ZoneOffset zoneOffsetFromString = ZoneOffset.of("+02:00");

这是一种非常方便的方法,可以将区域偏移快速添加到支持区域偏移的Temporal对象。例如,使用它将区域偏移添加到OffsetTimeOffsetDateTime(用于在数据库中存储日期或通过电线发送的方便方法):

OffsetTime offsetTime = OffsetTime.now(zoneOffsetFromString);
OffsetDateTime offsetDateTime 
  = OffsetDateTime.now(zoneOffsetFromString);

我们问题的另一个解决方法是依赖于从小时、分钟和秒来定义ZoneOffsetZoneOffset的一个助手方法专门用于:

// +08:30 (this was obtained from 8 hours and 30 minutes)
ZoneOffset zoneOffsetFromHoursMinutes 
  = ZoneOffset.ofHoursMinutes(8, 30);

ZoneOffset.ofHoursMinutes()旁边有ZoneOffset.ofHours()ofHoursMinutesSeconds()ofTotalSeconds()

最后,每个支持区域偏移的Temporal对象都提供了一个方便的getOffset()方法。例如,下面的代码从前面的offsetDateTime对象获取区域偏移:

// +02:00
ZoneOffset zoneOffsetFromOdt = offsetDateTime.getOffset();

72 在日期和时间之间转换

这里给出的解决方案将涵盖以下Temporal类—InstantLocalDateLocalDateTimeZonedDateTimeOffsetDateTimeLocalTimeOffsetTime

Date-Instant

为了从Date转换到Instant,可采用Date.toInstant()方法求解。可通过Date.from(Instant instant)方法实现反转:

  • DateInstant可以这样完成:
Date date = new Date();

// e.g., 2019-02-27T12:02:49.369Z, UTC
Instant instantFromDate = date.toInstant();
  • InstantDate可以这样完成:
Instant instant = Instant.now();

// Wed Feb 27 14:02:49 EET 2019, default system time zone
Date dateFromInstant = Date.from(instant);

请记住,Date不是时区感知的,但它显示在系统默认时区中(例如,通过toString())。Instant是 UTC 时区。

让我们快速地将这些代码片段包装在两个工具方法中,它们在一个工具类DateConverters中定义:

public static Instant dateToInstant(Date date) {

  return date.toInstant();
}

public static Date instantToDate(Instant instant) {

  return Date.from(instant);
}

此外,让我们使用以下屏幕截图中的方法来丰富此类:

屏幕截图中的常量DEFAULT_TIME_ZONE是系统默认时区:

public static final ZoneId DEFAULT_TIME_ZONE = ZoneId.systemDefault();

DateLocalDate

Date对象可以通过Instant对象转换为LocalDate。一旦我们从给定的Date对象中获得Instant对象,解决方案就可以应用于它系统默认时区,并调用toLocaleDate()方法:

// e.g., 2019-03-01
public static LocalDate dateToLocalDate(Date date) {

  return dateToInstant(date).atZone(DEFAULT_TIME_ZONE).toLocalDate();
}

LocalDateDate的转换应该考虑到LocalDate不包含Date这样的时间成分,所以解决方案必须提供一个时间成分作为一天的开始(关于这个问题的更多细节可以在“一天的开始和结束”问题中找到):

// e.g., Fri Mar 01 00:00:00 EET 2019
public static Date localDateToDate(LocalDate localDate) {

  return Date.from(localDate.atStartOfDay(
    DEFAULT_TIME_ZONE).toInstant());
}

DateLocalDateTime

DateDateLocalTime的转换与从DateLocalDate的转换是一样的,只是溶液应该调用toLocalDateTime()方法如下:

// e.g., 2019-03-01T07:25:25.624
public static LocalDateTime dateToLocalDateTime(Date date) {

  return dateToInstant(date).atZone(
    DEFAULT_TIME_ZONE).toLocalDateTime();
}

LocalDateTimeDate的转换非常简单。只需应用系统默认时区并调用toInstant()

// e.g., Fri Mar 01 07:25:25 EET 2019
public static Date localDateTimeToDate(LocalDateTime localDateTime) {

  return Date.from(localDateTime.atZone(
    DEFAULT_TIME_ZONE).toInstant());
}

DateZonedDateTime

DateZonedDateTime的转换可以通过从给定Date对象获取Instant对象和系统默认时区来完成:

// e.g., 2019-03-01T07:25:25.624+02:00[Europe/Athens]
public static ZonedDateTime dateToZonedDateTime(Date date) {

  return dateToInstant(date).atZone(DEFAULT_TIME_ZONE);
}

ZonedDateTime转换为Date就是将ZonedDateTime转换为Instant

// e.g., Fri Mar 01 07:25:25 EET 2019
public static Date zonedDateTimeToDate(ZonedDateTime zonedDateTime) {

  return Date.from(zonedDateTime.toInstant());
}

DateOffsetDateTime

DateOffsetDateTime的转换依赖于toOffsetDateTime()方法:

// e.g., 2019-03-01T07:25:25.624+02:00
public static OffsetDateTime dateToOffsetDateTime(Date date) {

  return dateToInstant(date).atZone(
    DEFAULT_TIME_ZONE).toOffsetDateTime();
}

OffsetDateTimeDate的转换方法需要两个步骤。首先将OffsetDateTime转换为LocalDateTime;其次将LocalDateTime转换为Instant,对应偏移量:

// e.g., Fri Mar 01 07:55:49 EET 2019
public static Date offsetDateTimeToDate(
    OffsetDateTime offsetDateTime) {

  return Date.from(offsetDateTime.toLocalDateTime()
    .toInstant(ZoneOffset.of(offsetDateTime.getOffset().getId())));
}

DateLocalTime

Date转换为LocalTime可以依赖LocalTime.toInstant()方法,如下所示:

// e.g., 08:03:20.336
public static LocalTime dateToLocalTime(Date date) {

  return LocalTime.ofInstant(dateToInstant(date), DEFAULT_TIME_ZONE);
}

LocalTime转换为Date应该考虑到LocalTime没有日期组件。这意味着解决方案应将日期设置为 1970 年 1 月 1 日,即纪元:

// e.g., Thu Jan 01 08:03:20 EET 1970
public static Date localTimeToDate(LocalTime localTime) {

  return Date.from(localTime.atDate(LocalDate.EPOCH)
    .toInstant(DEFAULT_TIME_ZONE.getRules()
      .getOffset(Instant.now())));
}

Date-OffsetTime

Date转换为OffsetTime可以依赖OffsetTime.toInstant()方法,如下所示:

// e.g., 08:03:20.336+02:00
public static OffsetTime dateToOffsetTime(Date date) {

  return OffsetTime.ofInstant(dateToInstant(date), DEFAULT_TIME_ZONE);
}

OffsetTime转换为Date应该考虑到OffsetTime没有日期组件。这意味着解决方案应将日期设置为 1970 年 1 月 1 日,即纪元:

// e.g., Thu Jan 01 08:03:20 EET 1970
public static Date offsetTimeToDate(OffsetTime offsetTime) {

  return Date.from(offsetTime.atDate(LocalDate.EPOCH).toInstant());
}

73 迭代一系列日期

假设范围是由开始日期 2019 年 2 月 1 日和结束日期 2019 年 2 月 21 日界定的。这个问题的解决方案应该循环【2019 年 2 月 1 日,2019 年 2 月 21 日】间隔一天,并在屏幕上打印每个日期。基本上要解决两个主要问题:

  • 一旦开始日期和结束日期相等,就停止循环。
  • 每天增加开始日期直到结束日期。

JDK8 之前

在 JDK8 之前,解决方案可以依赖于Calendar工具类。绑定到本书的代码包含此解决方案。

从 JDK8 开始

首先,从 JDK8 开始,可以很容易地将日期定义为LocalDate,而不需要Calendar的帮助:

LocalDate startLocalDate = LocalDate.of(2019, 2, 1);
LocalDate endLocalDate = LocalDate.of(2019, 2, 21);

一旦开始日期和结束日期相等,我们就通过LocalDate.isBefore(ChronoLocalDate other)方法停止循环。此标志方法检查此日期是否早于给定日期。

使用LocalDate.plusDays(long daysToAdd)方法逐日增加开始日期直到结束日期。在for循环中使用这两种方法会产生以下代码:

for (LocalDate date = startLocalDate; 
       date.isBefore(endLocalDate); date = date.plusDays(1)) {

  // do something with this day
  System.out.println(date);
}

输出的快照应如下所示:

2019-02-01
2019-02-02
2019-02-03
...
2019-02-20

从 JDK9 开始

JDK9 可以用一行代码解决这个问题。由于新的LocalDate.datesUntil(LocalDate endExclusive)方法,这是可能的。此方法返回Stream,增量步长为一天:

startLocalDate.datesUntil(endLocalDate).forEach(System.out::println);

如果增量步骤应以天、周、月或年表示,则依赖于LocalDate.datesUntil(LocalDate endExclusive, Period step)。例如,1 周的增量步骤可以指定如下:

startLocalDate.datesUntil(endLocalDate, Period.ofWeeks(1)).forEach(System.out::println);

输出应为(第 1-8 周,第 8-15 周),如下所示:

2019-02-01
2019-02-08
2019-02-15

74 计算年龄

可能最常用的两个日期之间的差异是关于计算一个人的年龄。通常,一个人的年龄以年表示,但有时应提供月,甚至天。

JDK8 之前

在 JDK8 之前,试图提供一个好的解决方案可以依赖于Calendar和/或SimpleDateFormat。绑定到本书的代码包含这样一个解决方案。

从 JDK8 开始

更好的方法是升级到 JDK8,并依赖以下简单的代码片段:

LocalDate startLocalDate = LocalDate.of(1977, 11, 2);
LocalDate endLocalDate = LocalDate.now();

long years = ChronoUnit.YEARS.between(startLocalDate, endLocalDate);

由于Period类的原因,将月和日添加到结果中也很容易实现:

Period periodBetween = Period.between(startLocalDate, endLocalDate);

现在,可以通过periodBetween.getYears()periodBetween.getMonths()periodBetween.getDays()获得以年、月、日为单位的年龄。

例如,在当前日期 2019 年 2 月 28 日和 1977 年 11 月 2 日之间,我们有 41 年 3 个月 26 天。

75 一天的开始和结束

在 JDK8 中,可以通过几种方法来找到一天的开始/结束。

让我们考虑一下通过LocalDate表达的一天:

LocalDate localDate = LocalDate.of(2019, 2, 28);

找到 2019 年 2 月 28 日一天的开始的解决方案依赖于一个名为atStartOfDay()的方法。此方法从该日期午夜 00:00 返回LocalDateTime

// 2019-02-28T00:00
LocalDateTime ldDayStart = localDate.atStartOfDay();

或者,该溶液可以使用of(LocalDate date, LocalTime time)方法。该方法将给定的日期和时间组合成LocalDateTime。因此,如果经过的时间是LocalTime.MIN(一天开始时的午夜时间),则结果如下:

// 2019-02-28T00:00
LocalDateTime ldDayStart = LocalDateTime.of(localDate, LocalTime.MIN);

一个LocalDate物体的一天结束时间至少可以用两种方法得到。一种解决方案是依靠LocalDate.atTime(LocalTime time)。得到的LocalDateTime可以表示该日期与一天结束时的组合,如果解决方案作为参数传递,LocalTime.MAX(一天结束时午夜前的时间):

// 2019-02-28T23:59:59.999999999
LocalDateTime ldDayEnd = localDate.atTime(LocalTime.MAX);

或者,该解决方案可以通过atDate(LocalDate date)方法将LocalTime.MAX与给定日期结合:

// 2019-02-28T23:59:59.999999999
LocalDateTime ldDayEnd = LocalTime.MAX.atDate(localDate);

由于LocalDate没有时区的概念,前面的例子容易出现由不同的角落情况引起的问题,例如夏令时。有些夏令时会在午夜(00:00 变为 01:00 AM)更改时间,这意味着一天的开始时间是 01:00:00,而不是 00:00:00。为了缓解这些问题,请考虑以下示例,这些示例将前面的示例扩展为使用夏令时感知的ZonedDateTime

// 2019-02-28T00:00+08:00[Australia/Perth]
ZonedDateTime ldDayStartZone 
  = localDate.atStartOfDay(ZoneId.of("Australia/Perth"));

// 2019-02-28T00:00+08:00[Australia/Perth]
ZonedDateTime ldDayStartZone = LocalDateTime
  .of(localDate, LocalTime.MIN).atZone(ZoneId.of("Australia/Perth"));

// 2019-02-28T23:59:59.999999999+08:00[Australia/Perth]
ZonedDateTime ldDayEndZone = localDate.atTime(LocalTime.MAX)
  .atZone(ZoneId.of("Australia/Perth"));

// 2019-02-28T23:59:59.999999999+08:00[Australia/Perth]
ZonedDateTime ldDayEndZone = LocalTime.MAX.atDate(localDate)
  .atZone(ZoneId.of("Australia/Perth"));

现在,我们来考虑一下-LocalDateTime,2019 年 2 月 28 日,18:00:00:

LocalDateTime localDateTime = LocalDateTime.of(2019, 2, 28, 18, 0, 0);

显而易见的解决方案是从LocalDateTime中提取LocalDate,并应用前面的方法。另一个解决方案依赖于这样一个事实,Temporal接口的每个实现(包括LocalDate)都可以利用with(TemporalField field, long newValue)方法。主要是,with()方法返回这个日期的一个副本,其中指定的字段ChronoField设置为newValue。因此,如果解决方案将ChronoField.NANO_OF_DAY(一天的纳秒)设置为LocalTime.MIN,那么结果将是一天的开始。这里的技巧是通过toNanoOfDay()LocalTime.MIN转换为纳秒,如下所示:

// 2019-02-28T00:00
LocalDateTime ldtDayStart = localDateTime
  .with(ChronoField.NANO_OF_DAY, LocalTime.MIN.toNanoOfDay());

这相当于:

LocalDateTime ldtDayStart 
   = localDateTime.with(ChronoField.HOUR_OF_DAY, 0);

一天的结束是非常相似的。只需通过LocalTime.MAX而不是MIN

// 2019-02-28T23:59:59.999999999
LocalDateTime ldtDayEnd = localDateTime
  .with(ChronoField.NANO_OF_DAY, LocalTime.MAX.toNanoOfDay());

这相当于:

LocalDateTime ldtDayEnd = localDateTime.with(
  ChronoField.NANO_OF_DAY, 86399999999999L);

LocalDate一样,LocalDateTime对象不知道时区。在这种情况下,ZonedDateTime可以帮助:

// 2019-02-28T00:00+08:00[Australia/Perth]
ZonedDateTime ldtDayStartZone = localDateTime
  .with(ChronoField.NANO_OF_DAY, LocalTime.MIN.toNanoOfDay())
  .atZone(ZoneId.of("Australia/Perth"));

// 2019-02-28T23:59:59.999999999+08:00[Australia/Perth]
ZonedDateTime ldtDayEndZone = localDateTime
  .with(ChronoField.NANO_OF_DAY, LocalTime.MAX.toNanoOfDay())
  .atZone(ZoneId.of("Australia/Perth"));

作为奖励,让我们看看 UTC 一天的开始/结束。除了依赖于with()方法的解决方案外,另一个解决方案可以依赖于toLocalDate(),如下所示:

// e.g., 2019-02-28T09:23:10.603572Z
ZonedDateTime zdt = ZonedDateTime.now(ZoneOffset.UTC);

// 2019-02-28T00:00Z
ZonedDateTime dayStartZdt 
  = zdt.toLocalDate().atStartOfDay(zdt.getZone());

// 2019-02-28T23:59:59.999999999Z
ZonedDateTime dayEndZdt = zdt.toLocalDate()
  .atTime(LocalTime.MAX).atZone(zdt.getZone());

由于java.util.DateCalendar存在许多问题,因此建议避免尝试用它们实现此问题的解决方案。

76 两个日期之间的差异

计算两个日期之间的差值是一项非常常见的任务(例如,请参阅“计算年龄”部分)。让我们看看其他方法的集合,这些方法可以用来获得以毫秒、秒、小时等为单位的两个日期之间的差异。

JDK8 之前

建议通过java.util.DateCalendar类来表示日期时间信息。最容易计算的差异用毫秒表示。绑定到本书的代码包含这样一个解决方案。

从 JDK8 开始

从 JDK8 开始,建议通过Temporal(例如,DateTimeDateLocalTimeZonedDateTime等)来表示日期时间信息。

假设两个LocalDate对象,2018 年 1 月 1 日和 2019 年 3 月 1 日:

LocalDate ld1 = LocalDate.of(2018, 1, 1);
LocalDate ld2 = LocalDate.of(2019, 3, 1);

计算这两个Temporal对象之间差异的最简单方法是通过ChronoUnit类。除了表示一组标准的日期周期单位外,ChronoUnit还提供了几种简便的方法,包括between(Temporal t1Inclusive, Temporal t2Exclusive)。顾名思义,between()方法计算两个Temporal对象之间的时间量。让我们看看计算ld1ld2之间的差值的工作原理,以天、月和年为单位:

// 424
long betweenInDays = Math.abs(ChronoUnit.DAYS.between(ld1, ld2));

// 14
long betweenInMonths = Math.abs(ChronoUnit.MONTHS.between(ld1, ld2));

// 1
long betweenInYears = Math.abs(ChronoUnit.YEARS.between(ld1, ld2));

或者,每个Temporal公开一个名为until()的方法。实际上,LocalDate有两个,一个返回Period作为两个日期之间的差,另一个返回long作为指定时间单位中两个日期之间的差。使用返回Period的方法如下:

Period period = ld1.until(ld2);

// Difference as Period: 1y2m0d
System.out.println("Difference as Period: " + period.getYears() + "y" 
  + period.getMonths() + "m" + period.getDays() + "d");

使用允许我们指定时间单位的方法如下:

// 424
long untilInDays = Math.abs(ld1.until(ld2, ChronoUnit.DAYS));

// 14
long untilInMonths = Math.abs(ld1.until(ld2, ChronoUnit.MONTHS));

// 1
long untilInYears = Math.abs(ld1.until(ld2, ChronoUnit.YEARS));

ChronoUnit.convert()方法也适用于LocalDateTime的情况。让我们考虑以下两个LocalDateTime对象:2018 年 1 月 1 日 22:15:15 和 2019 年 3 月 1 日 23:15:15:

LocalDateTime ldt1 = LocalDateTime.of(2018, 1, 1, 22, 15, 15);
LocalDateTime ldt2 = LocalDateTime.of(2018, 1, 1, 23, 15, 15);

现在,让我们看看ldt1ldt2之间的区别,用分钟表示:

// 60
long betweenInMinutesWithoutZone 
  = Math.abs(ChronoUnit.MINUTES.between(ldt1, ldt2));

并且,通过LocalDateTime.until()方法以小时表示的差异:

// 1
long untilInMinutesWithoutZone 
  = Math.abs(ldt1.until(ldt2, ChronoUnit.HOURS));

但是,ChronoUnit.between()until()有一个非常棒的地方,那就是它们与ZonedDateTime一起工作。例如,让我们考虑欧洲/布加勒斯特时区和澳大利亚/珀斯时区的ldt1,加上一小时:

ZonedDateTime zdt1 = ldt1.atZone(ZoneId.of("Europe/Bucharest"));
ZonedDateTime zdt2 = zdt1.withZoneSameInstant(
  ZoneId.of("Australia/Perth")).plusHours(1);

现在,我们用ChronoUnit.between()来表示zdt1zdt2之间的差分,用ZonedDateTime.until()来表示zdt1zdt2之间的差分,用小时表示:

// 60
long betweenInMinutesWithZone 
  = Math.abs(ChronoUnit.MINUTES.between(zdt1, zdt2));

// 1
long untilInHoursWithZone 
  = Math.abs(zdt1.until(zdt2, ChronoUnit.HOURS));

最后,让我们重复这个技巧,但是对于两个独立的ZonedDateTime对象:一个为ldt1获得,一个为ldt2获得:

ZonedDateTime zdt1 = ldt1.atZone(ZoneId.of("Europe/Bucharest"));
ZonedDateTime zdt2 = ldt2.atZone(ZoneId.of("Australia/Perth"));

// 300
long betweenInMinutesWithZone 
  = Math.abs(ChronoUnit.MINUTES.between(zdt1, zdt2));

// 5
long untilInHoursWithZone 
  = Math.abs(zdt1.until(zdt2, ChronoUnit.HOURS));

77 实现象棋时钟

从 JDK8 开始,java.time包有一个名为Clock的抽象类。这个类的主要目的是允许我们在需要时插入不同的时钟(例如,出于测试目的)。默认情况下,Java 有四种实现:SystemClockOffsetClockTickClockFixedClock。对于每个实现,Clock类中都有static方法。例如,下面的代码创建了FixedClock(一个总是返回相同Instant的时钟):

Clock fixedClock = Clock.fixed(Instant.now(), ZoneOffset.UTC);

还有一个TickClock,它返回给定时区整秒的当前Instant滴答声:

Clock tickClock = Clock.tickSeconds(ZoneId.of("Europe/Bucharest"));

还有一种方法可以用来在整分钟内打勾tickMinutes(),还有一种通用方法tick(),它允许我们指定Duration

Clock类也可以支持时区和偏移量,但是Clock类最重要的方法是instant()。此方法返回Clock的瞬间:

// 2019-03-01T13:29:34Z
System.out.println(tickClock.instant());

还有一个millis()方法,它以毫秒为单位返回时钟的当前时刻。

假设我们要实现一个时钟,它充当象棋时钟:

为了实现一个Clock类,需要遵循以下几个步骤:

  1. 扩展Clock类。
  2. 执行Serializable
  3. 至少覆盖从Clock继承的抽象方法。

Clock类的框架如下:

public class ChessClock extends Clock implements Serializable {

  @Override
  public ZoneId getZone() {
    ...
  }

  @Override
  public Clock withZone(ZoneId zone) {
    ...
  }

  @Override
  public Instant instant() {
    ...
  }
}

我们的ChessClock将只与 UTC 一起工作;不支持其他时区。这意味着getZone()withZone()方法可以实现如下(当然,将来可以修改):

@Override
public ZoneId getZone() {
  return ZoneOffset.UTC;
}

@Override
public Clock withZone(ZoneId zone) {
  throw new UnsupportedOperationException(
    "The ChessClock works only in UTC time zone");
}

我们实现的高潮是instant()方法。难度在于管理两个Instant,一个是左边的玩家(instantLeft),一个是右边的玩家(instantRight)。我们可以将instant()方法的每一次调用与当前玩家已经执行了一个移动的事实相关联,现在轮到另一个玩家了。所以,基本上,这个逻辑是说同一个玩家不能调用instant()两次。实现这个逻辑,instant()方法如下:

public class ChessClock extends Clock implements Serializable {

  public enum Player {
    LEFT,
    RIGHT
  }

  private static final long serialVersionUID = 1L;

  private Instant instantStart;
  private Instant instantLeft;
  private Instant instantRight;
  private long timeLeft;
  private long timeRight;
  private Player player;

  public ChessClock(Player player) {
    this.player = player;
  }

  public Instant gameStart() {

    if (this.instantStart == null) {
      this.timeLeft = 0;
      this.timeRight = 0;
      this.instantStart = Instant.now();
      this.instantLeft = instantStart;
      this.instantRight = instantStart;
      return instantStart;
    }

    throw new IllegalStateException(
      "Game already started. Stop it and try again.");
  }

  public Instant gameEnd() {

    if (this.instantStart != null) {
      instantStart = null;
      return Instant.now();
    }

    throw new IllegalStateException("Game was not started.");
  }

  @Override
  public ZoneId getZone() {
    return ZoneOffset.UTC;
  }

  @Override
  public Clock withZone(ZoneId zone) {
    throw new UnsupportedOperationException(
      "The ChessClock works only in UTC time zone");
  }

  @Override
  public Instant instant() {

    if (this.instantStart != null) {
      if (player == Player.LEFT) {
        player = Player.RIGHT;

        long secondsLeft = Instant.now().getEpochSecond() 
          - instantRight.getEpochSecond();
        instantLeft = instantLeft.plusSeconds(
          secondsLeft - timeLeft);
        timeLeft = secondsLeft;

        return instantLeft;
      } else {
        player = Player.LEFT;

        long secondsRight = Instant.now().getEpochSecond() 
          - instantLeft.getEpochSecond();
        instantRight = instantRight.plusSeconds(
          secondsRight - timeRight);
        timeRight = secondsRight;

        return instantRight;
      }
    }

    throw new IllegalStateException("Game was not started.");
  }
}

因此,根据哪个玩家调用了instant()方法,代码计算出该玩家在执行移动之前思考所需的秒数。此外,代码会切换播放器,因此下一次调用instant()将处理另一个播放器。

让我们考虑一个从2019-03-01T14:02:46.309459Z开始的国际象棋游戏:

ChessClock chessClock = new ChessClock(Player.LEFT);

// 2019-03-01T14:02:46.309459Z
Instant start = chessClock.gameStart();

此外,玩家执行以下一系列动作,直到右边的玩家赢得游戏:

Left moved first after 2 seconds: 2019-03-01T14:02:48.309459Z
Right moved after 5 seconds: 2019-03-01T14:02:51.309459Z
Left moved after 6 seconds: 2019-03-01T14:02:54.309459Z
Right moved after 1 second: 2019-03-01T14:02:52.309459Z
Left moved after 2 second: 2019-03-01T14:02:56.309459Z
Right moved after 3 seconds: 2019-03-01T14:02:55.309459Z
Left moved after 10 seconds: 2019-03-01T14:03:06.309459Z
Right moved after 11 seconds and win: 2019-03-01T14:03:06.309459Z

看来时钟正确地记录了运动员的动作。

最后,比赛在 40 秒后结束:

Game ended:2019-03-01T14:03:26.350749300Z
Instant end = chessClock.gameEnd();

Game duration: 40 seconds
// Duration.between(start, end).getSeconds();

总结

任务完成了!本章提供了使用日期和时间信息的全面概述。广泛的应用必须处理这类信息。因此,将这些问题的解决方案放在你的工具带下不是可选的。从DateCalendarLocalDateLocalTimeLocalDateTimeZoneDateTimeOffsetDateTimeOffsetTimeInstant——它们在涉及日期和时间的日常任务中都是非常重要和有用的。

从本章下载应用以查看结果和其他详细信息。

你可能感兴趣的:(java)