Java 提供了多套时间处理 API,从早期的 Date 和 Calendar 到现代的 java.time 包。本文将系统解析各 API 的核心类、使用场景及最佳实践。
传统时间 API (Java 8 之前)
java.util.Date
- 用途:表示特定的瞬间(毫秒精度)
- 问题:
- 月份从 0 开始(0 = 一月,11 = 十二月)
- 年份从 1900 年开始计算
- 非线程安全,大部分方法已过时
- 示例:
1
2
3
4
5
6
|
// 创建表示当前时间的 Date 对象
Date now = new Date();
System.out.println(now); // 输出: Mon Mar 31 09:33:00 CST 2025
// 创建特定时间的 Date 对象
Date specificDate = new Date(125, 3, 31); // 2025 年 3 月 31 日 (方法已废弃)
|
java.util.Calendar
- 用途:日期计算和字段操作
- 问题:
- API 设计复杂(如 Calendar.MONTH)
- 可变对象,非线程安全
- 月份仍从 0 开始
- 示例:
1
2
3
|
Calendar cal = Calendar.getInstance();
cal.set(2025, Calendar.SEPTEMBER, 20);
cal.add(Calendar.DAY_OF_MONTH, 7); // 增加 7 天
|
现代时间 API (Java 8+ java.time 包)
核心不可变类
| 类名 |
描述 |
示例 |
LocalDate |
日期(无时间) |
LocalDate.of(2025,3,31) |
LocalTime |
时间(无日期) |
LocalTime.parse("09:33:00") |
LocalDateTime |
日期+时间(无时区) |
LocalDateTime.now() |
ZonedDateTime |
带时区的完整日期时间 |
ZonedDateTime.now(ZoneId.of("Asia/Shanghai")) |
Instant |
时间戳(UTC 时间) |
Instant.now() |
时区处理
ZoneId:IANA 时区名(如 Asia/Shanghai)
1
2
|
Set<String> zoneIds = ZoneId.getAvailableZoneIds(); // 所有可用时区
ZoneId tokyoZone = ZoneId.of("Asia/Tokyo");
|
ZoneOffset:固定偏移(如 +08:00)
1
|
ZoneOffset offset = ZoneOffset.ofHours(8); // UTC+8
|
时间间隔与周期
Duration:基于时间的量(秒、纳秒)
1
2
|
Duration duration = Duration.between(startTime, endTime);
Duration towHours = Duration.ofHours(2);
|
Period:基于日期的量(年、月、日)
1
|
Period period = Period.between(LocalDate.now(), LocalDate.now().plusMonths(1));
|
格式化和解析
DateTimeFormatter:线程安全的格式化工具
1
2
3
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted = LocalDateTime.now().format(formatter); // 2025-03-31 09:33:00
LocalDateTime parsed = LocalDateTime.parse("2025-03-31 09:33:00", formatter);
|
时间调整器(TemporalAdjuster)
- 预定义调整:如月末、下个周一
1
2
|
LocalDate nextMonday = LocalDate.now().with(TemporalAdjusters.next(DayOfWeek.MONDAY));
LocalDate lastDayOfMonth = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth());
|
示例:给定一个日期,如果是周一、周二、周三,则调整到当周周三;如果是周四、周五、周六、周日,则调整到当周周日。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjuster;
import java.time.temporal.TemporalAdjusters;
public class DateAdjuster {
// 核心逻辑:自定义 TemporalAdjuster
public static TemporalAdjuster adjustToWednesdayOrSunday() {
return temporal -> {
LocalDate date = LocalDate.from(temporal);
DayOfWeek day = date.getDayOfWeek();
if (day == DayOfWeek.MONDAY || day == DayOfWeek.TUESDAY || day == DayOfWeek.WEDNESDAY) {
// 周一/二/三 → 调整为当周周三
return date.with(TemporalAdjusters.nextOrSame(DayOfWeek.WEDNESDAY));
} else {
// 周四/五/六/日 → 调整为当周周日
return date.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));
}
};
}
// 使用示例
public static void main(String[] args) {
// 测试不同日期
System.out.println(LocalDate.of(2025, 3, 31).with(adjustToWednesdayOrSunday())); // 周一 → 周三 (2025-04-02) (跨月)
System.out.println(LocalDate.of(2024, 12, 31).with(adjustToWednesdayOrSunday())); // 周二 → 周三 (2025-01-01) (跨年)
System.out.println(LocalDate.of(2025, 4, 2).with(adjustToWednesdayOrSunday())); // 周三 → 周三 (2025-04-02)
System.out.println(LocalDate.of(2025, 4, 3).with(adjustToWednesdayOrSunday())); // 周四 → 周日 (2025-04-06)
System.out.println(LocalDate.of(2025, 4, 4).with(adjustToWednesdayOrSunday())); // 周五 → 周日 (2025-04-06)
System.out.println(LocalDate.of(2025, 4, 5).with(adjustToWednesdayOrSunday())); // 周六 → 周日 (2025-04-06)
System.out.println(LocalDate.of(2025, 4, 6).with(adjustToWednesdayOrSunday())); // 周日 → 周日 (2025-04-06)
}
}
|
新旧 API 转换
旧 → 新
1
2
3
4
5
|
// Date 转 Instant
Instant instant = new Date().toInstant();
// Calendar 转 ZonedDateTime
ZonedDateTime zdt = calendar.toInstant().atZone(calendar.getTimeZone().toZoneId());
|
新 → 旧
1
2
3
4
5
6
|
// ZonedDateTime 转 Date
Date date = Date.from(zonedDateTime.toInstant());
// LocalDateTime 转 Calendar
Calendar cal = Calendar.getInstance();
cal.clear();
cal.set(localDateTime.getYear(), localDateTime.getMonthValue() - 1, localDateTime.getDayOfMonth());
|
最佳实践与常用操作
时间计算
1
2
3
4
|
// 添加 10 天
LocalDate newDate = LocalDate.now().plusDays(10);
// 计算两个日期间的天数
long daysBetween = ChronoUnit.DAYS.between(startDate, endDate);
|
时区转换
1
|
ZonedDateTime newYorkTime = ZonedDateTime.now().withZoneSameInstant(ZoneId.of("America/New_York"));
|
闰年判断
1
|
boolean isLeap = LocalDate.now().isLeapYear();
|
数据库交互
| Java 类型 |
JDBC 类型 |
LocalDate |
DATE |
LocalTime |
Time |
LocalDateTime |
TIMESTAMP |
Instant |
TIMESTAMP WITH TIME ZONE |
实用工具方法
工作日计算(跳过周末)
1
2
3
4
5
6
7
8
9
10
11
12
|
public static LocalDate addWorkDays(LocalDate start, int workDays) {
LocalDate date = start;
int addedDays = 0;
while (addedDays < workDays) {
date = date.plusDays(1);
if (date.getDayOfWeek() != DayOfWeek.SATURDAY
&& date.getDayOfWeek() != DayOfWeek.SUNDAY) {
addedDays++;
}
}
return date;
}
|
时间段重叠检测
1
2
3
4
|
public static boolean isOverlap(LocalDateTime start1, LocalDateTime end1,
LocalDateTime start2, LocalDateTime end2) {
return start1.isBefore(end2) && start2.isBefore(end1);
}
|
注意事项
- 时区陷阱:始终使用 IANA 时区名 (如
Asia/Shanghai),而非所写(如 CST)。
- 不可变性:所有
java.time 类均为不可变,每次操作返回新对象。
- 性能优化:重用
DateTimeFormatter 实例(避免重复解析模式)
总结
- 优先选择:始终使用
java.time 处理时间,避免传统 API。
- 复杂场景:时区转换、夏令时/冬令时处理使用
ZonedDateTime 和 ZoneRules。
- 扩展性:通过
TemporalAdjuster 实现自定义时间逻辑。
通过掌握现代 Java 时间 API,能够以更简洁、安全的方式处理复杂的日期时间需求。
FAQ
如何使用 IANA 时区数据库 ?
JDK
1.8+
Java 内置时区数据是 Java 运行时(JRE/JDK)自带时区数据文件(tzdb.dat),但这些数据可能不是最新版本。我们可以通过如下方式进行更新:
- 直接更新 JRE 时区数据:使用 Oracle 的
tzupdater 工具。
- 运行时动态加载:通过第三方库(如:
ThreeTen-BP 或 Joda-Time)加载最新 IANA 数据。
使用 tzupdater
- 下载工具 Oracle tzupdater
- 运行更新:
1
|
java -jar tzupdater.jar -v -l https://www.iana.org/time-zones/repository/tzdata-latest.tar.gz
|
附录: 常用第三方工具
一些常用的第三方工具均对日期时间有所封装,并提供了很多有用的方法。如果我们在工程中使用这些工具,可以避免重复造轮子。
Apache Commons Lang
Maven 依赖:
Apache Commons Lang
3.17.0
1
2
3
4
5
|
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${apache-commons-lang3.version}</version>
</dependency>
|
Google Guava
Maven 依赖:
Google Guava
33.4.6-jre
1
2
3
4
5
|
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${google-guava.version}</version>
</dependency>
|
Maven 依赖:
Hutool
5.8.36
1
2
3
4
5
|
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
|