# Java 中的日期和时间处理类:从传统到现代 2024-11-07 ## 1、概览 处理 `Date`(日期)和 `Time`(时间)是许多 Java 应用程序的基本组成部分。多年来,Java 在处理日期方面不断发展,引入了更好的解决方案来简化开发者的工作。 ## 2、传统的日期和时间处理类 在 `java.time` 包出现之前,Java 主要使用 `Date` 和 `Calendar` 类来处理日期。尽管它们现在也可以使用,但是有一些缺陷。 ### 2.1、java.util.Date 类 `java.util.Date` 类是 Java 最初处理日期的解决方案,但它有一些缺点: - 它是可变的,这意味着可能会遇到 **线程安全** 问题。 - 不支持时区。 - 它使用了令人困惑的方法名称和返回值,比如 `getYear()`,它返回的是自 *1900* 年以来的年数。 - 许多方法已废弃。 使用无参数构造函数创建 `Date` 对象,表示当前日期和时间(对象创建时)。 如下,实例化一个 `Date` 对象并打印其值: ```java Date now = new Date(); logger.info("Current date and time: {}", now); ``` 这将输出当前日期和时间,如 *Wed Sep 24 10:30:45 PDT 2024*。虽然该构造函数仍然有效,但由于上述原因,这里不再建议新项目使用该构造函数。 ### 2.2、java.util.Calendar 类 由于 `Date` 的局限性,Java 引入了 `Calendar` 类,对其进行了改进: - 支持各种日历系统。 - 时区管理。 - 更加直观的日期操作方法。 我们可以使用 `Calendar` 操作日期。 ```java Calendar cal = Calendar.getInstance(); cal.add(Calendar.DAY_OF_MONTH, 5); Date fiveDaysLater = cal.getTime(); ``` 如上,我们计算当前日期 5 天后的日期,并将其存储在 `Date` 对象中。 但是,`Calendar` 也有缺陷: - 和 `Date` 一样,它仍然是可变的,所以不是线程安全的。 - 其 API 既混乱又复杂,比如月份是从 *0* 开始的。 ## 3、现代的日期和时间处理类:java.time 包 Java 8 引入了 `java.time` 包,为处理日期和时间提供了一个现代、强大的 API。它旨在解决旧版 `Date` 和 `Calendar` 类的许多问题,使日期和时间操作更加直观和友好。 受到流行的 [Joda-Time](https://www.joda.org/joda-time/) 库的启发,`java.time` 现在已成为处理日期和时间的核心 Java 解决方案。 ### 3.1、java.time 包下的关键类 `java.time` 包提供了几个在实际应用中经常使用的重要类。这些类可分为三大类: #### 时间容器 - `LocalDate`:代表日期,不包含时间或时区。 - `LocalTime`:代表时间,不包含日期或时区。 - `LocalDateTime`:包括了日期和时间,但不包括时区。 - `ZonedDateTime`:包括了日期和时间以及时区。 - `Instant`:代表时间轴上的一个特定点,类似于时间戳。 #### 时间操作 - `Duration`:表示基于时间的时间量(例如 “5 小时” 或 “30 秒”)。 - `Period`:代表基于日期的时间量(如 “2 年 3 个月”)。 - `TemporalAdjusters`:提供调整日期的方法(如查找下一个星期一)。 - `Clock`:使用时区提供当前日期时间,并可进行时间控制。 #### 格式化和输出 - `DateTimeFormatter`:用于格式化和解析日期时间对象。 ### 3.2、java.time 包的优点 与旧的日期和时间类相比,`java.time` 包带来了多项改进: - **不可变**:所有类都不可变,确保线程安全。 - **清晰的 API**:方法一致,使 API 更容易理解。 - **专注的类**:每个类都有特定的作用,无论是处理日期存储、操作还是格式化。 - **格式化和解析**:内置方法可轻松格式化和解析日期。 ## 4、java.time 的使用示例 首先从使用 `java.time` 包创建日期和时间表示的基础知识开始。有了基础后,再了解如何调整日期以及如何格式化和解析日期。 ### 4.1、创建日期表示 `java.time` 包提供了多个类来表示日期和时间的不同方面。 代码如下,使用 `LocalDate`、`LocalTime` 和 `LocalDateTime` 创建一个基本日期: ```java @Test void givenCurrentDateTime_whenUsingLocalDateTime_thenCorrect() { LocalDate currentDate = LocalDate.now(); // 当前日期 LocalTime currentTime = LocalTime.now(); // 当前时间 LocalDateTime currentDateTime = LocalDateTime.now(); // 当前日期和时间 assertThat(currentDate).isBeforeOrEqualTo(LocalDate.now()); assertThat(currentTime).isBeforeOrEqualTo(LocalTime.now()); assertThat(currentDateTime).isBeforeOrEqualTo(LocalDateTime.now()); } ``` 还可以通过传递所需的参数来创建特定的日期和时间: ```java @Test void givenSpecificDateTime_whenUsingLocalDateTime_thenCorrect() { LocalDate date = LocalDate.of(2024, Month.SEPTEMBER, 18); LocalTime time = LocalTime.of(10, 30); LocalDateTime dateTime = LocalDateTime.of(date, time); assertEquals("2024-09-18", date.toString()); assertEquals("10:30", time.toString()); assertEquals("2024-09-18T10:30", dateTime.toString()); } ``` ### 4.2、用 TemporalAdjusters 调整日期 有了日期表示后,我们就可以使用 `TemporalAdjusters` 对其进行调整。 `TemporalAdjusters` 类提供了一组预定义的方法来操作日期: ```java @Test void givenTodaysDate_whenUsingVariousTemporalAdjusters_thenReturnCorrectAdjustedDates() { LocalDate today = LocalDate.now(); LocalDate nextMonday = today.with(TemporalAdjusters.next(DayOfWeek.MONDAY)); // 调整日期为下周一 assertThat(nextMonday.getDayOfWeek()) .as("Next Monday should be correctly identified") .isEqualTo(DayOfWeek.MONDAY); LocalDate firstDayOfMonth = today.with(TemporalAdjusters.firstDayOfMonth()); // 整日期为月初第一天 assertThat(firstDayOfMonth.getDayOfMonth()) .as("First day of the month should be 1") .isEqualTo(1); } ``` 除了预定义的 *Adjuster*(调整器)外,我们还可以根据特定需求创建自定义 *Adjuster*: ```java @Test void givenCustomTemporalAdjuster_whenAddingTenDays_thenCorrect() { LocalDate specificDate = LocalDate.of(2024, Month.SEPTEMBER, 18); TemporalAdjuster addTenDays = temporal -> temporal.plus(10, ChronoUnit.DAYS); LocalDate adjustedDate = specificDate.with(addTenDays); assertEquals( today.plusDays(10), adjustedDate, "The adjusted date should be 10 days later than September 18, 2024" ); } ``` ### 4.3、格式化日期 `java.time.format` 包中的 `DateTimeFormatter` 类允许我们以线程安全的方式格式化和解析日期时间对象: ```java @Test void givenDateTimeFormat_whenFormatting_thenVerifyResults() { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm"); LocalDateTime specificDateTime = LocalDateTime.of(2024, 9, 18, 10, 30); String formattedDate = specificDateTime.format(formatter); LocalDateTime parsedDateTime = LocalDateTime.parse("18-09-2024 10:30", formatter); assertThat(formattedDate).isNotEmpty().isEqualTo("18-09-2024 10:30"); } ``` 我们可以根据需要使用预定义的格式或自定义的格式。 ### 4.4、解析日期 同样,`DateTimeFormatter` 可以将字符串解析为日期或时间对象: ```java @Test void givenDateTimeFormat_whenParsing_thenVerifyResults() { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm"); LocalDateTime parsedDateTime = LocalDateTime.parse("18-09-2024 10:30", formatter); assertThat(parsedDateTime) .isNotNull() .satisfies(time -> { assertThat(time.getYear()).isEqualTo(2024); assertThat(time.getMonth()).isEqualTo(Month.SEPTEMBER); assertThat(time.getDayOfMonth()).isEqualTo(18); assertThat(time.getHour()).isEqualTo(10); assertThat(time.getMinute()).isEqualTo(30); }); } ``` ### 4.5、通过 OffsetDateTime 和 OffsetTime 处理时区 在处理不同时区时,`OffsetDateTime` 和 `OffsetTime` 类对于处理日期和时间或与 UTC 的偏移量非常有用: ```java @Test void givenVariousTimeZones_whenCreatingOffsetDateTime_thenVerifyOffsets() { // 巴黎时区 ZoneId parisZone = ZoneId.of("Europe/Paris"); // 纽约时区 ZoneId nyZone = ZoneId.of("America/New_York"); OffsetDateTime parisTime = OffsetDateTime.now(parisZone); OffsetDateTime nyTime = OffsetDateTime.now(nyZone); assertThat(parisTime) .isNotNull() .satisfies(time -> { assertThat(time.getOffset().getTotalSeconds()) .isEqualTo(parisZone.getRules().getOffset(Instant.now()).getTotalSeconds()); }); // 验证不同地区之间的时差 assertThat(ChronoUnit.HOURS.between(nyTime, parisTime) % 24) .isGreaterThanOrEqualTo(5) // 纽约一般比巴黎晚 5-6 个小时 .isLessThanOrEqualTo(7); } ``` 代码如上,演示了如何为不同时区创建 `OffsetDateTime` 实例并验证其偏移量。首先,使用 `ZoneId` 定义巴黎和纽约的时区。然后,使用 `OffsetDateTime.now()` 创建这两个时区的当前时间。 该测试检查巴黎时间的偏移量是否与巴黎时区的预期偏移量相匹配。最后,验证纽约和巴黎之间的时间差,确保它在典型的 *5* 到 *7* 小时范围内,反映了标准时区差异。 ### 4.6、高级用例:Clock `java.time` 软件包中的 `Clock` 类提供了一种灵活的方式来访问当前日期和时间,同时考虑到特定的时区。 在我们需要对时间进行更多控制或测试基于时间的逻辑时,该类非常有用。 与使用 `LocalDateTime.now()` 获取系统当前时间不同,`Clock` 允许我们获取相对于特定时区的时间,甚至为测试目的模拟时间。通过向 `Clock.system()` 方法传递 `ZoneId`,我们可以获得任何地区的当前时间。例如,在下面的测试用例中,我们使用 `Clock` 类获取 `America/New_York`(美国/纽约)时区的当前时间: ```java @Test void givenSystemClock_whenComparingDifferentTimeZones_thenVerifyRelationships() { Clock nyClock = Clock.system(ZoneId.of("America/New_York")); LocalDateTime nyTime = LocalDateTime.now(nyClock); assertThat(nyTime) .isNotNull() .satisfies(time -> { assertThat(time.getHour()).isBetween(0, 23); assertThat(time.getMinute()).isBetween(0, 59); // 验证是否在最后一分钟内(最近) assertThat(time).isCloseTo( LocalDateTime.now(), within(1, ChronoUnit.MINUTES) ); }); } ``` 这也使得 `Clock` 对于管理多个时区或控制时间流一致的应用非常有用。 ## 5、从传统类到现代类的迁移 我们可能仍然需要处理使用 `Date` 或 `Calendar` 的遗留代码或库。幸运的是,我们可以轻松地从旧的日期时间类迁移到新的日期时间类。 ### 5.1、转换 Date 为 Instant 使用 `toInstant()` 方法可以轻松地将传统的 `Date` 类转换为 `Instant`。这对我们迁移到 `java.time` 包中的类很有帮助,因为 `Instant` 表示时间轴上的一个点(纪元): ```java @Test void givenSameEpochMillis_whenConvertingDateAndInstant_thenCorrect() { long epochMillis = System.currentTimeMillis(); Date legacyDate = new Date(epochMillis); Instant instant = Instant.ofEpochMilli(epochMillis); assertEquals( legacyDate.toInstant(), instant, "Date and Instant should represent the same moment in time" ); } ``` 我们可以将传统的 `Date` 转换为 `Instant`,并通过从相同的毫秒纪元创建两者来确保它们代表相同的时间点。 ### 5.2、迁移 Calendar 到 ZonedDateTime 在使用 `Calendar` 时,我们可以迁移到更现代的 `ZonedDateTime`,它可以同时处理日期和时间以及时区信息: ```java @Test void givenCalendar_whenConvertingToZonedDateTime_thenCorrect() { Calendar calendar = Calendar.getInstance(); calendar.set(2024, Calendar.SEPTEMBER, 18, 10, 30); ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant( calendar.toInstant(), calendar.getTimeZone().toZoneId() ); assertEquals(LocalDate.of(2024, 9, 18), zonedDateTime.toLocalDate()); assertEquals(LocalTime.of(10, 30), zonedDateTime.toLocalTime()); } ``` 如上,我们将 `Calendar` 实例转换为 `ZonedDateTime`,并验证它们是否代表相同的日期时间。 ## 6、最佳实践 有一些使用 `java.time` 类的最佳实践,你可以参考: 1. 任何新项目都应使用 `java.time` 类。 2. 当不需要时区时,可以使用 `LocalDate`、`LocalTime` 或 `LocalDateTime`。 3. 处理时区或时间戳时,请使用 `ZonedDateTime` 或 `Instant` 代替。 4. 使用 `DateTimeFormatter` 来解析和格式化日期。 5. 为避免混淆,应始终明确指定时区。 这些最佳实践为在 Java 中处理日期和时间奠定了坚实的基础,确保我们可以在应用程序中高效、准确地处理它们。 ## 7、总结 Java 8 中引入的 `java.time` 包极大地改进了我们处理日期和时间的方式。此外,采用该 API 还能确保代码更简洁、更易于维护。 对于旧项目中遗留的 `Date` 或 `Calendar`,我们也可以轻松地迁移到新的 `java.time` API。