Java 中的日期和时间处理类:从传统到现代
1、概览
处理 Date
(日期)和 Time
(时间)是许多 Java 应用程序的基本组成部分。多年来,Java 在处理日期方面不断发展,引入了更好的解决方案来简化开发者的工作。
2、传统的日期和时间处理类
在 java.time
包出现之前,Java 主要使用 Date
和 Calendar
类来处理日期。尽管它们现在也可以使用,但是有一些缺陷。
2.1、java.util.Date 类
java.util.Date
类是 Java 最初处理日期的解决方案,但它有一些缺点:
- 它是可变的,这意味着可能会遇到 线程安全 问题。
- 不支持时区。
- 它使用了令人困惑的方法名称和返回值,比如
getYear()
,它返回的是自 1900 年以来的年数。 - 许多方法已废弃。
使用无参数构造函数创建 Date
对象,表示当前日期和时间(对象创建时)。
如下,实例化一个 Date
对象并打印其值:
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
操作日期。
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 库的启发,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
创建一个基本日期:
@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());
}
还可以通过传递所需的参数来创建特定的日期和时间:
@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
类提供了一组预定义的方法来操作日期:
@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:
@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
类允许我们以线程安全的方式格式化和解析日期时间对象:
@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
可以将字符串解析为日期或时间对象:
@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 的偏移量非常有用:
@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
(美国/纽约)时区的当前时间:
@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
表示时间轴上的一个点(纪元):
@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
,它可以同时处理日期和时间以及时区信息:
@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
类的最佳实践,你可以参考:
- 任何新项目都应使用
java.time
类。 - 当不需要时区时,可以使用
LocalDate
、LocalTime
或LocalDateTime
。 - 处理时区或时间戳时,请使用
ZonedDateTime
或Instant
代替。 - 使用
DateTimeFormatter
来解析和格式化日期。 - 为避免混淆,应始终明确指定时区。
这些最佳实践为在 Java 中处理日期和时间奠定了坚实的基础,确保我们可以在应用程序中高效、准确地处理它们。
7、总结
Java 8 中引入的 java.time
包极大地改进了我们处理日期和时间的方式。此外,采用该 API 还能确保代码更简洁、更易于维护。
对于旧项目中遗留的 Date
或 Calendar
,我们也可以轻松地迁移到新的 java.time
API。
Ref:https://www.baeldung.com/java-date-time-history