article/java/date.md
chenkuangwei ab6d79c94c 添加 java/date.md
Signed-off-by: chenkuangwei <git@x47.cn>
2024-12-30 15:51:03 +08:00

340 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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。