1. 为什么需要全新的时间API
如果你曾经在Java 8之前处理过日期和时间,一定对java.util.Date和java.util.Calendar的种种问题深有体会。这些老旧的API设计存在诸多缺陷:
- 线程安全问题:
SimpleDateFormat不是线程安全的,这意味着在多线程环境下必须额外小心 - 糟糕的API设计:月份从0开始(1月是0),年份从1900开始计算,这些反直觉的设计导致无数bug
- 时区处理复杂:时区转换需要繁琐的操作,容易出错
- 可变性:日期对象是可变的,可能导致意外的修改
JDK 8引入的java.time包彻底重构了日期时间API,解决了上述所有问题。这套新API的设计灵感来自Joda-Time,但做了进一步的改进。
2. 核心类概览
java.time包中的类可以分为以下几类:
2.1 基本日期时间类
LocalDate:只包含日期,如2023-05-15LocalTime:只包含时间,如14:30:00LocalDateTime:包含日期和时间,如2023-05-15T14:30:00ZonedDateTime:带时区的完整日期时间,如2023-05-15T14:30:00+08:00[Asia/Shanghai]
2.2 时间点和时间段
Instant:时间线上的一个瞬时点,通常用于记录事件时间戳Duration:基于时间的持续时间(小时、分钟、秒、纳秒)Period:基于日期的持续时间(年、月、日)
2.3 辅助类
ZoneId:时区标识符DateTimeFormatter:格式化和解析日期时间TemporalAdjusters:提供常用的日期调整器
3. 创建和访问日期时间
3.1 创建对象
新API提供了多种创建日期时间对象的方式:
java复制// 当前日期
LocalDate today = LocalDate.now();
// 指定日期
LocalDate birthday = LocalDate.of(1990, Month.MAY, 15);
// 从字符串解析
LocalDate parsedDate = LocalDate.parse("2023-05-15");
// 当前日期时间
LocalDateTime now = LocalDateTime.now();
// 指定日期时间
LocalDateTime meetingTime = LocalDateTime.of(2023, Month.MAY, 15, 14, 30);
// 带时区的日期时间
ZonedDateTime zonedNow = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
3.2 访问字段
与旧API不同,新API提供了直观的方法来访问各个字段:
java复制LocalDate date = LocalDate.of(2023, Month.MAY, 15);
int year = date.getYear(); // 2023
Month month = date.getMonth(); // MAY
int day = date.getDayOfMonth(); // 15
DayOfWeek dow = date.getDayOfWeek(); // MONDAY
int len = date.lengthOfMonth(); // 31
boolean leap = date.isLeapYear(); // false
4. 日期时间操作
新API的一个重要特点是不可变性,所有修改操作都会返回新的对象。
4.1 加减时间
java复制LocalDate date = LocalDate.of(2023, 5, 15);
// 加1周
LocalDate nextWeek = date.plusWeeks(1);
// 减3个月
LocalDate threeMonthsAgo = date.minusMonths(3);
// 加5年3天
LocalDate future = date.plusYears(5).plusDays(3);
4.2 修改特定字段
java复制LocalDate date = LocalDate.of(2023, 5, 15);
// 修改年份
LocalDate newYear = date.withYear(2024);
// 修改月份
LocalDate newMonth = date.withMonth(12);
// 使用TemporalAdjusters
LocalDate lastDayOfMonth = date.with(TemporalAdjusters.lastDayOfMonth());
4.3 自定义调整器
你可以创建自己的调整器:
java复制TemporalAdjuster nextWorkingDay = TemporalAdjusters.ofDateAdjuster(
date -> {
DayOfWeek dow = date.getDayOfWeek();
int daysToAdd = 1;
if (dow == DayOfWeek.FRIDAY) daysToAdd = 3;
if (dow == DayOfWeek.SATURDAY) daysToAdd = 2;
return date.plusDays(daysToAdd);
});
LocalDate date = LocalDate.of(2023, 5, 19); // 周五
LocalDate nextWorkDay = date.with(nextWorkingDay); // 2023-05-22 (周一)
5. 日期时间格式化
DateTimeFormatter是新API中用于格式化和解析的核心类。
5.1 预定义格式
java复制LocalDateTime now = LocalDateTime.now();
// 使用预定义格式
String isoDateTime = now.format(DateTimeFormatter.ISO_DATE_TIME);
String isoDate = now.format(DateTimeFormatter.ISO_DATE);
String isoTime = now.format(DateTimeFormatter.ISO_TIME);
// 本地化格式
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM);
String localized = now.format(formatter);
5.2 自定义格式
java复制DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime now = LocalDateTime.now();
String formatted = now.format(formatter); // "2023-05-15 14:30:00"
LocalDateTime parsed = LocalDateTime.parse("2023-05-15 14:30:00", formatter);
5.3 复杂格式
对于更复杂的需求,可以使用DateTimeFormatterBuilder:
java复制DateTimeFormatter complexFormatter = new DateTimeFormatterBuilder()
.appendText(ChronoField.DAY_OF_WEEK)
.appendLiteral(", ")
.appendText(ChronoField.MONTH_OF_YEAR)
.appendLiteral(" ")
.appendText(ChronoField.DAY_OF_MONTH)
.appendLiteral(", ")
.appendText(ChronoField.YEAR)
.toFormatter(Locale.US);
String formatted = LocalDate.now().format(complexFormatter);
// "Monday, May 15, 2023"
6. 时区处理
时区是日期时间处理中最复杂的部分之一,新API提供了清晰的解决方案。
6.1 ZoneId和ZoneOffset
java复制// 获取可用时区
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
// 创建时区对象
ZoneId shanghaiZone = ZoneId.of("Asia/Shanghai");
ZoneId utcZone = ZoneId.of("UTC");
// 创建带时区的日期时间
ZonedDateTime shanghaiTime = ZonedDateTime.now(shanghaiZone);
ZonedDateTime utcTime = shanghaiTime.withZoneSameInstant(utcZone);
6.2 时区转换
java复制// 创建本地日期时间
LocalDateTime localDateTime = LocalDateTime.of(2023, 5, 15, 14, 30);
// 转换为带时区的日期时间
ZonedDateTime shanghaiTime = localDateTime.atZone(ZoneId.of("Asia/Shanghai"));
// 转换为其他时区
ZonedDateTime newYorkTime = shanghaiTime.withZoneSameInstant(ZoneId.of("America/New_York"));
7. 时间点和持续时间
7.1 Instant
Instant表示时间线上的一个瞬时点,通常用于记录事件时间戳:
java复制// 当前时间戳
Instant now = Instant.now();
// 从秒数创建
Instant epoch = Instant.ofEpochSecond(0);
// 从毫秒数创建
Instant fromMillis = Instant.ofEpochMilli(System.currentTimeMillis());
// 转换为ZonedDateTime
ZonedDateTime zdt = now.atZone(ZoneId.of("Asia/Shanghai"));
7.2 Duration和Period
java复制// 计算两个时间点之间的持续时间
LocalTime start = LocalTime.of(9, 0);
LocalTime end = LocalTime.of(17, 30);
Duration duration = Duration.between(start, end);
long hours = duration.toHours(); // 8
// 计算两个日期之间的时间段
LocalDate birthday = LocalDate.of(1990, 5, 15);
LocalDate today = LocalDate.now();
Period period = Period.between(birthday, today);
int years = period.getYears(); // 年龄
8. 与旧API的互操作
虽然新API更好,但有时需要与旧代码交互:
8.1 转换为旧API
java复制// Instant -> Date
Instant instant = Instant.now();
Date legacyDate = Date.from(instant);
// LocalDateTime -> Date
LocalDateTime localDateTime = LocalDateTime.now();
Instant instant2 = localDateTime.atZone(ZoneId.systemDefault()).toInstant();
Date legacyDate2 = Date.from(instant2);
8.2 从旧API转换
java复制// Date -> Instant
Date date = new Date();
Instant instant = date.toInstant();
// Calendar -> ZonedDateTime
Calendar calendar = Calendar.getInstance();
ZonedDateTime zdt = ZonedDateTime.ofInstant(calendar.toInstant(), calendar.getTimeZone().toZoneId());
9. 最佳实践和常见问题
9.1 存储和传输
- 在数据库中存储时,考虑使用
Instant或LocalDateTime - 在API间传输时,使用ISO-8601格式的字符串
- 避免使用
Date和Calendar
9.2 性能考虑
Instant比Date更轻量- 重用
DateTimeFormatter实例(它们是线程安全的) - 对于高频操作,考虑缓存计算结果
9.3 常见陷阱
- 不要混淆
LocalDateTime和ZonedDateTime:前者没有时区信息 - 注意夏令时转换期间的边界情况
- 处理用户输入时,明确指定预期的时区
10. 实际应用示例
10.1 计算工作日
java复制public static LocalDate calculateWorkday(LocalDate startDate, int workdays) {
LocalDate result = startDate;
int addedDays = 0;
while (addedDays < workdays) {
result = result.plusDays(1);
if (result.getDayOfWeek() != DayOfWeek.SATURDAY
&& result.getDayOfWeek() != DayOfWeek.SUNDAY) {
addedDays++;
}
}
return result;
}
10.2 会议时间提醒
java复制public static void scheduleMeetingReminder(LocalDateTime meetingTime, ZoneId timeZone) {
ZonedDateTime zonedMeetingTime = meetingTime.atZone(timeZone);
ZonedDateTime now = ZonedDateTime.now(timeZone);
Duration duration = Duration.between(now, zonedMeetingTime);
if (duration.isNegative()) {
System.out.println("会议已结束");
} else {
System.out.printf("距离会议还有%d小时%d分钟%n",
duration.toHours(),
duration.toMinutesPart());
}
}
10.3 处理用户输入
java复制public static ZonedDateTime parseUserInput(String dateStr, String timeStr, String zoneStr) {
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm");
LocalDate date = LocalDate.parse(dateStr, dateFormatter);
LocalTime time = LocalTime.parse(timeStr, timeFormatter);
ZoneId zone = ZoneId.of(zoneStr);
return ZonedDateTime.of(date, time, zone);
}
JDK 8的时间API提供了强大而灵活的工具来处理各种日期时间场景。通过合理使用这些类和方法,你可以编写出更清晰、更健壮的日期时间处理代码。在实际项目中,建议完全转向新API,逐步淘汰旧的Date和Calendar类。
