作为Java开发者,处理日期和时间是日常工作中的常见任务。本文将带你全面掌握Java日期时间处理的方方面面,从基础概念到高级应用,从常见陷阱到最佳实践。
一、Java日期时间API演进史
让我们先通过一个对比表格了解Java日期时间API的发展历程:
版本 | API | 优点 | 缺点 | 现状 |
Java 1.0 | java.util.Date | 简单易用 | 设计缺陷多,月份从0开始 | 已废弃 |
Java 1.1 | java.util.Calendar | 解决了Date的部分问题 | API设计复杂,可变对象 | 不推荐 |
Java 8 | java.time (JSR-310) | 设计优良,线程安全 | 学习曲线稍陡 | 推荐使用 |
通俗解释:就像手机发展史,Date像早期的功能机,Calendar像复杂的智能机,而java.time就像现代流畅的智能手机。
1.1 总结对比表
特性 | java.util.Date | java.util.Calendar | java.time |
设计 | 糟糕 | 复杂 | 优秀 |
线程安全 | 否 | 否 | 是 |
可读性 | 差 | 一般 | 好 |
方法链 | 不支持 | 不支持 | 支持 |
时区支持 | 有限 | 有 | 完善 |
扩展性 | 差 | 一般 | 好 |
推荐使用 | 不推荐 | 不推荐 | 推荐 |
1.2 UML核心类图
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ Temporal │<|-----│ TemporalAccessor│ │ DateTimeFormatter│
└───────────────────┘ └───────────────────┘ └───────────────────┘
^ ^
| |
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ ChronoLocalDate │ │ LocalTime │ │ ZoneId │
└───────────────────┘ └───────────────────┘ └───────────────────┘
^ ^ ^
| | |
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ LocalDate │ │ LocalDateTime │------>│ ZonedDateTime │
└───────────────────┘ └───────────────────┘ └───────────────────┘
^ ^
| |
┌───────────────────┐ ┌───────────────────┐
│ Instant │ │ OffsetDateTime │
└───────────────────┘ └───────────────────┘
1.3 思维导图概览
Java日期时间处理
├── 传统API
│ ├── Date (不推荐)
│ └── Calendar (不推荐)
└── java.time (JSR-310)
├── 基本类
│ ├── Instant (时间点)
│ ├── LocalDate (日期)
│ ├── LocalTime (时间)
│ └── LocalDateTime (日期时间)
├── 时区类
│ ├── ZoneId (时区)
│ ├── ZonedDateTime (带时区时间)
│ └── OffsetDateTime (带偏移时间)
├── 时间段
│ ├── Duration (基于时间)
│ └── Period (基于日期)
├── 工具类
│ ├── DateTimeFormatter (格式化)
│ └── TemporalAdjusters (调整器)
└── 其他
├── ChronoUnit (时间单位)
└── DayOfWeek (星期枚举)
二、核心类详解
2.1 基础类介绍
// 1. LocalDate - 只包含日期,不包含时间和时区
LocalDate birthday = LocalDate.of(1990, 8, 20);
System.out.println("我的生日: " + birthday);
// 2. LocalTime - 只包含时间,不包含日期和时区
LocalTime lunchTime = LocalTime.of(12, 30);
System.out.println("午餐时间: " + lunchTime);
// 3. LocalDateTime - 包含日期和时间,但不包含时区
LocalDateTime meetingTime = LocalDateTime.of(2023, 8, 20, 14, 30);
System.out.println("会议时间: " + meetingTime);
// 4. ZonedDateTime - 完整的日期时间,包含时区
ZonedDateTime flightTime = ZonedDateTime.of(
LocalDateTime.of(2023, 8, 20, 15, 30),
ZoneId.of("Asia/Shanghai")
);
System.out.println("航班时间: " + flightTime);
// 5. Instant - 时间线上的瞬时点(UTC时间)
Instant now = Instant.now();
System.out.println("当前时刻(UTC): " + now);
// 6、时间段:Duration (基于时间的量,如"25秒")
Duration duration = Duration.ofSeconds(25);
2.2 核心类对比表
类 | 包含日期 | 包含时间 | 包含时区 | 典型用途 |
LocalDate | 不含时区的日期,生日、纪念日 | |||
LocalTime | 不含时区的时间,营业时间、会议时间 | |||
LocalDateTime | 不含时区的日期时间,本地活动时间 | |||
ZonedDateTime | 带时区的日期时间,国际航班时间 | |||
Instant | 隐含UTC | 日志时间戳 | ||
Period | 日期区间(年月日) | |||
Duration | 时间区间(时分秒纳秒),PT8H6M12.345S (8小时6分12.345秒) |
三、日期时间操作大全
3.1 创建与获取
// 获取当前日期时间
LocalDate today = LocalDate.now();
LocalTime now = LocalTime.now();
LocalDateTime current = LocalDateTime.now();
// 指定日期创建
LocalDate projectStart = LocalDate.of(2023, Month.SEPTEMBER, 1);
LocalTime standupTime = LocalTime.of(9, 30); // 每日站会时间
// 解析字符串
LocalDate parsedDate = LocalDate.parse("2023-08-20");
LocalTime parsedTime = LocalTime.parse("14:30:45");
LocalDateTime parsedDT = LocalDateTime.parse("2023-08-20T14:30:45");
3.2 日期时间运算
// 基本加减操作
LocalDate tomorrow = today.plusDays(1);
LocalDate nextWeek = today.plusWeeks(1);
LocalTime inAnHour = now.plusHours(1);
// 使用Period处理日期间隔
Period oneMonth = Period.ofMonths(1);
LocalDate nextMonth = today.plus(oneMonth);
// 使用Duration处理时间间隔
Duration twoHours = Duration.ofHours(2);
LocalTime later = now.plus(twoHours);
// 计算两个日期间隔
long daysBetween = ChronoUnit.DAYS.between(today, nextMonth);
System.out.println("间隔天数: " + daysBetween);
3.3 日期时间比较
// 比较方法
boolean isAfter = tomorrow.isAfter(today);
boolean isBefore = today.isBefore(tomorrow);
boolean isEqual = today.equals(LocalDate.now());
// 比较时间先后
LocalTime start = LocalTime.of(9, 0);
LocalTime end = LocalTime.of(17, 0);
if (now.isAfter(start) && now.isBefore(end)) {
System.out.println("现在是工作时间");
}
四、格式化与解析
4.1 SimpleDateFormat使用
java.util.Date
import java.util.Date;
import java.text.SimpleDateFormat;
// 创建当前时间的Date对象
Date now = new Date();
System.out.println(now); // 默认格式输出
// 格式化Date
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String formattedDate = sdf.format(now);
System.out.println(formattedDate);
// 解析字符串为Date
String dateString = "2023-05-15 14:30:00";
Date parsedDate = sdf.parse(dateString);
System.out.println(parsedDate);
java.util.Calendar
import java.util.Calendar;
// 获取Calendar实例
Calendar calendar = Calendar.getInstance();
// 设置特定日期时间
calendar.set(2023, Calendar.MAY, 15, 14, 30, 0);
// 获取各个字段
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1; // 月份从0开始
int day = calendar.get(Calendar.DAY_OF_MONTH);
// 转换为Date
Date date = calendar.getTime();
// 使用SimpleDateFormat格式化
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒");
System.out.println(sdf.format(date));
4.2 DateTimeFormatter使用
基础格式化
// 使用预定义格式
String isoDate = today.format(DateTimeFormatter.ISO_LOCAL_DATE);
String isoTime = now.format(DateTimeFormatter.ISO_LOCAL_TIME);
// 自定义格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
String formatted = current.format(formatter);
System.out.println("格式化结果: " + formatted);
// 解析
LocalDateTime parsed = LocalDateTime.parse("2023年08月20日 14:30:45", formatter);
本地化格式化
// 本地化格式
DateTimeFormatter germanFormatter = DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM)
.withLocale(Locale.GERMAN);
String germanDateTime = current.format(germanFormatter);
System.out.println("德国格式: " + germanDateTime);
// 中文格式
DateTimeFormatter chineseFormatter = DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.LONG)
.withLocale(Locale.CHINA);
System.out.println("中文格式: " + current.format(chineseFormatter));
常见模式符号
符号 | 含义 | 示例 |
y | 年 | 2023, 23 |
M | 月 | 8, 08, Aug, August |
d | 日 | 5, 05 |
H | 小时(0-23) | 0, 05, 14, 23 |
h | 小时(1-12, AM/PM) | 1, 12 |
m | 分钟 | 0, 05, 30, 59 |
s | 秒 | 0, 05, 30, 59 |
S | 毫秒 | 0, 500, 999 |
a | AM/PM | AM, PM |
E | 星期 | Mon, Monday |
z | 时区 | CST, GMT+08:00 |
Z | 时区偏移 | +0800 |
4.3 其他第三方库
4.3.1 Joda-Time (Java 8前推荐)
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
DateTime dt = new DateTime();
DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss");
// 格式化
String str = fmt.print(dt);
System.out.println(str);
// 解析
DateTime parsedDt = fmt.parseDateTime("2023-05-15 14:30:00");
System.out.println(parsedDt);
4.3.2 Apache Commons Lang的FastDateFormat
import org.apache.commons.lang3.time.FastDateFormat;
import java.util.Date;
FastDateFormat fdf = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss");
String formatted = fdf.format(new Date());
System.out.println(formatted);
五、时区处理实战
5.1 时区基础
// 获取可用时区
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
zoneIds.stream().filter(z -> z.contains("Asia")).forEach(System.out::println);
// 创建时区对象
ZoneId shanghaiZone = ZoneId.of("Asia/Shanghai");
ZoneId newYorkZone = ZoneId.of("America/New_York");
// 时区转换
ZonedDateTime shanghaiTime = ZonedDateTime.now(shanghaiZone);
ZonedDateTime newYorkTime = shanghaiTime.withZoneSameInstant(newYorkZone);
System.out.println("上海时间: " + shanghaiTime);
System.out.println("纽约时间: " + newYorkTime);
5.2 夏令时处理
// 检查夏令时
ZoneId londonZone = ZoneId.of("Europe/London");
ZoneRules rules = londonZone.getRules();
boolean isDst = rules.isDaylightSavings(Instant.now());
System.out.println("伦敦是否在夏令时: " + isDst);
// 夏令时转换示例
ZonedDateTime beforeDst = ZonedDateTime.of(
LocalDateTime.of(2023, 3, 12, 1, 59),
ZoneId.of("America/New_York"));
ZonedDateTime afterDst = beforeDst.plusMinutes(2);
System.out.println("夏令时转换: " + beforeDst + " → " + afterDst);
5.3 日期加减计算
LocalDate today = LocalDate.now();
// 加1天
LocalDate tomorrow = today.plusDays(1);
// 加1周
LocalDate nextWeek = today.plusWeeks(1);
// 加2个月
LocalDate inTwoMonths = today.plusMonths(2);
// 减10年
LocalDate tenYearsAgo = today.minusYears(10);
5.4 日期比较
LocalDate date1 = LocalDate.of(2023, 5, 1);
LocalDate date2 = LocalDate.of(2023, 5, 15);
// 比较
boolean isBefore = date1.isBefore(date2); // true
boolean isAfter = date1.isAfter(date2); // false
boolean isEqual = date1.isEqual(date2); // false
// 计算两个日期之间的天数
long daysBetween = ChronoUnit.DAYS.between(date1, date2); // 14
六、高级技巧
6.1 时间调整器
// 内置调整器
LocalDate nextFriday = today.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));
LocalDate lastDayOfYear = today.with(TemporalAdjusters.lastDayOfYear());
// 自定义调整器:下个工作日
TemporalAdjuster nextWorkingDay = temporal -> {
DayOfWeek day = DayOfWeek.from(temporal);
int daysToAdd = 1;
if (day == DayOfWeek.FRIDAY) daysToAdd = 3;
else if (day == DayOfWeek.SATURDAY) daysToAdd = 2;
return temporal.plus(daysToAdd, ChronoUnit.DAYS);
};
LocalDate nextWorkDate = today.with(nextWorkingDay);
System.out.println("下个工作日: " + nextWorkDate);
6.2 时间段计算
// 计算两个日期之间的工作日
public static long countWorkingDays(LocalDate start, LocalDate end) {
return start.datesUntil(end)
.filter(date -> date.getDayOfWeek() != DayOfWeek.SATURDAY
&& date.getDayOfWeek() != DayOfWeek.SUNDAY)
.count();
}
// 计算年龄
public static int calculateAge(LocalDate birthDate, LocalDate currentDate) {
return Period.between(birthDate, currentDate).getYears();
}
七、与传统API互操作
7.1 与Date/Calendar转换
// Date → java.time
Date oldDate = new Date();
Instant instant = oldDate.toInstant();
ZonedDateTime zdt = instant.atZone(ZoneId.systemDefault());
// java.time → Date
LocalDateTime ldt = LocalDateTime.now();
Date newDate = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());
// Calendar → java.time
Calendar calendar = Calendar.getInstance();
ZonedDateTime zdtFromCal = ZonedDateTime.ofInstant(
calendar.toInstant(), calendar.getTimeZone().toZoneId());
7.2 与数据库交互
// java.time → SQL
LocalDate localDate = LocalDate.now();
java.sql.Date sqlDate = java.sql.Date.valueOf(localDate);
LocalDateTime localDateTime = LocalDateTime.now();
java.sql.Timestamp timestamp = java.sql.Timestamp.valueOf(localDateTime);
// SQL → java.time
LocalDate fromSqlDate = sqlDate.toLocalDate();
LocalDateTime fromTimestamp = timestamp.toLocalDateTime();
八、最佳实践
- 始终明确时区:避免隐式使用系统默认时区
- // 不推荐
ZonedDateTime zdt = LocalDateTime.now().atZone(ZoneId.systemDefault());
// 推荐
ZonedDateTime zdt = LocalDateTime.now().atZone(ZoneId.of("Asia/Shanghai")); - 使用不可变对象:所有修改操作返回新对象
- LocalDate date = LocalDate.now();
// 错误 - 结果被丢弃
date.plusDays(1);
// 正确
LocalDate tomorrow = date.plusDays(1); - 选择合适的时间类型:
- 生日、纪念日 → LocalDate
- 营业时间 → LocalTime
- 本地活动 → LocalDateTime
- 国际事件 → ZonedDateTime
- 时间戳 → Instant
九、常见问题与解决方案
问题1:时区转换错误
场景:跨国会议时间显示不正确
解决方案:
LocalDateTime localMeetingTime = LocalDateTime.of(2023, 8, 20, 14, 0);
ZonedDateTime londonMeeting = localMeetingTime.atZone(ZoneId.of("Europe/London"));
ZonedDateTime newYorkMeeting = londonMeeting.withZoneSameInstant(ZoneId.of("America/New_York"));
System.out.println("伦敦时间: " + londonMeeting);
System.out.println("纽约时间: " + newYorkMeeting);
问题2:日期格式解析失败
场景:用户输入的日期字符串格式不固定
解决方案:
String[] possiblePatterns = {"yyyy-MM-dd", "yyyy/MM/dd", "MM/dd/yyyy"};
LocalDate parseDate(String dateStr) {
for (String pattern : possiblePatterns) {
try {
return LocalDate.parse(dateStr, DateTimeFormatter.ofPattern(pattern));
} catch (DateTimeParseException e) {
// 尝试下一种格式
}
}
throw new IllegalArgumentException("无法解析日期: " + dateStr);
}
十、性能优化技巧
- 重用DateTimeFormatter:避免重复创建格式化对象
- // 类级别定义
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public String formatDateTime(LocalDateTime dateTime) {
return dateTime.format(FORMATTER);
} - 使用Instant处理高频时间操作:
- Instant start = Instant.now();
// 执行操作...
Instant end = Instant.now();
Duration elapsed = Duration.between(start, end);
System.out.println("耗时: " + elapsed.toMillis() + "ms");
附录:完整方法参考表
LocalDate常用方法
方法 | 描述 | 示例 |
now() | 获取当前日期 | LocalDate.now() |
of() | 指定日期创建 | LocalDate.of(2023, 8, 20) |
parse() | 解析字符串 | LocalDate.parse("2023-08-20") |
plusDays() | 加天数 | today.plusDays(1) |
minusMonths() | 减月数 | today.minusMonths(1) |
getDayOfWeek() | 获取星期 | today.getDayOfWeek() |
isLeapYear() | 是否闰年 | today.isLeapYear() |
datesUntil() | 日期范围流 | start.datesUntil(end) |
通过本指南,你应该已经掌握了Java日期时间处理的完整知识体系。记住,对于新项目,始终优先使用java.time API,它提供了更现代、更安全的日期时间操作方式。
日期指南已解锁,别嘚瑟!时区偏移量没处理好,约会时间都能给你整成跨国异地恋!
笑过之后请记住:生活就像我的排版,乱中有序,序中带梗。