在本章中,我们将介绍以下配方:
- 如何构造与时区无关的日期和时间实例
- 如何构造时区相关的时间实例
- 如何在日期实例之间创建基于日期的期间
- 如何在时间实例之间创建基于时间的时段
- 如何表示大纪元时间
- 如何操作日期和时间实例
- 如何比较日期和时间
- 如何使用不同的日历系统
- 如何使用
DateTimeFormatter
设置日期格式
介绍
在 Stephen Colebourne 之前,与java.util.Date
和java.util.Calendar
一起工作对 Java 开发人员来说是一种痛苦。 Java8 引入了 JodaTime,一个用 Java 处理日期和时间的库。与 JDK API 相比,Joda Time 提供了以下优势:
- 更丰富的 API,用于获取日期组件(如一个月的一天、一周的一天、一个月和一年),以及时间组件(如小时、分钟和秒)。
- 易于操作和比较日期和时间。
- 时区独立 API 和时区相关 API 都可用。大多数情况下,我们将使用与时区无关的 API,这使得 API 的使用更加容易。
- 惊人的 API 计算日期和时间之间的持续时间。
- 默认情况下,日期格式和持续时间计算遵循 ISO 标准。
- 支持多种日历,如公历、佛历和伊斯兰教日历。
JodaTime 启发了 JSR-310,它在
java.time
包下将 API 移植到 JDK,并作为 Java8 的一部分发布。由于新的日期/时间 API 基于 ISO 标准,因此跨应用程序的不同层集成日期/时间库非常简单。例如,在 JavaScript 层,我们可以使用 moment.js 处理日期和时间,并使用其默认格式样式(符合 ISO 标准)向服务器发送数据。在服务器层,我们可以根据需要使用新的日期/时间 API 来获取日期和时间实例。因此,我们使用标准的日期表示在客户机和服务器之间进行交互。
在本章中,我们将研究利用新的日期/时间 API 的不同方法。
如何使用与时区无关的日期和时间实例
在 JSR-310 之前,为日历中的任何时间点或任何一天创建日期和时间实例并不简单。唯一的方法是使用java.util.Calendar
对象设置所需的日期和时间,然后调用getTime()
方法获取java.util.Date
的实例。这些日期和时间实例也包含时区信息,这有时会导致应用程序出现错误。
在新的 API 中,获取日期和时间实例要简单得多,而且这些日期和时间实例没有任何与之关联的时区信息。在此配方中,我们将向您展示如何使用由java.time.LocalDate
表示的仅日期实例、由java.time.LocalTime
表示的仅时间实例以及由java.time.LocalDateTime
表示的日期/时间实例。这些日期和时间实例与时区无关,表示机器当前时区中的信息。
准备
您至少需要安装 JDK 8 才能使用这些较新的库,本章中的示例使用 Java 10 及以后版本支持的语法。如果需要,可以直接在 JShell 中运行这些代码段。您可以使用 JShell 访问第十二章“读取评估打印循环(REPL)”,了解更多关于 JShell 的信息。
怎么做…
java.time.LocalDate
中包装的当前日期可以通过now()
方法获得,如下所示:var date = LocalDate.now();
- 我们可以使用通用的
get(fieldName)
方法或具体的getDayOfMonth()
、getDayOfYear()
、getDayOfWeek()
、getMonth()
、getYear()
等方法获取java.time.LocalDate
实例的各个字段,如下所示:var dayOfWeek = date.getDayOfWeek(); var dayOfMonth = date.getDayOfMonth(); var month = date.getMonth(); var year = date.getYear();
- 我们可以使用
of()
方法获取日历中任意日期的java.time.LocalDate
实例,如下所示:var date1 = LocalDate.of(2018, 4, 12); var date2 = LocalDate.of(2018, Month.APRIL, 12); date2 = LocalDate.ofYearDay(2018, 102); date2 = LocalDate.parse("2018-04-12");
- 有
java.time.LocalTime
类,用于表示任何时间实例,而不考虑日期。可使用以下方法获得当前时间:var time = LocalTime.now();
java.time.LocalTime
类还附带了of()
工厂方法,可用于创建表示任何时间的实例。同样,也有一些方法可以获得时间的不同部分,如下所示:time = LocalTime.of(23, 11, 11, 11); time = LocalTime.ofSecondOfDay(3600); var hour = time.getHour(); var minutes = time.getMinute(); var seconds = time.get(ChronoField.SECOND_OF_MINUTE);
java.time.LocalDateTime
用于表示同时包含时间和日期的实体。由java.time.LocalDate
和java.time.LocalTime
组成,分别表示日期和时间。可以使用now()
和of()
工厂方法的不同风格创建其实例,如下所示:var dateTime1 = LocalDateTime.of(2018, 04, 12, 13, 30, 22); var dateTime2 = LocalDateTime.of(2018, Month.APRIL, 12, 13, 30, 22); dateTime2 = LocalDateTime.of(date2, LocalTime.of(13, 30, 22));
它是如何工作的…
java.time
包中的以下三个类代表默认时区(系统时区)中的日期和时间值:
java.time.LocalDate
:仅包含日期信息java.time.LocalTime
:仅包含时间信息java.time.LocalDateTime
:包含日期和时间信息
每个类都由字段组成,即以下字段:
- 一天
- 月
- 年
- 时辰
- 会议记录
- 秒
- 毫秒
所有类都包含返回当前日期和时间值的now()
方法。提供了of()
工厂方法,从其字段(如日、月、年、小时和分钟)构建日期和时间实例。java.time.LocalDateTime
由java.time.LocalDate
和java.time.LocalTime
组成,因此可以从java.time.LocalDate
和java.time.LocalTime
构建java.time.LocalDateTime
。
从这个配方中学到的重要 API 如下:
now()
:给出当前日期和时间of()
:此工厂方法用于构造所需的日期、时间和日期/时间实例
还有更多…
在 Java 9 中,有一个新的 API,datesUntil
,它获取结束日期并返回从当前对象的日期到结束日期(但不包括结束日期)的连续日期流(换句话说,java.time.LocalDate
。使用此 API 可将给定月份和年份的所有日期分组为一周中各自的几天,即周一、周二、周三等。
让我们接受月份和年份,并将其分别存储在month
和year
变量中。该范围的起始日期为该月和该年的第一天,如下所示:
var startDate = LocalDate.of(year, month, 1);
范围的结束日期将是当月的天数,如以下代码段所示:
var endDate = startDate.plusDays(startDate.lengthOfMonth());
我们正在使用lengthOfMonth
方法获取月份的天数。然后我们使用datesUntil
方法得到java.time.LocalDate
的流,然后我们执行一些流操作:
- 按一周中的某一天对
java.time.LocalDate
实例进行分组。 - 正在将分组的实例收集到
java.util.ArrayList
。但在此之前,我们正在应用一个转换,将java.time.LocalDate
实例转换为一个简单的月份,这将为我们提供一个表示月份日期的整数列表。
代码中的前两个操作显示在以下代码段中:
var dayBuckets = startDate.datesUntil(endDate).collect(
Collectors.groupingBy(date -> date.getDayOfWeek(),
Collectors.mapping(LocalDate::getDayOfMonth,
Collectors.toList())
));
此代码可在下载代码的Chapter13/1_2_print_calendar
处找到。
如何构造时区相关的时间实例
在前面的配方“如何构造时区独立的日期和时间实例”中,我们构造了不包含任何时区信息的日期和时间对象。它们隐式地表示系统时区中的值;这些类别分别为java.time.LocalDate
、java.time.LocalTime
和java.time.LocalDateTime
。
通常,我们需要表示某个时区的时间;在这种情况下,我们将使用java.time.ZonedDateTime
,它包含时区信息和java.time.LocalDateTime
。使用java.time.ZoneId
或java.time.ZoneOffset
实例嵌入时区信息。还有另外两个类,java.time.OffsetTime
和java.time.OffsetDateTime
,它们也是java.time.LocalTime
和java.time.LocalDateTime
的时区特定变体。
在本食谱中,我们将向您展示如何使用java.time.ZonedDateTime
、java.time.ZoneId
、java.time.ZoneOffset
、java.time.OffsetTime
和java.time.OffsetDateTime
。
准备
我们将使用 Java10 语法,该语法将var
用于局部变量声明和模块。除了 Java10 和更高版本之外,没有其他先决条件。
怎么做…
- 我们将使用
now()
工厂方法,根据系统时区获取当前日期、时间和时区信息,如下所示:var dateTime = ZonedDateTime.now();
- 我们将利用
java.time.ZoneId
获取基于任何给定时区的当前日期和时间信息:var indianTz = ZoneId.of("Asia/Kolkata"); var istDateTime = ZonedDateTime.now(indianTz);
java.time.ZoneOffset
还可用于提供日期和时间的时区信息,如下所示:var indianTzOffset = ZoneOffset.ofHoursMinutes(5, 30); istDateTime = ZonedDateTime.now(indianTzOffset);
- 我们利用
of()
工厂方法构建java.time.ZonedDateTime
实例:ZonedDateTime dateTimeOf = ZonedDateTime.of(2018, 4, 22, 14, 30, 11, 33, indianTz);
- 我们甚至可以从
java.time.ZonedDateTime
中提取java.time.LocalDateTime
:var localDateTime = dateTimeOf.toLocalDateTime();
它是如何工作的…
首先,让我们看看如何捕获时区信息。它是根据格林威治标准时间(GMT)(也称为协调世界时(UTC))的小时数和分钟数捕获的。例如,印度标准时间(IST),也称为亚洲/加尔各答,比格林尼治标准时间早 5:30 小时。
Java 提供了java.time.ZoneId
和java.time.ZoneOffset
来表示时区信息。java.time.ZoneId
根据时区名称捕获时区信息,如亚洲/加尔各答、美国/太平洋和美国/山区。大约有 599 个区域 ID。这是使用以下代码行计算的:
jshell> ZoneId.getAvailableZoneIds().stream().count()
$16 ==> 599
我们将打印 10 个区域 ID:
jshell> ZoneId.getAvailableZoneIds().stream().limit(10).forEach(System.out::println)
Asia/Aden
America/Cuiaba
Etc/GMT+9
Etc/GMT+8
Africa/Nairobi
America/Marigot
Asia/Aqtau
Pacific/Kwajalein
America/El_Salvador
Asia/Pontianak
Time zone names, such as Asia/Kolkata, Africa/Nairobi, and America/Cuiaba, are based on the time zone database released by International Assigned Numbers Authority (IANA). The time zone region names provided by IANA are the default for Java. Sometimes time zone region names are also represented as GMT+02:30 or simply +02:30, which indicates the offset (ahead or behind) of the current time zone from the GMT zone.
此java.time.ZoneId
捕获java.time.zone.ZoneRules
,其中包含获取时区偏移过渡和其他信息(如夏令时)的规则。让我们调查一下美国/太平洋地区的区域规则:
jshell> ZoneId.of("US/Pacific").getRules().getDaylightSavings(Instant.now())
$31 ==> PT1H
jshell> ZoneId.of("US/Pacific").getRules().getOffset(LocalDateTime.now())
$32 ==> -07:00
jshell> ZoneId.of("US/Pacific").getRules().getStandardOffset(Instant.now())
$33 ==> -08:00
getDaylightSavings()
方法返回一个java.time.Duration
对象,该对象以小时、分钟和秒表示一些持续时间。默认的toString()
实现返回使用基于 ISO 8601 秒的表示法表示的持续时间,其中 1 小时、20 分钟和 20 秒的持续时间表示为PT1H20M20S
。关于这方面的更多信息将在本章的“如何在时间实例之间创建基于时间的时段”配方中介绍。
我们不会详细讨论它是如何计算的。欲了解更多关于java.time.zone.ZoneRules
和java.time.ZoneId
的信息,请分别访问这里和这里。
java.time.ZoneOffset
类根据时区在 GMT 之前或之后的小时数和分钟数捕获时区信息。让我们使用of*()
工厂方法创建java.time.ZoneOffset
类的实例:
jshell> ZoneOffset.ofHoursMinutes(5,30)
$27 ==> +05:30
java.time.ZoneOffset
类从java.time.ZoneId
扩展并添加了一些新方法。需要记住的重要一点是根据应用程序中使用的所需时区构造正确的java.time.ZoneOffset
和java.time.ZoneId
实例。
现在我们已经了解了时区表示法,java.time.ZonedDateTime
只不过是java.time.LocalDateTime
以及java.time.ZoneId
或java.time.ZoneOffset
。还有另外两个类,java.time.OffsetTime
和java.time.OffsetDateTime
,分别与java.time.ZoneOffset
一起包装java.time.LocalTime
和java.time.LocalDateTime
。
让我们来看一些构造java.time.ZonedDateTime
实例的方法。
第一种方式是使用now()
:
Signatures:
ZonedDateTime ZonedDateTime.now()
ZonedDateTime ZonedDateTime.now(ZoneId zone)
ZonedDateTime ZonedDateTime.now(Clock clock)
jshell> ZonedDateTime.now()
jshell> ZonedDateTime.now(ZoneId.of("Asia/Kolkata"))
$36 ==> 2018-05-04T21:58:24.453113900+05:30[Asia/Kolkata]
jshell> ZonedDateTime.now(Clock.fixed(Instant.ofEpochSecond(1525452037), ZoneId.of("Asia/Kolkata")))
$54 ==> 2018-05-04T22:10:37+05:30[Asia/Kolkata]
第一次使用now()
使用系统时钟以及系统时区打印当前日期和时间。第二次使用now()
使用系统时钟,但时区由java.time.ZoneId
提供,在本例中为亚洲/加尔各答。now()
的第三种用法使用java.time.ZoneId
提供的固定时钟和时区。
固定时钟是使用java.time.Clock
类及其静态方法fixed()
创建的,该方法以java.time.Instant
和java.time.ZoneId
为例。java.time.Instant
的实例是在纪元之后使用一些静态秒数构建的。java.time.Clock
用于表示一个时钟,新的日期/时间 API 可以使用该时钟来确定当前时间。如前所述,时钟可以固定,然后我们可以在亚洲/加尔各答时区创建一个比当前系统时间提前一小时的时钟,如下所示:
var hourAheadClock = Clock.offset(Clock.system(ZoneId.of("Asia/Kolkata")), Duration.ofHours(1));
我们可以使用这个新时钟来构建java.time.LocalDateTime
和java.time.ZonedDateTime
的实例,如下所示:
jshell> LocalDateTime.now(hourAheadClock)
$64 ==> 2018-05-04T23:29:58.759973700
jshell> ZonedDateTime.now(hourAheadClock)
$65 ==> 2018-05-04T23:30:11.421913800+05:30[Asia/Kolkata]
日期和时间值都基于相同的时区,即Asia/Kolkata
,但正如我们已经了解到的,java.time.LocalDateTime
没有任何时区信息,它基于系统的时区或本例中提供的java.time.Clock
来计算。另一方面,java.time.ZonedDateTime
包含时区信息并显示为[Asia/Kolkata]
。
创建java.time.ZonedDateTime
实例的另一种方法是使用其of()
工厂方法:
Signatures:
ZonedDateTime ZonedDateTime.of(LocalDate date, LocalTime time, ZoneId zone)
ZonedDateTime ZonedDateTime.of(LocalDateTime localDateTime, ZoneId zone)
ZonedDateTime ZonedDateTime.of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond, ZoneId zone)
jshell> ZonedDateTime.of(LocalDateTime.of(2018, 1, 1, 13, 44, 44), ZoneId.of("Asia/Kolkata"))
$70 ==> 2018-01-01T13:44:44+05:30[Asia/Kolkata]
jshell> ZonedDateTime.of(LocalDate.of(2018,1,1), LocalTime.of(13, 44, 44), ZoneId.of("Asia/Kolkata"))
$71 ==> 2018-01-01T13:44:44+05:30[Asia/Kolkata]
jshell> ZonedDateTime.of(LocalDate.of(2018,1,1), LocalTime.of(13, 44, 44), ZoneId.of("Asia/Kolkata"))
$72 ==> 2018-01-01T13:44:44+05:30[Asia/Kolkata]
jshell> ZonedDateTime.of(2018, 1, 1, 13, 44, 44, 0, ZoneId.of("Asia/Kolkata"))
$73 ==> 2018-01-01T13:44:44+05:30[Asia/Kolkata]
还有更多…
我们提到了java.time.OffsetTime
和java.time.OffsetDateTime
类。两者都包含特定于时区的时间值。在总结本食谱之前,让我们先来看看这些课程:
- 使用
of()
工厂方法:jshell> OffsetTime.of(LocalTime.of(14,12,34), ZoneOffset.ofHoursMinutes(5, 30)) $74 ==> 14:12:34+05:30 jshell> OffsetTime.of(14, 34, 12, 11, ZoneOffset.ofHoursMinutes(5, 30)) $75 ==> 14:34:12.000000011+05:30
- 使用
now()
工厂方法:Signatures: OffsetTime OffsetTime.now() OffsetTime OffsetTime.now(ZoneId zone) OffsetTime OffsetTime.now(Clock clock) jshell> OffsetTime.now() $76 ==> 21:49:16.895192800+03:00 jshell> OffsetTime.now(ZoneId.of("Asia/Kolkata")) jshell> OffsetTime.now(ZoneId.of("Asia/Kolkata")) $77 ==> 00:21:04.685836900+05:30 jshell> OffsetTime.now(Clock.offset(Clock.systemUTC(), Duration.ofMinutes(330))) $78 ==> 00:22:00.395463800Z
值得注意的是,我们构建java.time.Clock
实例的方式比 UTC 时钟早 330 分钟(5 小时 30 分钟)。另一个类java.time.OffsetDateTime
与java.time.OffsetTime
相同,只是它使用了java.time.LocalDateTime
。因此,您将把日期信息,即年、月和日,连同时间信息一起传递到其工厂方法of()
。
如何在日期实例之间创建基于日期的期间
过去,我们曾尝试测量两个日期实例之间的时间间隔,但由于 Java 8 之前缺乏 API,也缺乏捕获此信息的适当支持,因此我们采用了不同的方法。我们记得使用基于 SQL 的方法来处理此类信息。但是从 Java8 开始,我们有了一个新的类,java.time.Period
,它可以用来捕获两个日期实例之间的时间段,以年、月和日为单位。
此外,此类还支持解析用于表示句点的基于 ISO 8601 标准的字符串。本标准规定,任何时期都可以用PnYnMnD
表示,其中P
为固定字符表示时期,nY
表示年数,nM
表示月数,nD
表示天数。例如,2 年、4 个月和 10 天的周期表示为P2Y4M10D
。
准备
您至少需要 JDK8 来处理java.time.Period
,jdk9 来使用 JShell,至少需要 JDK10 来使用本配方中使用的示例。
怎么做…
- 让我们使用
java.time.Period
的of()
工厂方法创建一个java.time.Period
实例,其签名为Period.of(int years, int months, int days)
:jshell> Period.of(2,4,30) $2 ==> P2Y4M30D
of*()
方法有具体变体,即ofDays()
、ofMonths()
和ofYears()
,也可以使用:jshell> Period.ofDays(10) $3 ==> P10D jshell> Period.ofMonths(4) $4 ==> P4M jshell> Period.ofWeeks(3) $5 ==> P21D jshell> Period.ofYears(3) $6 ==> P3Y
请注意,ofWeeks()
方法是一种助手方法,通过接受周数,基于天构建java.time.Period
。
- 周期也可以使用周期字符串构造,周期字符串的形式通常为
P<x>Y<y>M<z>D
,其中x
、y
和z
分别表示年数、月数和天数:jshell> Period.parse("P2Y4M23D").getDays() $8 ==> 23
- 我们还可以计算
java.time.ChronoLocalDate
的两个实例之间的周期(它的一个实现是java.time.LocalDate
:jshell> Period.between(LocalDate.now(), LocalDate.of(2018, 8, 23)) $9 ==> P2M2D jshell> Period.between(LocalDate.now(), LocalDate.of(2018, 2, 23)) $10 ==> P-3M-26D
这些是创建java.time.Period
实例最有用的方法。开始日期为包含日期,结束日期为独占日期。
它是如何工作的…
我们使用java.time.Period
中的工厂方法来创建它的实例。java.time.Period
有三个字段分别保存年、月和日的值,如下所示:
/**
* The number of years.
*/
private final int years;
/**
* The number of months.
*/
private final int months;
/**
* The number of days.
*/
private final int days;
有一组有趣的方法,即withDays()
、withMonths()
和withYears()
。如果要更新的字段具有相同的值,则这些方法返回相同的实例;否则,它将返回一个具有更新值的新实例,如下所示:
jshell> Period period1 = Period.ofWeeks(2)
period1 ==> P14D
jshell> Period period2 = period1.withDays(15)
period2 ==> P15D
jshell> period1 == period2
$19 ==> false
jshell> Period period3 = period1.withDays(14)
period3 ==> P14D
jshell> period1 == period3
$21 ==> true
还有更多…
我们甚至可以使用java.time.ChronoLocalDate
中的until()
方法计算两个日期实例之间的java.time.Period
:
jshell> LocalDate.now().until(LocalDate.of(2018, 2, 23))
$11 ==> P-3M-26D
jshell> LocalDate.now().until(LocalDate.of(2018, 8, 23))
$12 ==> P2M2D
给定一个java.time.Period
实例,我们可以使用它来操作一个给定的日期实例。有两种可能的方法:
- 使用周期对象的
addTo
或subtractFrom
方法 - 使用日期对象的
plus
或minus
方法
以下代码段中显示了这两种方法:
jshell> Period period1 = Period.ofWeeks(2)
period1 ==> P14D
jshell> LocalDate date = LocalDate.now()
date ==> 2018-06-21
jshell> period1.addTo(date)
$24 ==> 2018-07-05
jshell> date.plus(period1)
$25 ==> 2018-07-05
在类似的线路上,您可以尝试subtractFrom
和minus
方法。还有一组方法用于操作java.time.Period
实例,即:
minus
、minusDays
、minusMonths
、minusYears
:从周期中减去给定值。plus
、plusDays
、plusMonths
、plusYears
:将给定值加到期间。negated
:返回新期间,其每个值均为负数。normalized
:通过规范化其高阶字段(如月和日),返回新的期间。例如,将 15 个月标准化为 1 年零 3 个月。
我们将从minus
方法开始,向您展示这些方法的作用,如下所示:
jshell> period1.minus(Period.of(1,3,4))
$28 ==> P2Y12M25D
jshell> period1.minusDays(4)
$29 ==> P3Y15M25D
jshell> period1.minusMonths(3)
$30 ==> P3Y12M29D
jshell> period1.minusYears(1)
$31 ==> P2Y15M29D
然后我们将看到plus
方法:
jshell> Period period1 = Period.of(3, 15, 29)
period1 ==> P3Y15M29D
jshell> period1.plus(Period.of(1, 3, 4))
$33 ==> P4Y18M33D
jshell> period1.plusDays(4)
$34 ==> P3Y15M33D
jshell> period1.plusMonths(3)
$35 ==> P3Y18M29D
jshell> period1.plusYears(1)
$36 ==> P4Y15M29D
最后,这里是negated()
和normalized()
方法:
jshell> Period period1 = Period.of(3, 15, 29)
period1 ==> P3Y15M29D
jshell> period1.negated()
$38 ==> P-3Y-15M-29D
jshell> period1
period1 ==> P3Y15M29D
jshell> period1.normalized()
$40 ==> P4Y3M29D
jshell> period1
period1 ==> P3Y15M29D
请注意,在前面的两种情况下,它都没有改变现有的周期,而是返回一个新实例。
如何在时间实例之间创建基于时间的时段
在我们之前的配方中,我们创建了一个基于日期的时段,由java.time.Period
表示。在本食谱中,我们将使用java.time.Duration
类在时间实例之间创建以秒和纳秒为单位的基于时间的差异。
我们将研究不同的方法来创建java.time.Duration
实例,操作持续时间实例,并以不同的单位获取持续时间,例如小时和分钟。ISO 8601 标准规定了表示持续时间的一种可能模式为PnYnMnDTnHnMnS
,适用于以下情况:
Y
、M
、D
表示日期组件字段,即年、月、日T
将日期与时间信息分开H
、M
、S
表示时间分量字段,即小时、分钟、秒java.time.Duration
的字符串表示实现松散地基于 ISO 8601。在“工作原理”一节中有更多关于这方面的内容。
准备
您至少需要 JDK 8 才能使用java.time.Duration
和 JDK 9 才能使用 JShell。
怎么做…
java.time.Duration
实例可以使用of*()
工厂方法创建。我们将展示如何使用其中的一些,如下所示:jshell> Duration.of(56, ChronoUnit.MINUTES) $66 ==> PT56M jshell> Duration.of(56, ChronoUnit.DAYS) $67 ==> PT1344H jshell> Duration.ofSeconds(87) $68 ==> PT1M27S jshell> Duration.ofHours(7) $69 ==> PT7H
- 也可以通过解析持续时间字符串来创建它们,如下所示:
jshell> Duration.parse("P12D") $70 ==> PT288H jshell> Duration.parse("P12DT7H5M8.009S") $71 ==> PT295H5M8.009S jshell> Duration.parse("PT7H5M8.009S") $72 ==> PT7H5M8.009S
- 它们可以通过查找两个支持时间信息的
java.time.Temporal
实例(即java.time.LocalDateTime
等实例)之间的跨度来构造,如下所示:jshell> LocalDateTime time1 = LocalDateTime.now() time1 ==> 2018-06-23T10:51:21.038073800 jshell> LocalDateTime time2 = LocalDateTime.of(2018, 6, 22, 11, 00) time2 ==> 2018-06-22T11:00 jshell> Duration.between(time1, time2) $77 ==> PT-23H-51M-21.0380738S jshell> ZonedDateTime time1 = ZonedDateTime.now() time1 ==> 2018-06-23T10:56:57.965606200+03:00[Asia/Riyadh] jshell> ZonedDateTime time2 = ZonedDateTime.of(LocalDateTime.now(), ZoneOffset.ofHoursMinutes(5, 30)) time2 ==> 2018-06-23T10:56:59.878712600+05:30 jshell> Duration.between(time1, time2) $82 ==> PT-2H-29M-58.0868936S
它是如何工作的…
java.time.Duration
所需的数据存储在两个字段中,分别表示秒和纳秒。提供了以分钟、小时和天为单位获取持续时间的方便方法,即toMinutes()
、toHours()
和toDays()
。
让我们讨论一下字符串表示的实现。java.time.Duration
支持解析 ISO 字符串表示形式,该表示形式只包含日期部分中的日期组件,以及时间部分中的小时、分钟、秒和纳秒。例如,P2DT3M
是可以接受的,而解析P3M2DT3M
会导致java.time.format.DateTimeParseException
,因为字符串包含日期部分中的月份组件。
java.time.Duration
的toString()
方法总是返回一个PTxHyMz.nS
形式的字符串,其中x
表示小时数,y
表示分钟数,z.n
表示秒到纳秒的精度。让我们看一些例子:
jshell> Duration.parse("P2DT3M")
$2 ==> PT48H3M
jshell> Duration.parse("P3M2DT3M")
| Exception java.time.format.DateTimeParseException: Text cannot be parsed to a Duration
| at Duration.parse (Duration.java:417)
| at (3:1)
jshell> Duration.ofHours(4)
$4 ==> PT4H
jshell> Duration.parse("PT3H4M5.6S")
$5 ==> PT3H4M5.6S
jshell> Duration d = Duration.parse("PT3H4M5.6S")
d ==> PT3H4M5.6S
jshell> d.toDays()
$7 ==> 0
jshell> d.toHours()
$9 ==> 3
还有更多…
让我们看一下提供的操作方法,这些方法允许从特定的时间单位(如天、小时、分钟、秒或纳秒)中添加/减去值。这些方法都是不可变的,因此每次都会返回一个新实例,如下所示:
jshell> Duration d = Duration.parse("PT1H5M4S")
d ==> PT1H5M4S
jshell> d.plusDays(3)
$14 ==> PT73H5M4S
jshell> d
d ==> PT1H5M4S
jshell> d.plusDays(3)
$16 ==> PT73H5M4S
jshell> d.plusHours(3)
$17 ==> PT4H5M4S
jshell> d.plusMillis(4)
$18 ==> PT1H5M4.004S
jshell> d.plusMinutes(40)
$19 ==> PT1H45M4S
同样,你也可以尝试minus*()
方法,进行减法运算。还有一些方法可以处理java.time.LocalDateTime
、java.time.ZonedDateTime
等实例。这些方法在日期/时间信息中添加/减去持续时间。让我们看一些例子:
jshell> Duration d = Duration.parse("PT1H5M4S")
d ==> PT1H5M4S
jshell> d.addTo(LocalDateTime.now())
$21 ==> 2018-06-25T21:15:53.725373600
jshell> d.addTo(ZonedDateTime.now())
$22 ==> 2018-06-25T21:16:03.396595600+03:00[Asia/Riyadh]
jshell> d.addTo(LocalDate.now())
| Exception java.time.temporal.UnsupportedTemporalTypeException: Unsupported unit: Seconds
| at LocalDate.plus (LocalDate.java:1272)
| at LocalDate.plus (LocalDate.java:139)
| at Duration.addTo (Duration.java:1102)
| at (23:1)
您可以在前面的示例中观察到,当我们试图将持续时间添加到仅包含日期信息的实体时,出现了一个异常。
如何表示大纪元时间
在本配方中,我们将使用java.time.Instant
表示一个时间点,并将该时间点转换为历元秒/毫秒。Java 历元用于表示从1970-01-01 00:00:00Z
开始的时间瞬间,java.time.Instant
存储 Java 历元的秒数。正值表示时间在历元之前,负值表示时间在历元之后。它使用 UTC 的系统时钟来计算当前时间的瞬时值。
准备
您需要安装支持新日期/时间 API 和 JShell 的 JDK,以便能够尝试提供的解决方案。
怎么做…
- 我们只需创建一个
java.time.Instant
实例并打印出历元秒数,它将给出 Java 历元后的 UTC 时间:jshell> Instant.now() $40 ==> 2018-07-06T07:56:40.651529300Z jshell> Instant.now().getEpochSecond() $41 ==> 1530863807
- 我们还可以打印出历元毫秒数,它显示历元后的毫秒数。这比几秒钟更精确:
jshell> Instant.now().toEpochMilli() $42 ==> 1530863845158
它是如何工作的…
java.time.Instant
类在其两个字段中存储时间信息:
- 秒,属于
long
类型:存储从1970-01-01T00:00:00Z
开始的秒数。 - 纳秒,属于
int
类型:它存储纳秒数
当您调用now()
方法时,java.time.Instant
使用 UTC 的系统时钟来表示该时刻。然后我们可以使用atZone()
或atOffset()
将其转换为所需的时区,我们将在下一节中看到。
如果您只想在 UTC 中表示操作的时间线,请使用此类;这样,为不同事件存储的时间戳将基于 UTC,然后您可以根据需要将其转换为所需的时区。
还有更多…
我们可以通过添加/减去纳秒、毫秒和秒来操纵java.time.Instant
,如下所示:
jshell> Instant.now().plusMillis(1000)
$43 ==> 2018-07-06T07:57:57.092259400Z
jshell> Instant.now().plusNanos(1991999)
$44 ==> 2018-07-06T07:58:06.097966099Z
jshell> Instant.now().plusSeconds(180)
$45 ==> 2018-07-06T08:01:15.824141500Z
同样,你也可以尝试minus*()
方法。我们还可以使用java.time.Instant
方法、atOffset()
和atZone()
方法获得时区相关日期时间,如下所示:
jshell> Instant.now().atZone(ZoneId.of("Asia/Kolkata"))
$36 ==> 2018-07-06T13:15:13.820694500+05:30[Asia/Kolkata]
jshell> Instant.now().atOffset(ZoneOffset.ofHoursMinutes(2,30))
$37 ==> 2018-07-06T10:15:19.712039+02:30
如何操作日期和时间实例
日期和时间类java.time.LocalDate
、java.time.LocalTime
、java.time.LocalDateTime
和java.time.ZonedDateTime
提供了从它们的组件(即天、小时、分钟、秒、周、月、年等)中加减值的方法。
在本食谱中,我们将介绍一些这样的方法,它们可以通过加减不同的值来操作日期和时间实例。
准备
您将需要一个支持新的日期/时间 API 和 JShell 控制台的 JDK 安装。
怎么做…
- 让我们操纵
java.time.LocalDate
:jshell> LocalDate d = LocalDate.now() d ==> 2018-07-27 jshell> d.plusDays(3) $5 ==> 2018-07-30 jshell> d.minusYears(4) $6 ==> 2014-07-27
- 让我们操纵日期和时间实例,
java.time.LocalDateTime
:jshell> LocalDateTime dt = LocalDateTime.now() dt ==> 2018-07-27T15:27:40.733389700 jshell> dt.plusMinutes(45) $8 ==> 2018-07-27T16:12:40.733389700 jshell> dt.minusHours(4) $9 ==> 2018-07-27T11:27:40.733389700
- 让我们操纵时区相关的日期和时间,
java.time.ZonedDateTime
:jshell> ZonedDateTime zdt = ZonedDateTime.now() zdt ==> 2018-07-27T15:28:28.309915200+03:00[Asia/Riyadh] jshell> zdt.plusDays(4) $11 ==> 2018-07-31T15:28:28.309915200+03:00[Asia/Riyadh] jshell> zdt.minusHours(3) $12 ==> 2018-07-27T12:28:28.309915200+03:00[Asia/Riyadh]
还有更多…
我们只看了一些由plus*()
和minus*()
表示的加法和减法 API。提供了不同的方法来处理日期和时间的不同组件,例如年、日、月、小时、分、秒和纳秒。您可以尝试使用这些 API 作为练习。
如何比较日期和时间
通常,我们希望将日期和时间实例与其他实例进行比较,以检查它们是否在其他实例之前、之后或与其他实例相同。为了实现这一点,JDK 在java.time.LocalDate
、java.time.LocalDateTime
和java.time.ZonedDateTime
类中提供了isBefore()
、isAfter()
和isEqual()
方法。在本食谱中,我们将介绍如何使用这些方法来比较日期和时间实例。
准备
您将需要一个具有新的日期/时间 API 并支持 JShell 的 JDK 安装。
怎么做…
- 让我们尝试比较两个
java.time.LocalDate
实例:jshell> LocalDate d = LocalDate.now() d ==> 2018-07-28 jshell> LocalDate d2 = LocalDate.of(2018, 7, 27) d2 ==> 2018-07-27 jshell> d.isBefore(d2) $4 ==> false jshell> d.isAfter(d2) $5 ==> true jshell> LocalDate d3 = LocalDate.of(2018, 7, 28) d3 ==> 2018-07-28 jshell> d.isEqual(d3) $7 ==> true jshell> d.isEqual(d2) $8 ==> false
- 我们还可以比较时区相关的日期和时间实例:
jshell> ZonedDateTime zdt1 = ZonedDateTime.now(); zdt1 ==> 2018-07-28T14:49:34.778006400+03:00[Asia/Riyadh] jshell> ZonedDateTime zdt2 = zdt1.plusHours(4) zdt2 ==> 2018-07-28T18:49:34.778006400+03:00[Asia/Riyadh] jshell> zdt1.isBefore(zdt2) $11 ==> true jshell> zdt1.isAfter(zdt2) $12 ==> false jshell> zdt1.isEqual(zdt2) $13 ==> false
还有更多…
可在java.time.LocalTime
和java.time.LocalDateTime
上进行比较。这是留给读者去探索的。
如何使用不同的日历系统
到目前为止,在我们的食谱中,我们使用了 ISO 日历系统,这是世界上沿用的事实上的日历系统。世界上还有其他地区性的日历系统,如 Hijrah、日语和泰语。JDK 还提供对此类日历系统的支持。
在本食谱中,我们将介绍如何使用两种日历系统:日语和 Hijri。
准备
您应该安装一个支持新的日期/时间 API 和 JShell 工具的 JDK。
怎么做…
- 让我们在 JDK 支持的不同日历系统中打印当前日期:
jshell> Chronology.getAvailableChronologies().forEach(chrono -> System.out.println(chrono.dateNow())) 2018-07-30 Minguo ROC 107-07-30 Japanese Heisei 30-07-30 ThaiBuddhist BE 2561-07-30 Hijrah-umalqura AH 1439-11-17
- 让我们来看看日本日历系统中表示的日期:
jshell> JapaneseDate jd = JapaneseDate.now() jd ==> Japanese Heisei 30-07-30 jshell> jd.getChronology() $7 ==> Japanese jshell> jd.getEra() $8 ==> Heisei jshell> jd.lengthOfYear() $9 ==> 365 jshell> jd.lengthOfMonth() $10 ==> 31
- 可以使用
java.time.chrono.JapeneseEra
枚举日文日历中支持的不同纪元:jshell> JapaneseEra.values() $42 ==> JapaneseEra[5]
- 让我们在 Hijrah 日历系统中创建一个日期:
jshell> HijrahDate hd = HijrahDate.of(1438, 12, 1) hd ==> Hijrah-umalqura AH 1438-12-01
- 我们甚至可以在 Hijrah 日历系统中将 ISO 日期/时间转换为日期/时间,如下所示:
jshell> HijrahChronology.INSTANCE.localDateTime(LocalDateTime.now()) $23 ==> Hijrah-umalqura AH 1439-11-17T19:56:52.056465900 jshell> HijrahChronology.INSTANCE.localDateTime(LocalDateTime.now()).toLocalDate() $24 ==> Hijrah-umalqura AH 1439-11-17 jshell> HijrahChronology.INSTANCE.localDateTime(LocalDateTime.now()).toLocalTime() $25 ==> 19:57:07.705740500
它是如何工作的…
日历系统由java.time.chrono.Chronology
及其实现表示,其中有几个是java.time.chrono.IsoChronology
、java.time.chrono.HijrahChronology
和java.time.chrono.JapaneseChronology
。java.time.chrono.IsoChronology
是世界上使用的基于 ISO 的事实日历系统。每个日历系统中的日期由java.time.chrono.ChronoLocalDate
及其实现表示,其中一些是java.time.chrono.HijrahDate
、java.time.chrono.JapaneseDate
和众所周知的java.time.LocalDate
。
为了能够在 JShell 中使用这些 API,您需要导入相关包,如下所示:
jshell> import java.time.*
jshell> import java.time.chrono.*
这适用于所有使用 JShell 的配方。
我们可以直接使用java.time.chrono.ChronoLocalDate
的实现,例如java.time.chrono.JapaneseDate
,或者使用java.time.chrono.Chronology
的实现来获取相关的日期表示,如下所示:
jshell> JapaneseDate jd = JapaneseDate.of(JapaneseEra.SHOWA, 26, 12, 25)
jd ==> Japanese Showa 26-12-25
jshell> JapaneseDate jd = JapaneseDate.now()
jd ==> Japanese Heisei 30-07-30
jshell> JapaneseDate jd = JapaneseChronology.INSTANCE.dateNow()
jd ==> Japanese Heisei 30-07-30
jshell> JapaneseDate jd = JapaneseChronology.INSTANCE.date(LocalDateTime.now())
jd ==> Japanese Heisei 30-07-30
jshell> ThaiBuddhistChronology.INSTANCE.date(LocalDate.now())
$41 ==> ThaiBuddhist BE 2561-07-30
从前面的代码片段中,我们可以看到,可以使用日历系统的date(TemporalAccessor temporal)
方法将 ISO 系统日期转换为所需日历系统中的日期。
还有更多…
您可以使用 JDK 支持的其他日历系统,即泰国、佛教和民国(中文)日历系统。通过编写java.time.chrono.Chronology
、java.time.chrono.ChronoLocalDate
和java.time.chrono.Era
的实现来创建我们的定制日历系统也是值得探索的。
如何使用DateTimeFormatter
格式化日期
在使用java.util.Date
时,我们使用java.text.SimpleDateFormat
将日期格式化为不同的文本表示形式,反之亦然。格式化日期是指给定一个日期或一个时间对象,以不同的格式表示该日期,例如:
- 2018 年 6 月 23 日
- 23-08-2018
- 2018-08-23
- 2018 年 6 月 23 日上午 11:03:33
这些格式由格式字符串控制,例如:
dd MMM yyyy
dd-MM-yyyy
yyyy-MM-DD
dd MMM yyyy hh:mm:ss
在这个配方中,我们将查看java.time.format.DateTimeFormatter
在新的日期和时间 API 中格式化日期和时间实例,并查看最常用的模式字母。
准备
您将需要一个具有新的日期/时间 API 以及jshell
工具的 JDK。
怎么做…
- 让我们使用内置格式设置日期和时间的格式:
jshell> LocalDate ld = LocalDate.now() ld ==> 2018-08-01 jshell> ld.format(DateTimeFormatter.ISO_DATE) $47 ==> "2018-08-01" jshell> LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME) $49 ==> "2018-08-01T17:24:49.1985601"
- 让我们创建一个自定义日期/时间格式:
jshell> DateTimeFormatter dtf = DateTimeFormatter.ofPattern("dd MMM yyyy hh:mm:ss a") dtf ==> Value(DayOfMonth,2)' 'Text(MonthOfYear,SHORT)' 'V ... 2)' 'Text(AmPmOfDay,SHORT)
- 让我们使用自定义
java.time.format.DateTimeFormatter
设置当前日期/时间的格式:jshell> LocalDateTime ldt = LocalDateTime.now() ldt ==> 2018-08-01T17:36:22.442159 jshell> ldt.format(dtf) $56 ==> "01 Aug 2018 05:36:22 PM"
它是如何工作的…
让我们了解最常用的格式字母:
| 符号 | 含义 | 示例 |
| | — | — |
| d
| 月中的日期 | 1, 2, 3, 5
|
| M
、MMM
、MMMM
| 年中的月份 | M
:1, 2, 3
,MMM
:Jun, Jul, Aug
,MMMM
:July, August
|
| y
、yy
| 年 | y
、yyyy
:2017, 2018
,yy
:18, 19
|
| h
| 一天中的小时数(1-12) | 1, 2, 3
|
| k
| 一天中的小时数(0-23) | 0, 1, 2, 3
|
| m
| 分 | 1, 2, 3
|
| s
| 秒 | 1, 2, 3
|
| a
| 每日上午/下午 | AM, PM
|
| VV
| 时区 ID | Asia/Kolkata
|
| ZZ
| 时区名称 | IST, PST, AST
|
| O
| 时区偏移 | GMT+5:30, GMT+3
|
根据前面的格式字母,我们来格式化java.time.ZonedDateTime
:
jshell> DateTimeFormatter dtf = DateTimeFormatter.ofPattern("dd MMMM yy h:mm:ss a VV")
dtf ==> Value(DayOfMonth,2)' 'Text(MonthOfYear)' 'Reduced ... mPmOfDay,SHORT)' 'ZoneId()
jshell> ZonedDateTime.now().format(dtf)
$67 ==> "01 August 18 6:26:04 PM Asia/Kolkata"
jshell> DateTimeFormatter dtf = DateTimeFormatter.ofPattern("dd MMMM yy h:mm:ss a zz")
dtf ==> Value(DayOfMonth,2)' 'Text(MonthOfYear)' 'Reduced ... y,SHORT)' 'ZoneText(SHORT)
jshell> ZonedDateTime.now().format(dtf)
$69 ==> "01 August 18 6:26:13 PM IST"
jshell> DateTimeFormatter dtf = DateTimeFormatter.ofPattern("dd MMMM yy h:mm:ss a O")
dtf ==> Value(DayOfMonth,2)' 'Text(MonthOfYear)' 'Reduced ... )' 'LocalizedOffset(SHORT)
jshell> ZonedDateTime.now().format(dtf)
$72 ==> "01 August 18 6:26:27 PM GMT+5:30"
java.time.format.DateTimeFormatter
附带了大量基于 ISO 标准的默认格式。在处理日期操作时,如果不涉及任何用户,即在应用程序的不同层之间交换日期和时间,这些格式应该足够了。
但是,为了向最终用户显示日期和时间信息,我们需要将其格式化为可读的格式,为此,我们需要一个自定义的DateTimeFormatter
。如果您需要自定义java.time.format.DateTimeFormatter
,这里有两种创建方式:
- 使用模式,例如
dd-MMMM-yyyy
和java.time.format.DateTimeFormatter
中的ofPattern()
方法 - 使用
java.time.DateTimeFormatterBuilder
使用模式:
我们创建了一个java.time.format.DateTimeFormatter
实例,如下所示:
jshell> DateTimeFormatter dtf = DateTimeFormatter.ofPattern("dd MMMM yy h:mm:ss a VV")
dtf ==> Value(DayOfMonth,2)' 'Text(MonthOfYear)' 'Reduced ... mPmOfDay,SHORT)' 'ZoneId()
然后我们将该格式应用于日期和时间实例:
jshell> ZonedDateTime.now().format(dtf)
$92 ==> "01 August 18 7:25:00 PM Asia/Kolkata"
模式方法还使用DateTimeFormatterBuilder
,其中生成器解析给定的格式字符串以构建DateTimeFormatter
对象。
使用java.time.format.DateTimeFormatterBuilder
:
让我们使用DateTimeFormatterBuilder
构建DateTimeFormatter
,如下所示:
jshell> DateTimeFormatter dtf = new DateTimeFormatterBuilder().
...> appendValue(DAY_OF_MONTH, 2).
...> appendLiteral(" ").
...> appendText(MONTH_OF_YEAR).
...> appendLiteral(" ").
...> appendValue(YEAR, 4).
...> toFormatter()
dtf ==> Value(DayOfMonth,2)' 'Text(MonthOfYear)' 'Value(Year,4)
jshell> LocalDate.now().format(dtf) E$106 ==> "01 August 2018"
您可以观察到DateTimeFormatter
对象由一组关于如何表示日期和时间的指令组成。这些指令以Value()
、Text()
和分隔符的形式呈现。
文章列表
- Java11开发秘籍-一、安装和对 Java11 的窥探
- Java11开发秘籍-七、并发和多线程编程
- Java11开发秘籍-三、模块化编程
- Java11开发秘籍-九、使用 springboot 的 restfulWeb 服务
- Java11开发秘籍-二、面向对象编程的快速通道-类和接口
- Java11开发秘籍-五、流和管道
- Java11开发秘籍-八、更好地管理操作系统进程
- Java11开发秘籍-六、数据库编程
- Java11开发秘籍-十、网络
- Java11开发秘籍-十一、内存管理和调试
- Java11开发秘籍-十三、使用新的日期和时间 API
- Java11开发秘籍-十二、使用 JShell 的读取求值打印循环(REPL)
- Java11开发秘籍-十五、Java10 和 Java11 的编程新方法
- Java11开发秘籍-十六、将 JavaFX 用于 GUI 编程
- Java11开发秘籍-十四、测试
- Java11开发秘籍-四、走向函数式
- Java11开发秘籍-零、序言