乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

新日期时间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:00ZUTC
Period日期差(年、月、日)P1Y2M3D (1年2月3天)-
Duration时间差(秒、纳秒)PT2H30M (2小时30分钟)-
ZoneId时区标识Asia/Shanghai, America/New_York-
ZoneOffsetUTC 偏移量+08:00, -05:00-
DateTimeFormatter格式化器(线程安全)yyyy-MM-dd HH:mm:ss-
TemporalAdjusters时间调节器工厂firstDayOfMonth(), next(DayOfWeek.MONDAY)-

1.2 新旧API对比

特性java.util.Date/Calendarjava.time (JDK 8)
可变性可变(线程不安全)不可变(线程安全)
API 设计反直觉(月份从0开始,年份从1900开始)语义清晰(month=3 就是3月)
时区处理混乱(Date 无时区概念)明确分为 Local/Zoned/Offset
格式化SimpleDateFormat(线程不安全)DateTimeFormatter(线程安全)
日期运算需要 Calendar 的 add/rollplusDays(), 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.Datejava.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 的区别?各自什么场景使用?

答:

PeriodDuration
维度日期层面(年、月、日)时间层面(秒、纳秒)
ISO 表示P1Y2M3DPT8H30M
适用类型LocalDateLocalTime, 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")) // 同一时刻不同时区
上一页
Optional 详解