Contents

Java 8 日期/时间API简介

1. 概述

Java 8 为DateTime引入了新的 API,以解决旧java.util.Datejava.util.Calendar的缺点。 在本教程中,让我们从现有的日期日历API 中的问题开始,并讨论新的 Java 8DateTime API 如何解决这些问题。 我们还将查看新 Java 8 项目的一些核心类,它们是java.time包的一部分,例如LocalDateLocalTimeLocalDateTimeZonedDateTimePeriodDuration及其支持的 API。

2. 现有Date/TimeAPI的问题

  • 线程安全——DateCalendar类不是线程安全的,这让开发人员不得不处理难以调试的并发问题,并编写额外的代码来处理线程安全。相反,Java 8 中引入的新DateTimeAPI 是不可变的和线程安全的,从而使开发人员摆脱了并发问题。
  • API 设计和易于理解——DateCalendarAPI 设计不佳,执行日常操作的方法不足。新的Date/TimeAPI 以 ISO 为中心,并遵循日期、时间、持续时间和期间的一致域模型。有各种各样的实用程序方法支持最常见的操作。
  • ZonedDateTime** – 开发人员必须编写额外的逻辑来使用旧 API 处理时区逻辑,而使用新 API,可以使用LocalZonedDate / Time API 来处理时区。

3. 使用LocalDateLocalTimeLocalDateTime

最常用的类是LocalDateLocalTimeLocalDateTime。正如它们的名字所表明的,它们代表了观察者上下文中的本地日期/时间。 我们主要在不需要在上下文中明确指定时区时使用这些类。作为本节的一部分,我们将介绍最常用的 API。

3.1. 使用LocalDate

LocalDate表示不带时间的 ISO 格式 (yyyy-MM-dd) 的日期。我们可以用它来存储生日和发薪日等日期。 可以从系统时钟创建当前日期的实例:

LocalDate localDate = LocalDate.now();

我们可以通过of方法或parse方法获取表示特定日、月和年的LocalDate。 例如,这些代码片段表示2015 年 2 月 20 日的LocalDate

LocalDate.of(2015, 02, 20);
LocalDate.parse("2015-02-20");

LocalDate提供了各种实用方法来获取各种信息。让我们快速浏览一下其中的一些 API 方法。

以下代码片段获取当前本地日期并添加一天:

LocalDate tomorrow = LocalDate.now().plusDays(1);

此示例获取当前日期并减去一个月。请注意它如何接受Enum作为时间单位:

LocalDate previousMonthSameDay = LocalDate.now().minus(1, ChronoUnit.MONTHS);

在以下两个代码示例中,我们解析日期“2016-06-12”并分别获取星期几和月份中的日期。注意返回值——第一个是表示DayOfWeek的对象,而第二个是表示月份序数值的int

DayOfWeek sunday = LocalDate.parse("2016-06-12").getDayOfWeek();
int twelve = LocalDate.parse("2016-06-12").getDayOfMonth();

我们可以测试某个日期是否出现在闰年,例如当前日期:

boolean leapYear = LocalDate.now().isLeapYear();

此外,可以确定一个日期与另一个日期的关系发生在另一个日期之前或之后:

boolean notBefore = LocalDate.parse("2016-06-12")
  .isBefore(LocalDate.parse("2016-06-11"));
boolean isAfter = LocalDate.parse("2016-06-12")
  .isAfter(LocalDate.parse("2016-06-11"));

最后,可以从给定日期获得日期边界。 在以下两个示例中,我们分别获得表示给定日期的开始日期 (2016-06-12T00:00) 的LocalDateTime和表示月份开始的 LocalDate (2016-06-01)

LocalDateTime beginningOfDay = LocalDate.parse("2016-06-12").atStartOfDay();
LocalDate firstDayOfMonth = LocalDate.parse("2016-06-12")
  .with(TemporalAdjusters.firstDayOfMonth());

现在让我们看看我们如何处理当地时间。

3.2. 使用LocalTime

LocalTime表示没有日期的时间。 与LocalDate类似,我们可以从系统时钟或使用parseof方法创建LocalTime的实例。 我们现在将快速浏览一些常用的 API。 可以从系统时钟创建当前LocalTime的实例:

LocalTime now = LocalTime.now();

我们可以通过解析字符串表示来创建表示早上 6:30的LocalTime

LocalTime sixThirty = LocalTime.parse("06:30");

of工厂方法也可用于创建LocalTime。此代码使用工厂方法创建表示上午 6:30 的LocalTime

LocalTime sixThirty = LocalTime.of(6, 30);

让我们通过解析一个字符串并使用“加号”API 为其添加一个小时来创建一个LocalTime。结果将是表示上午 7:30的LocalTime

LocalTime sevenThirty = LocalTime.parse("06:30").plus(1, ChronoUnit.HOURS);

可以使用各种 getter 方法来获取特定的时间单位,例如小时、分钟和秒:

int six = LocalTime.parse("06:30").getHour();

我们还可以检查特定时间是在另一个特定时间之前还是之后。此代码示例比较结果为真的两个LocalTime

boolean isbefore = LocalTime.parse("06:30").isBefore(LocalTime.parse("07:30"));

最后,可以通过LocalTime类中的常量获得一天的最大、最小和中午时间。这在执行数据库查询以查找给定时间跨度内的记录时非常有用。 例如,下面的代码代表 23:59:59.99:

LocalTime maxTime = LocalTime.MAX

现在让我们深入了解LocalDateTime

3.3. 使用LocalDateTime

LocalDateTime用于表示**日期和时间的组合。**当我们需要日期和时间的组合时,这是最常用的类。

该类提供了多种 API。在这里,我们将看看一些最常用的。 可以从类似于LocalDateLocalTime的系统时钟中获取LocalDateTime的实例:

LocalDateTime.now();

下面的代码示例解释了如何使用工厂“of”和“parse”方法创建实例。结果将是表示 2015 年 2 月 20 日上午 6:30的LocalDateTime实例:

LocalDateTime.of(2015, Month.FEBRUARY, 20, 06, 30);
LocalDateTime.parse("2015-02-20T06:30:00");

有一些实用 API 可以支持特定时间单位的加法和减法,例如天、月、年和分钟。 下面的代码演示了“加号”和“减号”方法。这些 API 的行为与LocalDateLocalTime中的对应物完全相同:

localDateTime.plusDays(1);
localDateTime.minusHours(2);

Getter 方法也可用于提取类似于日期和时间类的特定单位。鉴于上述LocalDateTime实例,此代码示例将返回二月:

localDateTime.getMonth();

4. 使用ZonedDateTime API

当我们需要处理特定于时区的日期和时间时,Java 8 提供了 ZonedDateTimeZoneId是用于表示不同区域的标识符。 大约有 40 个不同的时区,ZoneId 表示它们如下。 在这里,我们为巴黎创建一个Zone

ZoneId zoneId = ZoneId.of("Europe/Paris");

我们可以得到一组所有的区域 id:

Set<String> allZoneIds = ZoneId.getAvailableZoneIds();

LocalDateTime可以转换为特定区域:

ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, zoneId);

ZonedDateTime提供parse方法来获取特定于时区的日期时间:

ZonedDateTime.parse("2015-05-03T10:15:30+01:00[Europe/Paris]");

使用时区的另一种方法是使用OffsetDateTimeOffsetDateTime是具有偏移量的日期时间的不可变表示。此类存储所有日期和时间字段,精度为纳秒,以及与 UTC/格林威治的偏移量。 OffSetDateTime实例可以使用ZoneOffset创建。在这里,我们创建一个表示 2015 年 2 月 20 日上午 6:30的LocalDateTime

LocalDateTime localDateTime = LocalDateTime.of(2015, Month.FEBRUARY, 20, 06, 30);

然后我们通过创建一个ZoneOffset并为localDateTime实例设置添加两个小时:

ZoneOffset offset = ZoneOffset.of("+02:00");
OffsetDateTime offSetByTwo = OffsetDateTime
  .of(localDateTime, offset);

我们现在的localDateTime为 2015-02-20 06:30 +02:00。 现在让我们继续讨论如何使用PeriodDuration类修改日期和时间值。

5. 使用PeriodDuration

