新日期时间API详解
大翔盯着屏幕上的代码,眉头紧锁。
Date meetingDate = new Date(2024, 2, 15); System.out.println(meetingDate); // Thu Mar 15 00:00:00 CST 3924 ← 什么鬼!白歌一眼就看出问题:"
Date的年份是从 1900 开始算的,月份是从 0 开始的。你传2024, 2, 15,实际表示的是 3924 年 3 月 15 日。"大翔崩溃:"这不是反人类设计吗?"
朱璐在一旁幸灾乐祸:"更坑的是
Date还是可变的,多线程下能给你表演'时间旅行'。"Frank 总监路过:"这就是为什么 JDK 8 引入了
java.time包。今天所有人必须学会新 API——不可变、线程安全、语义清晰。"小崔虔诚地打开 API 文档:"Joda-Time 的作者 Stephen Colebourne 亲自操刀的?那稳了。"
一、定义表
1.1 核心类一览
| 类 | 描述 | 示例值 | 是否包含时区 |
|---|---|---|---|
LocalDate | 日期(年-月-日) | 2024-03-15 | 否 |
LocalTime | 时间(时:分:秒.纳秒) | 14:30:00.123456789 | 否 |
LocalDateTime | 日期+时间 | 2024-03-15T14:30:00 | 否 |
ZonedDateTime | 带时区的日期时间 | 2024-03-15T14:30:00+08:00[Asia/Shanghai] | 是 |
OffsetDateTime | 带偏移量的日期时间 | 2024-03-15T14:30:00+08:00 | 是 |
Instant | 时间戳(UTC 时间线上的一个点) | 2024-03-15T06:30:00Z | UTC |
Period | 日期差(年、月、日) | P1Y2M3D (1年2月3天) | - |
Duration | 时间差(秒、纳秒) | PT2H30M (2小时30分钟) | - |
ZoneId | 时区标识 | Asia/Shanghai, America/New_York | - |
ZoneOffset | UTC 偏移量 | +08:00, -05:00 | - |
DateTimeFormatter | 格式化器(线程安全) | yyyy-MM-dd HH:mm:ss | - |
TemporalAdjusters | 时间调节器工厂 | firstDayOfMonth(), next(DayOfWeek.MONDAY) | - |
1.2 新旧API对比
| 特性 | java.util.Date/Calendar | java.time (JDK 8) |
|---|---|---|
| 可变性 | 可变(线程不安全) | 不可变(线程安全) |
| API 设计 | 反直觉(月份从0开始,年份从1900开始) | 语义清晰(month=3 就是3月) |
| 时区处理 | 混乱(Date 无时区概念) | 明确分为 Local/Zoned/Offset |
| 格式化 | SimpleDateFormat(线程不安全) | DateTimeFormatter(线程安全) |
| 日期运算 | 需要 Calendar 的 add/roll | plusDays(), minusMonths(), with(TemporalAdjusters) |
| 纳秒精度 | 不支持 | 支持(最高 9 位纳秒) |
二、Mermaid 类图:java.time 核心类体系
三、Mermaid 流程图:日期运算与 TemporalAdjuster 执行流程
四、核心API深度解析
4.1 创建日期时间对象
// 当前时间
LocalDate today = LocalDate.now();
LocalTime now = LocalTime.now();
LocalDateTime dateTime = LocalDateTime.now();
ZonedDateTime zonedNow = ZonedDateTime.now(); // 使用系统默认时区
Instant instant = Instant.now(); // UTC 时间戳
// 指定值创建
LocalDate date = LocalDate.of(2024, 3, 15); // 2024-03-15
LocalTime time = LocalTime.of(14, 30, 0); // 14:30:00
LocalDateTime dt = LocalDateTime.of(2024, 3, 15, 14, 30);
ZonedDateTime zdt = ZonedDateTime.of(dt, ZoneId.of("Asia/Shanghai"));
Instant instant2 = Instant.ofEpochSecond(1710484800);
// 字符串解析
LocalDate parsed = LocalDate.parse("2024-03-15");
LocalDateTime parsedDt = LocalDateTime.parse("2024-03-15T14:30:00");
4.2 日期时间运算(不可变对象,返回新实例)
// plus/minus 运算
LocalDate nextWeek = today.plusWeeks(1);
LocalDate lastMonth = today.minusMonths(1);
LocalDateTime plusHours = dateTime.plusHours(3).minusMinutes(30);
// with 运算(设置指定字段)
LocalDate firstOfMonth = today.withDayOfMonth(1); // 本月第一天
LocalDate lastOfYear = today.with(TemporalAdjusters.lastDayOfYear());
// 日期比较
boolean isAfter = date1.isAfter(date2);
boolean isBefore = date1.isBefore(date2);
4.3 Period 与 Duration
// Period:日期层面的差异(年、月、日)
Period period = Period.between(
LocalDate.of(2024, 1, 1),
LocalDate.of(2024, 3, 15)
); // P2M14D
// Duration:时间层面的差异(秒、纳秒)
Duration duration = Duration.between(
LocalTime.of(9, 0),
LocalTime.of(17, 30)
); // PT8H30M
4.4 新旧API转换
// java.util.Date → java.time
Instant instant = oldDate.toInstant();
ZonedDateTime zdt = instant.atZone(ZoneId.systemDefault());
LocalDateTime ldt = zdt.toLocalDateTime();
// java.time → java.util.Date
Date newDate = Date.from(instant);
// LocalDateTime → Date 需要先附加时区
Date date = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());
// java.util.Calendar → java.time
Instant instant2 = calendar.toInstant();
五、DateTimeFormatter(线程安全!)
// 预定义格式器
DateTimeFormatter.ISO_LOCAL_DATE; // 2024-03-15
DateTimeFormatter.ISO_LOCAL_DATE_TIME; // 2024-03-15T14:30:00
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL); // 2024年3月15日 星期五
// 自定义格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
String formatted = LocalDateTime.now().format(formatter);
LocalDateTime parsed = LocalDateTime.parse("2024年03月15日 14:30:00", formatter);
六、完整代码示例
示例1:飞翔科技全球办公时区转换
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.*;
/**
* 飞翔科技全球办公时区转换系统
* 演示 ZonedDateTime、ZoneId、时区转换
*/
public class FeixiangTimeZoneDemo {
public static void main(String[] args) {
System.out.println("========== 飞翔科技全球办公时区转换 ==========\n");
// 飞翔科技的全球办公室
Map<String, ZoneId> offices = new LinkedHashMap<>();
offices.put("北京总部", ZoneId.of("Asia/Shanghai")); // UTC+8
offices.put("深圳研发中心", ZoneId.of("Asia/Shanghai")); // UTC+8
offices.put("东京办事处", ZoneId.of("Asia/Tokyo")); // UTC+9
offices.put("新加坡分部", ZoneId.of("Asia/Singapore")); // UTC+8
offices.put("纽约分部", ZoneId.of("America/New_York")); // UTC-5 / -4(夏令时)
offices.put("伦敦分部", ZoneId.of("Europe/London")); // UTC+0 / +1(夏令时)
offices.put("柏林研发中心", ZoneId.of("Europe/Berlin")); // UTC+1 / +2(夏令时)
offices.put("悉尼分部", ZoneId.of("Australia/Sydney")); // UTC+10 / +11(夏令时)
// ===== 1. 北京总部当前时间 =====
ZonedDateTime beijingTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z (O)");
System.out.println("【北京总部时间】");
System.out.println(" " + beijingTime.format(fmt));
System.out.println(" UTC偏移: " + beijingTime.getOffset());
System.out.println(" 时区: " + beijingTime.getZone());
System.out.println();
// ===== 2. 全部办公室当前时间 =====
System.out.println("【全球各办公室当前时间(基于同一 UTC 瞬时点)】");
Instant now = Instant.now(); // UTC 基准
System.out.printf(" %-16s %-30s %s\n", "地点", "本地时间", "UTC偏移");
System.out.println(" " + "-".repeat(65));
for (Map.Entry<String, ZoneId> entry : offices.entrySet()) {
ZonedDateTime localTime = now.atZone(entry.getValue());
System.out.printf(" %-16s %-30s %s\n",
entry.getKey(),
localTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
localTime.getOffset());
}
// ===== 3. 时差计算 =====
System.out.println("\n【时差计算】");
ZoneId beijing = ZoneId.of("Asia/Shanghai");
ZoneId newYork = ZoneId.of("America/New_York");
ZoneId london = ZoneId.of("Europe/London");
ZonedDateTime bjNow = ZonedDateTime.now(beijing);
ZonedDateTime nyNow = bjNow.withZoneSameInstant(newYork); // 同一时刻
ZonedDateTime londonNow = bjNow.withZoneSameInstant(london);
System.out.printf(" 当北京是 %s\n",
bjNow.format(DateTimeFormatter.ofPattern("HH:mm")));
System.out.printf(" 纽约是 %s (%s 天)\n",
nyNow.format(DateTimeFormatter.ofPattern("HH:mm")),
nyNow.toLocalDate().equals(bjNow.toLocalDate()) ? "同" :
(nyNow.toLocalDate().isBefore(bjNow.toLocalDate()) ? "前" : "后"));
System.out.printf(" 伦敦是 %s (%s 天)\n",
londonNow.format(DateTimeFormatter.ofPattern("HH:mm")),
londonNow.toLocalDate().equals(bjNow.toLocalDate()) ? "同" :
(londonNow.toLocalDate().isBefore(bjNow.toLocalDate()) ? "前" : "后"));
// ===== 4. 会议时间选择 =====
System.out.println("\n【全球会议时间推荐】");
// 假设选在北京时间 20:00 开会
LocalTime meetingBeijingTime = LocalTime.of(20, 0); // 晚上8点
ZonedDateTime meetingBeijing = ZonedDateTime.of(
LocalDate.now(), meetingBeijingTime, beijing);
System.out.println(" 提议会议时间(北京时间): " +
meetingBeijing.format(DateTimeFormatter.ofPattern("HH:mm")));
System.out.println();
// 检查各办公室是否在工作时间(9:00-18:00)
System.out.printf(" %-16s %-12s %-15s\n", "办公室", "本地时间", "是否工作时间");
System.out.println(" " + "-".repeat(50));
for (Map.Entry<String, ZoneId> entry : offices.entrySet()) {
ZonedDateTime localMeeting = meetingBeijing.withZoneSameInstant(entry.getValue());
LocalTime localTime = localMeeting.toLocalTime();
boolean isWorkingHour = !localTime.isBefore(LocalTime.of(9, 0))
&& !localTime.isAfter(LocalTime.of(18, 0));
System.out.printf(" %-16s %-12s %-15s\n",
entry.getKey(),
localTime.format(DateTimeFormatter.ofPattern("HH:mm")),
isWorkingHour ? "✓ 工作时间" : "✗ 非工作时间");
}
// ===== 5. 夏令时影响演示 =====
System.out.println("\n【夏令时影响】");
// 纽约 2024年3月10日进入夏令时(UTC-4),11月3日退出(UTC-5)
LocalDate beforeDST = LocalDate.of(2024, 3, 9);
LocalDate afterDST = LocalDate.of(2024, 3, 11);
ZonedDateTime nyBeforeDST = ZonedDateTime.of(beforeDST,
LocalTime.of(12, 0), newYork);
ZonedDateTime nyAfterDST = ZonedDateTime.of(afterDST,
LocalTime.of(12, 0), newYork);
System.out.println(" 纽约 2024年3月9日 12:00 偏移: " + nyBeforeDST.getOffset()); // -05:00
System.out.println(" 纽约 2024年3月11日 12:00 偏移: " + nyAfterDST.getOffset()); // -04:00
System.out.println(" 说明:3月10日凌晨进入夏令时,时钟拨快1小时");
System.out.println("\n========== End ==========");
}
}
运行输出(示例):
========== 飞翔科技全球办公时区转换 ==========
【北京总部时间】
2024-03-15 14:30:00 CST (+08:00)
UTC偏移: +08:00
时区: Asia/Shanghai
【全球各办公室当前时间(基于同一 UTC 瞬时点)】
地点 本地时间 UTC偏移
-----------------------------------------------------------------
北京总部 2024-03-15 14:30:00 +08:00
深圳研发中心 2024-03-15 14:30:00 +08:00
东京办事处 2024-03-15 15:30:00 +09:00
新加坡分部 2024-03-15 14:30:00 +08:00
纽约分部 2024-03-15 02:30:00 -04:00
伦敦分部 2024-03-15 06:30:00 +00:00
柏林研发中心 2024-03-15 07:30:00 +01:00
悉尼分部 2024-03-15 17:30:00 +11:00
【时差计算】
当北京是 14:30
纽约是 02:30 (同 天)
伦敦是 06:30 (同 天)
【全球会议时间推荐】
提议会议时间(北京时间): 20:00
办公室 本地时间 是否工作时间
--------------------------------------------------
北京总部 20:00 ✗ 非工作时间
深圳研发中心 20:00 ✗ 非工作时间
东京办事处 21:00 ✗ 非工作时间
新加坡分部 20:00 ✗ 非工作时间
纽约分部 08:00 ✗ 非工作时间
伦敦分部 12:00 ✓ 工作时间
柏林研发中心 13:00 ✓ 工作时间
悉尼分部 23:00 ✗ 非工作时间
【夏令时影响】
纽约 2024年3月9日 12:00 偏移: -05:00
纽约 2024年3月11日 12:00 偏移: -04:00
说明:3月10日凌晨进入夏令时,时钟拨快1小时
========== End ==========
示例2:飞翔科技项目里程碑日期计算
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.*;
import java.util.*;
/**
* 飞翔科技项目管理系统 —— 里程碑日期计算
* 演示 Period、TemporalAdjusters、自定义 TemporalAdjuster
*/
public class FeixiangProjectMilestoneDemo {
// 自定义 TemporalAdjuster:找最近的工作日(跳过周末)
static class NextWorkingDay implements TemporalAdjuster {
@Override
public Temporal adjustInto(Temporal temporal) {
DayOfWeek dow = DayOfWeek.from(temporal);
int daysToAdd = 1;
if (dow == DayOfWeek.FRIDAY) {
daysToAdd = 3; // 周五 → 下周一
} else if (dow == DayOfWeek.SATURDAY) {
daysToAdd = 2; // 周六 → 下周一
}
return temporal.plus(daysToAdd, ChronoUnit.DAYS);
}
}
// 自定义 TemporalAdjuster:找本季度第一天
static class FirstDayOfQuarter implements TemporalAdjuster {
@Override
public Temporal adjustInto(Temporal temporal) {
LocalDate date = LocalDate.from(temporal);
int month = date.getMonthValue();
int quarterStartMonth = ((month - 1) / 3) * 3 + 1;
return date.withMonth(quarterStartMonth).withDayOfMonth(1);
}
}
public static void main(String[] args) {
System.out.println("========== 飞翔科技项目里程碑系统 ==========\n");
DateTimeFormatter dateFmt = DateTimeFormatter.ofPattern("yyyy年MM月dd日 (EEEE)");
// ===== 1. 项目基本信息 =====
LocalDate projectStart = LocalDate.of(2024, 3, 15);
System.out.println("【项目启动日期】" + projectStart.format(dateFmt));
System.out.println();
// ===== 2. 使用 Period 计算阶段间隔 =====
System.out.println("【里程碑计划(使用 plus/Period)】");
System.out.println("-".repeat(45));
// 需求分析阶段:2周
LocalDate requirementEnd = projectStart.plusWeeks(2);
Period reqPeriod = Period.between(projectStart, requirementEnd);
System.out.printf(" 需求分析: %s → %s (%d 天)\n",
projectStart.format(dateFmt),
requirementEnd.format(dateFmt),
reqPeriod.getDays());
// 设计阶段:3周
LocalDate designEnd = requirementEnd.plusWeeks(3);
Period designPeriod = Period.between(requirementEnd, designEnd);
System.out.printf(" 系统设计: %s → %s (%d 周)\n",
requirementEnd.format(dateFmt),
designEnd.format(dateFmt),
designPeriod.getDays() / 7);
// 开发阶段:8周
LocalDate devEnd = designEnd.plusWeeks(8);
System.out.printf(" 开发编码: %s → %s (8 周)\n",
designEnd.format(dateFmt),
devEnd.format(dateFmt));
// 测试阶段:4周
LocalDate testEnd = devEnd.plusWeeks(4);
System.out.printf(" 测试阶段: %s → %s (4 周)\n",
devEnd.format(dateFmt),
testEnd.format(dateFmt));
// 部署上线:1周
LocalDate deployEnd = testEnd.plusWeeks(1);
System.out.printf(" 部署上线: %s → %s (1 周)\n",
testEnd.format(dateFmt),
deployEnd.format(dateFmt));
// 总工期
Period totalPeriod = Period.between(projectStart, deployEnd);
System.out.printf("\n 总工期: %d 个月 %d 天 (共 %d 天)\n",
totalPeriod.getMonths(),
totalPeriod.getDays(),
ChronoUnit.DAYS.between(projectStart, deployEnd));
// ===== 3. TemporalAdjusters 使用 =====
System.out.println("\n【使用 TemporalAdjusters 计算关键日期】");
System.out.println("-".repeat(45));
// 找到上线日期所在月的最后一天
LocalDate lastDayOfMonth = deployEnd.with(TemporalAdjusters.lastDayOfMonth());
System.out.printf(" 部署所在月最后一天: %s\n", lastDayOfMonth.format(dateFmt));
// 找到测试阶段开始的第一个周一
LocalDate firstMonday = devEnd.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY));
System.out.printf(" 开发结束后第一个周一: %s\n", firstMonday.format(dateFmt));
// 下个季度第一天
LocalDate nextQuarter = deployEnd.with(new FirstDayOfQuarter());
System.out.printf(" 部署所在季度第一天: %s\n",
deployEnd.with(new FirstDayOfQuarter()).plusMonths(3).format(dateFmt));
// 本月第二个周五(版本发布日期候选)
LocalDate secondFriday = deployEnd.with(TemporalAdjusters.dayOfWeekInMonth(2, DayOfWeek.FRIDAY));
System.out.printf(" 本月第二个周五(候选发布日期): %s\n",
secondFriday.format(dateFmt));
// ===== 4. Duration 用时计算 =====
System.out.println("\n【工作时长计算(Duration)】");
// 大翔今天的工作时间
LocalTime workStart = LocalTime.of(9, 0);
LocalTime lunchStart = LocalTime.of(12, 0);
LocalTime lunchEnd = LocalTime.of(13, 30);
LocalTime workEnd = LocalTime.of(18, 30);
Duration morningDuration = Duration.between(workStart, lunchStart);
Duration afternoonDuration = Duration.between(lunchEnd, workEnd);
Duration totalWorkDuration = morningDuration.plus(afternoonDuration);
System.out.printf(" 上午工作时间: %s ~ %s = %d 小时\n",
workStart, lunchStart, morningDuration.toHours());
System.out.printf(" 午休时间: %s ~ %s = %d 分钟\n",
lunchStart, lunchEnd, Duration.between(lunchStart, lunchEnd).toMinutes());
System.out.printf(" 下午工作时间: %s ~ %s = %d 小时\n",
lunchEnd, workEnd, afternoonDuration.toHours());
System.out.printf(" 全天工作时长: %d 小时 %d 分钟\n",
totalWorkDuration.toHours(),
totalWorkDuration.toMinutesPart()); // JDK 9+ 才有 toMinutesPart()
// JDK 8 替代:
long totalMinutes = totalWorkDuration.toMinutes();
System.out.printf(" 全天工作时长: %d 小时 %d 分钟 (JDK8方式)\n",
totalMinutes / 60, totalMinutes % 60);
// ===== 5. 自定义 TemporalAdjuster:跳转到下一个工作日 =====
System.out.println("\n【自定义 TemporalAdjuster:工作日计算】");
// 假设测试结束日是周六,部署应该从下周一算起
LocalDate testEndFriday = LocalDate.of(2024, 9, 13); // 周五
LocalDate nextWorkingDay = testEndFriday.with(new NextWorkingDay());
System.out.printf(" 测试结束于周五 %s\n", testEndFriday.format(dateFmt));
System.out.printf(" 下一个工作日: %s\n", nextWorkingDay.format(dateFmt));
LocalDate testEndSaturday = LocalDate.of(2024, 9, 14); // 周六
LocalDate nextWorkingDay2 = testEndSaturday.with(new NextWorkingDay());
System.out.printf(" 若结束于周六 %s\n", testEndSaturday.format(dateFmt));
System.out.printf(" 下一个工作日: %s\n", nextWorkingDay2.format(dateFmt));
// ===== 6. 版本发布时间 =====
System.out.println("\n【版本发布时间线】");
System.out.println("-".repeat(45));
String[] versions = {"v1.0 Alpha", "v1.0 Beta", "v1.0 RC", "v1.0 GA"};
LocalDate baseDate = LocalDate.of(2024, 3, 15);
for (int i = 0; i < versions.length; i++) {
LocalDate releaseDate = baseDate.plusMonths(i * 2);
// 总是发布在周五(或最近的工作日)
LocalDate adjustedDate = releaseDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.FRIDAY));
System.out.printf(" %-12s 计划发布: %s\n",
versions[i], adjustedDate.format(dateFmt));
}
// ===== 7. 不可变性证明 =====
System.out.println("\n【不可变性证明】");
LocalDate original = LocalDate.of(2024, 3, 15);
LocalDate modified = original.plusDays(10);
System.out.println(" original.plusDays(10):");
System.out.println(" original = " + original); // 2024-03-15(不变!)
System.out.println(" modified = " + modified); // 2024-03-25(新对象)
System.out.println("\n========== 项目计划生成完毕 ==========");
}
}
运行输出:
========== 飞翔科技项目里程碑系统 ==========
【项目启动日期】2024年03月15日 (星期五)
【里程碑计划(使用 plus/Period)】
---------------------------------------------
需求分析: 2024年03月15日 (星期五) → 2024年03月29日 (星期五) (14 天)
系统设计: 2024年03月29日 (星期五) → 2024年04月19日 (星期五) (3 周)
开发编码: 2024年04月19日 (星期五) → 2024年06月14日 (星期五) (8 周)
测试阶段: 2024年06月14日 (星期五) → 2024年07月12日 (星期五) (4 周)
部署上线: 2024年07月12日 (星期五) → 2024年07月19日 (星期五) (1 周)
总工期: 4 个月 4 天 (共 126 天)
【使用 TemporalAdjusters 计算关键日期】
---------------------------------------------
部署所在月最后一天: 2024年07月31日 (星期三)
开发结束后第一个周一: 2024年06月17日 (星期一)
部署所在季度第一天: 2024年10月01日 (星期二)
本月第二个周五(候选发布日期): 2024年07月12日 (星期五)
【工作时长计算(Duration)】
上午工作时间: 09:00 ~ 12:00 = 3 小时
午休时间: 12:00 ~ 13:30 = 90 分钟
下午工作时间: 13:30 ~ 18:30 = 5 小时
全天工作时长: 8 小时 0 分钟
全天工作时长: 8 小时 0 分钟 (JDK8方式)
【自定义 TemporalAdjuster:工作日计算】
测试结束于周五 2024年09月13日 (星期五)
下一个工作日: 2024年09月16日 (星期一)
若结束于周六 2024年09月14日 (星期六)
下一个工作日: 2024年09月16日 (星期一)
【版本发布时间线】
---------------------------------------------
v1.0 Alpha 计划发布: 2024年03月15日 (星期五)
v1.0 Beta 计划发布: 2024年05月17日 (星期五)
v1.0 RC 计划发布: 2024年07月19日 (星期五)
v1.0 GA 计划发布: 2024年09月20日 (星期五)
【不可变性证明】
original.plusDays(10):
original = 2024-03-15
modified = 2024-03-25
========== 项目计划生成完毕 ==========
七、易错场景
易错1:错误地将 LocalDateTime 用于时间戳存储
// ✗ 错误:LocalDateTime 不含时区,跨时区系统会出问题
LocalDateTime now = LocalDateTime.now(); // 取决于服务器时区
// 如果服务器从北京迁移到纽约,now 的含义完全不同!
// ✓ 正确:使用 Instant 存时间戳
Instant now = Instant.now(); // UTC 时间,全球唯一
// ✓ 或者:存带时区的时间
ZonedDateTime now = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
// 明确表达了"北京时间"
// 黄金法则:时间戳用 Instant,人类可读时间用 ZonedDateTime
易错2:Period.between 只返回日期差,不含时间
// ✗ 错误理解
LocalDateTime start = LocalDateTime.of(2024, 3, 15, 9, 0);
LocalDateTime end = LocalDateTime.of(2024, 3, 15, 18, 0);
Period period = Period.between(start.toLocalDate(), end.toLocalDate());
System.out.println(period.getDays()); // 输出: 0 ← 跨天才会计数!
// ✓ 正确:时间差用 Duration
Duration duration = Duration.between(start, end);
System.out.println(duration.toHours()); // 输出: 9
易错3:DateTimeFormatter 线程安全,SimpleDateFormat 不是
// ✗ 旧代码:SimpleDateFormat 线程不安全
private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd");
// 多线程同时调用 SDF.parse() → 数据错乱或 NumberFormatException
// ✓ 新代码:DateTimeFormatter 线程安全
private static final DateTimeFormatter DTF =
DateTimeFormatter.ofPattern("yyyy-MM-dd");
// 多线程安全使用!
易错4:Month 枚举值不需要 +1
// ✗ 旧习惯:Date 月份从 0 开始,+1 是标准操作
// int month = new Date().getMonth() + 1; // 需要 +1
// ✓ java.time:月份自然表达
int month = LocalDate.now().getMonthValue(); // 3 就是 3 月
Month monthEnum = LocalDate.now().getMonth(); // Month.MARCH
// 不需要任何加减操作!
八、面试考点
考点1:java.util.Date 和 java.time.LocalDateTime 有什么区别?为什么 JDK 8 要引入新的日期 API?
答:
| 维度 | java.util.Date | java.time |
|---|---|---|
| 可变性 | 可变,线程不安全,可以 setTime() | 不可变,线程安全,所有操作返回新对象 |
| 语义 | 混乱——名为 Date 但包含时间;月份 0-based,年份 1900-based | 清晰——LocalDate 只有日期,LocalTime 只有时间 |
| 时区 | Date 本身无时区概念,toString() 默认系统时区 | 明确区分 LocalDate(无时区)、ZonedDateTime(有时区) |
| 格式化 | SimpleDateFormat 线程不安全 | DateTimeFormatter 线程安全 |
| 精度 | 毫秒 | 纳秒(9位小数秒) |
| 日期运算 | 需借助 Calendar.add(),代码繁琐 | 自带 plus/minus/with,链式调用 |
引入原因:JSR 310 规范,由 Joda-Time 作者 Stephen Colebourne 主导设计,解决了旧 API 的几乎所有痛点。
考点2:Period 和 Duration 的区别?各自什么场景使用?
答:
| Period | Duration | |
|---|---|---|
| 维度 | 日期层面(年、月、日) | 时间层面(秒、纳秒) |
| ISO 表示 | P1Y2M3D | PT8H30M |
| 适用类型 | LocalDate | LocalTime, LocalDateTime, Instant |
| 典型场景 | "距离项目截止还有 2 个月 10 天" | "服务器响应时间:350ms" |
| 不可混合 | 不能用于 LocalTime | 不能直接用于 LocalDate |
Period.between(LocalDate.of(2024,1,1), LocalDate.of(2024,3,15)) // P2M14D
Duration.between(LocalTime.of(9,0), LocalTime.of(17,30)) // PT8H30M
考点3:ZonedDateTime、OffsetDateTime、Instant 三者什么关系?
答:
- Instant:UTC 时间线上的一个点(纳秒精度),类似于旧 API 的
Date.getTime()长整数值。适合作为系统内部的时间戳。 - OffsetDateTime:Instant + ZoneOffset(如 +08:00)。适合需要记录"UTC偏移但不需要完整时区规则"的场景,如数据库 JDBC 映射。
- ZonedDateTime:Instant + ZoneId(如 Asia/Shanghai)。包含完整的时区规则(包括夏令时切换历史),适合面向用户的展示。
转换关系:
Instant → atOffset(ZoneOffset) → OffsetDateTime
Instant → atZone(ZoneId) → ZonedDateTime
ZonedDateTime → toOffsetDateTime() → OffsetDateTime
ZonedDateTime → toInstant() → Instant
考点4:SimpleDateFormat 为什么线程不安全?DateTimeFormatter 如何保证线程安全?
答:
- SimpleDateFormat 线程不安全原因:内部维护了一个
Calendar对象作为工作区(calendar字段),parse()和format()都会修改这个共享的 Calendar 状态。多线程并发调用会导致数据覆盖。 - DateTimeFormatter 线程安全原因:它是不可变对象。格式化时不会修改自身状态,而是创建临时的格式化上下文。所有字段都是 final 的,解析状态在方法栈上而非对象内部。
- 因此:
DateTimeFormatter可以安全地声明为static final常量,而SimpleDateFormat必须使用ThreadLocal或每次创建新实例。
Frank 总监合上投影仪:"总结一下。新日期时间 API 的三大优势:不可变(线程安全)、语义清晰(LocalDate ≠ LocalDateTime)、功能完整(时区/周期/格式化一把梭)。"
大翔感叹:"早该这样了。我以前处理跨时区项目时,Date 转来转去总是出错。"
白歌笑道:"ZonedDateTime 一行代码搞定时区转换,这就是 API 设计的魅力。"
小崔突然发问:"那如果项目还是 JDK 7,能用 java.time 吗?"
朱璐回答:"可以用 ThreeTen-Backport 库——它是
java.time的向后移植版,API 几乎一模一样。甚至还有 Android 版——ThreeTenABP。"大翔若有所思:"怪不得 Google 推荐 Android 项目用 ThreeTenABP 而不是旧 Date...原来 JDK 8 的设计如此超前。"
李眉(财务)走进来:"你们讨论完了吗?财务系统要做下个财年的日期规划,能不能帮我算一下所有月末结算日是不是工作日?"
众人微笑:"用 TemporalAdjusters.lastDayOfMonth() + 自定义工作日调整器,十分钟搞定。"
JDK 8 java.time 常用速查
// === 获取当前时间 === LocalDate.now() // 当前日期 LocalTime.now() // 当前时间 LocalDateTime.now() // 当前日期时间 Instant.now() // UTC 时间戳 ZonedDateTime.now() // 当前带时区日期时间 // === 创建指定时间 === LocalDate.of(2024, 3, 15) // 2024-03-15 LocalTime.of(14, 30, 0) // 14:30:00 LocalDateTime.of(2024, 3, 15, 14, 30) // 2024-03-15T14:30 // === 日期运算 === today.plusDays(10) // 加10天 today.minusMonths(1) // 减1个月 today.with(TemporalAdjusters.firstDayOfMonth()) // 本月第一天 today.with(TemporalAdjusters.next(DayOfWeek.MONDAY)) // 下周一 // === 日期差 === Period.between(date1, date2) // 日期差 (年月日) Duration.between(time1, time2) // 时间差 (秒纳秒) ChronoUnit.DAYS.between(d1, d2) // 相差天数 // === 格式化 === DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") LocalDateTime.now().format(formatter) LocalDateTime.parse("2024-03-15 14:30", formatter) // === 时区转换 === ZonedDateTime.now(ZoneId.of("America/New_York")) zdt.withZoneSameInstant(ZoneId.of("Asia/Shanghai")) // 同一时刻不同时区