Java 日期/时间 API 简介

更新于 2025-12-29

baeldung 2023-10-13

1. 概述

Java 8 引入了全新的日期和时间 API,以解决旧版 java.util.Datejava.util.Calendar 存在的问题。

在本教程中,我们将首先探讨现有 Date 和 Calendar API 中存在的问题,并讨论 Java 8 新的日期/时间 API 是如何解决这些问题的。

我们还将介绍 Java 8 中属于 java.time 包的一些核心类,例如 LocalDateLocalTimeLocalDateTimeZonedDateTimePeriodDuration 以及它们所支持的 API。


2. 现有日期/时间 API 的问题

  • 线程安全性DateCalendar 类不是线程安全的,这使得开发者不得不处理难以调试的并发问题,并编写额外代码来保证线程安全。相比之下,Java 8 引入的新日期/时间 API 是不可变且线程安全的,从而消除了这一并发难题。

  • API 设计与易用性:旧的 Date 和 Calendar API 设计不佳,缺乏执行日常操作所需的足够方法。而新的日期/时间 API 以 ISO 标准为中心,采用一致的领域模型来表示日期、时间、持续时间和周期,并提供了大量实用方法来支持最常见的操作。

  • 时区处理:使用旧 API 时,开发者需要编写额外逻辑来处理时区;而新 API 则通过 LocalDateTimeZonedDateTime 等类直接内置了对时区的支持。


3. 使用 LocalDateLocalTimeLocalDateTime

最常用的类是 LocalDateLocalTimeLocalDateTime。顾名思义,它们从观察者的上下文中表示本地日期/时间。

当上下文中不需要显式指定时区时,我们通常会使用这些类。本节将介绍其中一些最常用的 API。

3.1 使用 LocalDate

LocalDate 表示 ISO 格式(yyyy-MM-dd)的日期,不含时间部分。可用于存储生日、发薪日等日期信息。

可通过系统时钟创建当前日期的实例:

LocalDate localDate = LocalDate.now();

也可以使用 of() 方法或 parse() 方法创建特定年月日的 LocalDate 实例。

例如,以下代码片段都表示 2015 年 2 月 20 日:

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

LocalDate 提供了多种实用方法来获取各种信息。下面快速浏览一些常用 API:

  • 获取当前日期并加一天:

    LocalDate tomorrow = LocalDate.now().plusDays(1);
    
  • 获取当前日期并减去一个月(注意这里使用了枚举作为时间单位):

    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 beginningOfDay = LocalDate.parse("2016-06-12").atStartOfDay();
      
    • 获取当月的第一天(2016-06-01):

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

接下来,我们看看如何处理本地时间。

3.2 使用 LocalTime

LocalTime 表示不带日期的时间。

LocalDate 类似,我们可以从系统时钟创建 LocalTime 实例,也可以使用 parse()of() 方法。

以下是一些常用 API 示例:

  • 从系统时钟创建当前 LocalTime 实例:

    LocalTime now = LocalTime.now();
    
  • 通过解析字符串创建表示早上 6:30 的 LocalTime

    LocalTime sixThirty = LocalTime.parse("06:30");
    
  • 使用工厂方法 of() 创建同样的时间:

    LocalTime sixThirty = LocalTime.of(6, 30);
    
  • 解析字符串后加一小时(结果为早上 7:30):

    LocalTime sevenThirty = LocalTime.parse("06:30").plus(1, ChronoUnit.HOURS);
    
  • 使用 getter 方法获取具体的时间单位(如小时、分钟、秒):

    int six = LocalTime.parse("06:30").getHour();
    
  • 比较两个时间的先后(以下代码返回 true):

    boolean isBefore = LocalTime.parse("06:30").isBefore(LocalTime.parse("07:30"));
    
  • 获取一天中的最大、最小和正午时间(常用于数据库查询):

    LocalTime maxTime = LocalTime.MAX; // 23:59:59.999999999
    

现在我们深入了解一下 LocalDateTime

3.3 使用 LocalDateTime

LocalDateTime 用于表示日期和时间的组合,是我们需要同时处理日期和时间时最常用的类。

