原文:Java Coding Problems 协议:CC BY-NC-SA 4.0 贡献者:飞龙 本文来自【ApacheCN Java 译文集】,自豪地采用谷歌翻译。
本章包括 20 个涉及日期和时间的问题。这些问题通过Date
、Calendar
、LocalDate
、LocalTime
、LocalDateTime
、ZoneDateTime
、OffsetDateTime
、OffsetTime
、Instant
等涵盖了广泛的主题(转换、格式化、加减、定义时段/持续时间、计算等)。到本章结束时,您将在确定日期和时间方面没有问题,同时符合您的应用的需要。本章介绍的基本问题将非常有助于了解日期-时间 API 的整体情况,并将像拼图中需要拼凑起来的部分一样解决涉及日期和时间的复杂挑战。
问题
使用以下问题来测试您的日期和时间编程能力。我强烈建议您在使用解决方案和下载示例程序之前,先尝试一下每个问题:
- 将字符串转换为日期和时间:编写一个程序,演示字符串和日期/时间之间的转换。
- 格式化日期和时间:**解释日期和时间的格式模式。
- 获取当前日期/时间(不含日期/时间):编写程序,提取当前日期(不含时间或日期)。
- 从
LocalDate
和LocalTime
到LocalDateTime
:编写一个程序,从LocalDate
对象和LocalTime
构建一个LocalDateTime
。它将日期和时间组合在一个LocalDateTime
对象中。 - 通过
Instant
类获取机器时间:解释并举例说明Instant
API。 - 定义使用基于日期的值的时间段(
Period
)和使用基于时间的值的时间段(Duration
):解释并举例说明Period
和Duration
API 的用法。 - 获取日期和时间单位:编写一个程序,从表示日期时间的对象中提取日期和时间单位(例如,从日期中提取年、月、分钟等)。
- 对日期时间的加减:编写一个程序,对日期时间对象加减一定的时间(如年、日、分等)(如对日期加 1 小时,对
LocalDateTime
减 2 天等)。 - 获取 UTC 和 GMT 的所有时区:编写一个程序,显示 UTC 和 GMT 的所有可用时区。
- 获取所有可用时区的本地日期时间:编写一个程序,显示所有可用时区的本地时间。68. 显示航班日期时间信息:编写程序,显示 15 小时 30 分钟的航班时刻信息。更确切地说,是从澳大利亚珀斯飞往欧洲布加勒斯特的航班。
- 将 Unix 时间戳转换为日期时间:编写将 Unix 时间戳转换为
java.util.Date
和java.time.LocalDateTime
的程序。 - 查找月份的第一天/最后一天:编写一个程序,通过 JDK8,
TemporalAdjusters
查找月份的第一天/最后一天。 - 定义/提取区域偏移:编写一个程序,展示定义和提取区域偏移的不同技术。
-
Date
与Temporal
之间的转换:编写Date
与Instant
、LocalDate
、LocalDateTime
等之间的转换程序。 - 迭代一系列日期:编写一个程序,逐日(以一天的步长)迭代一系列给定日期。
- 计算年龄:编写一个计算一个人年龄的程序。
- 一天的开始和结束:编写一个程序,返回一天的开始和结束时间。
- 两个日期之间的差异:编写一个程序,计算两个日期之间的时间量(以天为单位)。
- 实现象棋时钟:编写实现象棋时钟的程序。
以下各节介绍上述问题的解决方案。记住,通常没有一个正确的方法来解决一个特定的问题。另外,请记住,这里显示的解释仅包括解决问题所需的最有趣和最重要的细节。下载示例解决方案以查看更多详细信息,并在这个页面中试用程序。
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-01
。LocalDate
提供了一种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
。
从LocalDate
、LocalDateTime
或ZonedDateTime
到String
的转换至少可以通过两种方式完成:
- 依赖于
LocalDate
、LocalDateTime
或ZonedDateTime.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);
在讨论中添加一个明确的时区可以如下所示:
代码语言:javascript复制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);
这次,字符串表示欧洲/巴黎时区中的日期/时间:
代码语言:javascript复制// 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 开始,日期和时间可以通过专用类LocalDate
和LocalTime
从java.time
包中获得:
// 2019-02-24
LocalDate onlyDate = LocalDate.now();
// 12:53:28.812637300
LocalTime onlyTime = LocalTime.now();
61 LocalDate
和LocalTime
中的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)
这导致LocalDate
和LocalTime
,如下所示:
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();
使用以下代码段可以获得类似的输出:
代码语言:javascript复制OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
或者,使用以下代码段:
代码语言:javascript复制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 分钟,请使用以下代码段:
代码语言:javascript复制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);
在Instant
和LocalDateTime
、ZonedDateTime
和OffsetDateTime
之间转换
这些常见的转换可以在以下示例中完成:
- 在
Instant
和LocalDateTime
之间转换-因为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);
- 在
Instant
和ZonedDateTime
之间转换—将Instant
UTC 0 转换为巴黎ZonedDateTime
UTC 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();
- 在
Instant
和OffsetDateTime
之间转换-指定 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.Period
和java.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 周期格式PnYnMnD
和PnW
的String
对象获得Period
。例如,P2019Y2M25D
字符串表示 2019 年、2 个月和 25 天:
Period periodFromString = Period.parse("P2019Y2M25D");
调用Period.toString()
将返回时间段,同时也遵循 ISO-8601 时间段格式,PnYnMnD
和PnW
(例如P120D
、P2000Y11M24D
)。
但是,当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
周期,如果B
在A
之前,应用Period.between(A, B)
的结果可以是负的,如果A
在B
之前,应用isNegative()
的结果可以是正的,如果B
在A
之前,false
在A
之前,则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.nS
的String
对象获得Duration
,其中天被认为正好是 24 小时。例如,P2DT3H4M
字符串有 2 天 3 小时 4 分钟:
Duration durationFromString = Duration.parse("P2DT3H4M");
调用Duration.toString()
将返回符合 ISO-8601 持续时间格式的持续时间PnDTnHnMn.nS
(例如,PT10H
、PT3M
或PT51H4M
)。
但是,与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
,如果B
在A
之前,应用Duration.between(A, B)
的结果可以是负的,如果A
在B
之前,应用Duration.between(A, B)
的结果可以是正的,进一步逻辑,isNegative()
如果B
在A
之前,则返回true
,如果A
在B
之前,则返回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.109389100
的LocalDateTime
对象可以被切割成日期时间单位,结果如下:
Year: 2019 Month: 2 Day: 25 Hour: 12 Minute: 58 Second: 13 Nano: 109389100
通过一点直觉和文档,很容易将此示例改编为LocalDate
、LocalTime
、ZonedDateTime
和其他示例。
65 日期时间的加减
这个问题的解决方案依赖于专用于处理日期和时间的 Java API。让我们在下一节中看看它们。
使用Date
对于Date
对象,解决方案可能依赖于Calendar
实例。绑定到本书的代码包含此解决方案。
使用LocalDateTime
跳转到 JDK8,重点是LocalDate
、LocalTime
、LocalDateTime
、Instant
等等。新的 Java 日期时间 API 提供了专门用于加减时间量的方法。LocalDate
、LocalTime
、LocalDateTime
、ZonedDateTime
、OffsetDateTime
、Instant
、Period
、Duration
以及许多其他方法,如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);
输出将显示以下日期:
代码语言:javascript复制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 个小时:
代码语言:javascript复制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<String> 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
格式。
现在,让我们将这些步骤合并到一个辅助方法中:
代码语言:javascript复制public static List<String> fetchTimeZones(OffsetType type) {
List<String> timezones = new ArrayList<>();
Set<String> 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<String> timezones
= DateTimes.fetchTimeZones(DateTimes.OffsetType.GMT);
Collections.sort(timezones); // optional sort
timezones.forEach(System.out::println);
此外,还显示了一个输出快照,如下所示:
代码语言:javascript复制(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 获取所有可用时区中的本地日期时间
可通过以下步骤获得此问题的解决方案:
- 获取本地日期和时间。
- 获取可用时区。
- 在 JDK8 之前,使用
SimpleDateFormat
和setTimeZone()
方法。 - 从 JDK8 开始,使用
ZonedDateTime
。
JDK8 之前
在 JDK8 之前,获取当前本地日期时间的快速解决方案是调用Date
空构造器。此外,还可以使用Date
在所有可用的时区中显示,这些时区可以通过TimeZone
类获得。绑定到本书的代码包含此解决方案。
从 JDK8 开始
从 JDK8 开始,获取默认时区中当前本地日期时间的一个方便解决方案是调用ZonedDateTime.now()
方法:
ZonedDateTime zlt = ZonedDateTime.now();
所以,这是默认时区中的当前日期。此外,该日期应显示在通过ZoneId
类获得的所有可用时区中:
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
最后,代码可以循环zoneIds
,对于每个区域 ID,可以调用ZonedDateTime.withZoneSameInstant(ZoneId zone)
方法。此方法返回具有不同时区的此日期时间的副本,并保留以下瞬间:
public static List<String> localTimeToAllTimeZones() {
List<String> result = new ArrayList<>();
Set<String> 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;
}
此方法的输出快照可以如下所示:
代码语言:javascript复制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):
代码语言:javascript复制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);
现在,让我们计算一下布加勒斯特的日期时间和珀斯的出发日期时间。基本上,以下代码表示从布加勒斯特时区的珀斯时区出发的日期和时间:
代码语言:javascript复制// 10:00 AM, Feb 26, 2019 0200 Europe/Bucharest
ZonedDateTime euBucharestDepart
= auPerthDepart.withZoneSameInstant(ZoneId.of("Europe/Bucharest"));
最后,让我们计算一下到达布加勒斯特的日期和时间。以下代码表示布加勒斯特时区珀斯时区的到达日期时间:
代码语言:javascript复制// 01:30 AM, Feb 27, 2019 0200 Europe/Bucharest
ZonedDateTime euBucharestArrive
= auPerthArrive.withZoneSameInstant(ZoneId.of("Europe/Bucharest"));
如下图所示,从珀斯出发的 UTC 时间是上午 8:00,而到达布加勒斯特的 UTC 时间是晚上 11:30:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rdxM6AQc-1657077517351)(img/09ecaf27-f809-42b1-8858-ecf44aa33d5f.png)]
这些时间可以很容易地提取为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、2822Thursday, 14-Nov-19 22:00:00 UTC
,RFC 28222019-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);
上一示例中获得的瞬间可用于创建LocalDateTime
或ZonedDateTime
,如下所示:
// 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、Temporal
和TemporalAdjuster
接口。
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);
让我们看看二月的第一天/最后一天是什么时候:
代码语言:javascript复制// 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 表达式,如以下辅助方法中所示:
代码语言:javascript复制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 日):
代码语言:javascript复制// 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);
}
}
下面是用法示例:
代码语言:javascript复制NextSaturdayAdjuster nsa = new NextSaturdayAdjuster();
// 2019-03-02
LocalDate nextSaturday = date.with(nsa);
71 定义/提取区域偏移
通过区域偏移,我们了解需要从 GMT/UTC 时间中添加/减去的时间量,以便获得全球特定区域(例如,澳大利亚珀斯)的日期时间。通常,区域偏移以固定的小时和分钟数打印: 02:00
、-08:30
、 0400
、UTC 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
对象。例如,使用它将区域偏移添加到OffsetTime
和OffsetDateTime
(用于在数据库中存储日期或通过电线发送的方便方法):
OffsetTime offsetTime = OffsetTime.now(zoneOffsetFromString);
OffsetDateTime offsetDateTime
= OffsetDateTime.now(zoneOffsetFromString);
我们问题的另一个解决方法是依赖于从小时、分钟和秒来定义ZoneOffset
。ZoneOffset
的一个助手方法专门用于:
// 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
类—Instant
、LocalDate
、LocalDateTime
、ZonedDateTime
、OffsetDateTime
、LocalTime
和OffsetTime
。
Date
-Instant
为了从Date
转换到Instant
,可采用Date.toInstant()
方法求解。可通过Date.from(Instant instant)
方法实现反转:
Date
到Instant
可以这样完成:
Date date = new Date();
// e.g., 2019-02-27T12:02:49.369Z, UTC
Instant instantFromDate = date.toInstant();
Instant
到Date
可以这样完成:
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);
}
此外,让我们使用以下屏幕截图中的方法来丰富此类:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VC4DDDjb-1657077517352)(img/ae07c212-3c31-4d8f-91b1-b494d5b9d393.png)]
屏幕截图中的常量DEFAULT_TIME_ZONE
是系统默认时区:
public static final ZoneId DEFAULT_TIME_ZONE = ZoneId.systemDefault();
Date
–LocalDate
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();
}
从LocalDate
到Date
的转换应该考虑到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());
}
Date
–LocalDateTime
从Date
到DateLocalTime
的转换与从Date
到LocalDate
的转换是一样的,只是溶液应该调用toLocalDateTime()
方法如下:
// e.g., 2019-03-01T07:25:25.624
public static LocalDateTime dateToLocalDateTime(Date date) {
return dateToInstant(date).atZone(
DEFAULT_TIME_ZONE).toLocalDateTime();
}
从LocalDateTime
到Date
的转换非常简单。只需应用系统默认时区并调用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());
}
Date
–ZonedDateTime
Date
到ZonedDateTime
的转换可以通过从给定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());
}
Date
–OffsetDateTime
从Date
到OffsetDateTime
的转换依赖于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();
}
从OffsetDateTime
到Date
的转换方法需要两个步骤。首先将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())));
}
Date
–LocalTime
将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);
}
输出的快照应如下所示:
代码语言:javascript复制2019-02-01
2019-02-02
2019-02-03
...
2019-02-20
从 JDK9 开始
JDK9 可以用一行代码解决这个问题。由于新的LocalDate.datesUntil(LocalDate endExclusive)
方法,这是可能的。此方法返回Stream<LocalDate>
,增量步长为一天:
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 周),如下所示:
代码语言:javascript复制2019-02-01
2019-02-08
2019-02-15
74 计算年龄
可能最常用的两个日期之间的差异是关于计算一个人的年龄。通常,一个人的年龄以年表示,但有时应提供月,甚至天。
JDK8 之前
在 JDK8 之前,试图提供一个好的解决方案可以依赖于Calendar
和/或SimpleDateFormat
。绑定到本书的代码包含这样一个解决方案。
从 JDK8 开始
更好的方法是升级到 JDK8,并依赖以下简单的代码片段:
代码语言:javascript复制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());
这相当于:
代码语言:javascript复制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());
这相当于:
代码语言:javascript复制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.Date
和Calendar
存在许多问题,因此建议避免尝试用它们实现此问题的解决方案。
76 两个日期之间的差异
计算两个日期之间的差值是一项非常常见的任务(例如,请参阅“计算年龄”部分)。让我们看看其他方法的集合,这些方法可以用来获得以毫秒、秒、小时等为单位的两个日期之间的差异。
JDK8 之前
建议通过java.util.Date
和Calendar
类来表示日期时间信息。最容易计算的差异用毫秒表示。绑定到本书的代码包含这样一个解决方案。
从 JDK8 开始
从 JDK8 开始,建议通过Temporal
(例如,DateTime
、DateLocalTime
、ZonedDateTime
等)来表示日期时间信息。
假设两个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
对象之间的时间量。让我们看看计算ld1
和ld2
之间的差值的工作原理,以天、月和年为单位:
// 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");
使用允许我们指定时间单位的方法如下:
代码语言:javascript复制// 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);
现在,让我们看看ldt1
和ldt2
之间的区别,用分钟表示:
// 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()
来表示zdt1
和zdt2
之间的差分,用ZonedDateTime.until()
来表示zdt1
和zdt2
之间的差分,用小时表示:
// 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 有四种实现:SystemClock
、OffsetClock
、TickClock
和FixedClock
。对于每个实现,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()
方法,它以毫秒为单位返回时钟的当前时刻。
假设我们要实现一个时钟,它充当象棋时钟:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QaOdoWVZ-1657077517353)(img/ad6496ac-4425-407c-9a5f-c922283d6bcb.png)]
为了实现一个Clock
类,需要遵循以下几个步骤:
- 扩展
Clock
类。 - 执行
Serializable
。 - 至少覆盖从
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();
此外,玩家执行以下一系列动作,直到右边的玩家赢得游戏:
代码语言:javascript复制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 秒后结束:
代码语言:javascript复制Game ended:2019-03-01T14:03:26.350749300Z
Instant end = chessClock.gameEnd();
Game duration: 40 seconds
// Duration.between(start, end).getSeconds();
总结
任务完成了!本章提供了使用日期和时间信息的全面概述。广泛的应用必须处理这类信息。因此,将这些问题的解决方案放在你的工具带下不是可选的。从Date
、Calendar
到LocalDate
、LocalTime
、LocalDateTime
、ZoneDateTime
、OffsetDateTime
、OffsetTime
、Instant
——它们在涉及日期和时间的日常任务中都是非常重要和有用的。