添加 java/date.md
Signed-off-by: chenkuangwei <git@x47.cn>
This commit is contained in:
parent
ad98620120
commit
ab6d79c94c
340
java/date.md
Normal file
340
java/date.md
Normal 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。
|
Loading…
x
Reference in New Issue
Block a user