添加 java/date.md

Signed-off-by: chenkuangwei <git@x47.cn>
This commit is contained in:
chenkuangwei 2024-12-30 15:51:03 +08:00
parent ad98620120
commit ab6d79c94c

340
java/date.md Normal file
View File

@ -0,0 +1,340 @@
# 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。