Period类表示以年、月和日表示的时间量,而Duration类表示以秒和纳秒为单位的时间量。

5.1. Period操作

Period类广泛用于修改给定日期的值或获取两个日期之间的差异:

LocalDate initialDate = LocalDate.parse("2007-05-10");

我们可以使用Period来操作日期

LocalDate finalDate = initialDate.plus(Period.ofDays(5));

Period类有各种 getter 方法,例如getYearsgetMonthsgetDays来从Period对象中获取值。 例如,当我们尝试获取天数的差异时,这将返回一个int值:

int five = Period.between(initialDate, finalDate).getDays();

我们可以使用ChronoUnit.between以特定单位(例如天、月或年)获取两个日期之间的Period

long five = ChronoUnit.DAYS.between(initialDate, finalDate);

此代码示例返回五天。 让我们继续看一下Duration类。

5.2. 使用Duration

Period 类似,Duration类用于处理Time。 让我们创建一个上午 6:30 的 LocalTime,然后添加 30 秒的持续时间以使LocalTime上午6:30:30:

LocalTime initialTime = LocalTime.of(6, 30, 0);
LocalTime finalTime = initialTime.plus(Duration.ofSeconds(30));

我们可以将两个瞬间之间的Duration时间作为Duration或特定单位。 首先,我们使用Duration类的between()方法找到finalTimeinitialTime之间的时间差,并返回以秒为单位的差:

long thirty = Duration.between(initialTime, finalTime).getSeconds();

在第二个示例中,我们使用ChronoUnit类的*between()*方法来执行相同的操作:

long thirty = ChronoUnit.SECONDS.between(initialTime, finalTime);

现在我们将看看如何将现有的DateCalendar转换为新的Date / Time

6. 兼容DateCalendar

Java 8 添加了toInstant()方法,该方法有助于将现有的DateCalendar实例转换为新的 Date 和 Time API:

LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
LocalDateTime.ofInstant(calendar.toInstant(), ZoneId.systemDefault());

LocalDateTime可以从纪元秒构建。以下代码的结果将是表示 2016-06-13T11:34:50 的LocalDateTime

LocalDateTime.ofEpochSecond(1465817690, 0, ZoneOffset.UTC);

现在让我们继续DateTime格式。

7. DateTime格式

Java 8 提供了用于轻松格式化DateTime的 API :

LocalDateTime localDateTime = LocalDateTime.of(2015, Month.JANUARY, 25, 6, 30);

此代码传递 ISO 日期格式来格式化本地日期,结果为 2015-01-25:

String localDateString = localDateTime.format(DateTimeFormatter.ISO_DATE);

DateTimeFormatter提供各种标准格式选项。 自定义模式也可以提供给 format 方法,这里返回LocalDate为 2015/01/25:

localDateTime.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));

我们可以将格式样式作为SHORTLONGMEDIUM作为格式选项的一部分传递。 例如,这将给出表示LocalDateTime in 25-Jan-2015, 06:30:00 的输出:

localDateTime
  .format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
  .withLocale(Locale.UK));

让我们看看 Java 8 核心Date/TimeAPI 的可用替代方案。

8. 反向移植和替代选项

8.1. 使用 ThreeTen 项目

对于正在从 Java 7 或 Java 6 迁移到 Java 8 并希望使用日期和时间 API 的组织,ThreeTen 项目提供了向后移植功能。 开发人员可以使用此项目中可用的类来实现与新 Java 8DateTimeAPI相同的功能。一旦他们迁移到 Java 8,就可以切换包。 ThreeTen 项目的工件可以在Maven 中央存储库 中找到:

<dependency>
    <groupId>org.threeten</groupId>
    <artifactId>threetenbp</artifactId>
    <version>1.3.1</version>
</dependency>

8.2. Joda-Time库

Java 8DateTime库的另一个替代方案是Joda-Time 库。事实上,Java 8Date/TimeAPI 已经由 Joda-Time 库的作者(Stephen Colebourne)和 Oracle 共同领导。这个库提供了 Java 8Date/Time项目支持的几乎所有功能。 通过在我们的项目中包含以下 pom 依赖项,可以在Maven Central 中找到该工件:

<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
    <version>2.9.4</version>
</dependency>