该类提供了丰富的 API。以下是一些最常用的示例:

  • 从系统时钟获取当前 LocalDateTime

    LocalDateTime.now();
    
  • 使用 of()parse() 工厂方法创建表示 2015 年 2 月 20 日早上 6:30 的实例:

    LocalDateTime.of(2015, Month.FEBRUARY, 20, 06, 30);
    LocalDateTime.parse("2015-02-20T06:30:00");
    
  • 支持对时间单位(如天、月、年、分钟)进行加减操作:

    localDateTime.plusDays(1);
    localDateTime.minusHours(2);
    
  • 提供 getter 方法提取特定时间单位(如下例返回 Month.FEBRUARY):

    localDateTime.getMonth();
    

4. 使用 ZonedDateTime API

当我们需要处理带时区的日期和时间时,Java 8 提供了 ZonedDateTimeZoneId 是用于表示不同时区的标识符。目前大约有 40 个不同的时区,ZoneId 按如下方式表示它们。

例如,创建巴黎的时区:

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/Greenwich 的偏移量。

可以通过 ZoneOffset 创建 OffsetDateTime 实例。例如:

LocalDateTime localDateTime = LocalDateTime.of(2015, Month.FEBRUARY, 20, 06, 30);
ZoneOffset offset = ZoneOffset.of("+02:00");
OffsetDateTime offSetByTwo = OffsetDateTime.of(localDateTime, offset);

结果为:2015-02-20T06:30+02:00


5. 使用 PeriodDuration

Period 类以年、月、日的形式表示一段时间,而 Duration 类则以秒和纳秒的形式表示一段时间。

5.1 使用 Period

Period 常用于修改给定日期或计算两个日期之间的差值:

LocalDate initialDate = LocalDate.parse("2007-05-10");
LocalDate finalDate = initialDate.plus(Period.ofDays(5));

Period 类提供了 getYears()getMonths()getDays() 等 getter 方法。

例如,以下代码返回 5(天数差):

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

也可以使用 ChronoUnit.between() 按特定单位(如天、月、年)获取两个日期之间的差值:

long five = ChronoUnit.DAYS.between(initialDate, finalDate); // 返回 5

5.2 使用 Duration

Period 类似,Duration 用于处理时间。

例如,创建早上 6:30 的 LocalTime,然后加上 30 秒:

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

可以使用 Duration.between() 获取两个时间点之间的差值(以秒为单位):

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

也可以使用 ChronoUnit.SECONDS.between() 达到相同效果:

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

6. 与 DateCalendar 的兼容性

Java 8 添加了 toInstant() 方法,可将现有的 DateCalendar 实例转换为新的日期/时间 API:

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

还可以通过纪元秒(epoch second)构造 LocalDateTime。以下代码将生成 2016-06-13T11:34:50

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

7. 日期和时间格式化

Java 8 提供了便捷的日期/时间格式化 API:

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

使用 ISO 日期格式进行格式化(结果为 2015-01-25):

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

DateTimeFormatter 提供了多种标准格式选项。

也可以使用自定义模式(如下返回 2015/01/25):

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

还可以传入格式样式(如 SHORTLONGMEDIUM):

localDateTime
  .format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
  .withLocale(Locale.UK));
// 输出示例:25-Jan-2015, 06:30:00

8. Java 8 核心日期/时间 API 的替代方案

8.1 使用 ThreeTen 项目

对于正在从 Java 7 或 Java 6 升级到 Java 8 的组织,若希望提前使用新的日期/时间 API,ThreeTen 项目提供了向后移植(backport)功能。

开发者可以使用该项目中的类实现与 Java 8 日期/时间 API 相同的功能。一旦升级到 Java 8,只需切换包即可。

Maven 依赖如下:

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

8.2 Joda-Time 库

另一个 Java 8 日期/时间库的替代方案是 Joda-Time。事实上,Java 8 的日期/时间 API 正是由 Joda-Time 的作者 Stephen Colebourne 与 Oracle 共同主导开发的。该库几乎提供了 Java 8 日期/时间项目所支持的所有功能。

Maven 依赖如下:

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

9. 结论

Java 8 提供了一套功能丰富、设计一致的日期/时间 API,极大简化了开发工作。