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

    • 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 详解

本章定位:全面掌握Java 8日期时间API体系。飞翔科技的黄俪负责考勤系统重构,面对项目中同时存在的java.util.Date旧代码和java.time新需求,她必须精通两套API及其互操作。本章从Date/Calendar基础出发,深入到JDK 8全新的java.time包,覆盖LocalDate、LocalTime、LocalDateTime、Instant、Duration、Period、DateTimeFormatter等核心类,帮助读者在真实项目中游刃有余地处理日期时间。


定义与语法

属性说明
java.util.DateJDK 1.0引入的传统日期时间类,表示时间戳(毫秒数),大多数方法已废弃
java.util.CalendarJDK 1.1引入的抽象日历类,用于日期运算,设计有缺陷(月份从0开始)
java.text.SimpleDateFormat传统日期格式化类,线程不安全,多线程环境需注意
java.time.LocalDateJDK 8新增,表示不含时间的日期(年-月-日),不可变、线程安全
java.time.LocalTimeJDK 8新增,表示不含日期的时间(时-分-秒-纳秒),不可变
java.time.LocalDateTimeJDK 8新增,表示日期+时间(不含时区),最常用的本地日期时间类
java.time.InstantJDK 8新增,表示UTC时间线上的一个瞬时点,精确到纳秒
java.time.DurationJDK 8新增,表示基于时间的量(秒+纳秒),如"36.5秒"
java.time.PeriodJDK 8新增,表示基于日期的量(年+月+日),如"2年3个月4天"
java.time.format.DateTimeFormatterJDK 8新增,线程安全的日期时间格式化器
java.time.ZoneIdJDK 8新增,时区标识符,如"Asia/Shanghai"
java.time.temporal.ChronoUnitJDK 8新增,日期时间单位枚举,如DAYS、HOURS、MINUTES
ISO-8601国际日期时间标准格式,java.time遵循此标准

设计哲学对比:旧API(Date/Calendar)是可变、非线程安全、设计混乱的;新API(java.time)是不可变、线程安全、职责清晰、符合ISO标准的。JDK 8项目中两者并存,掌握互操作至关重要。


完整代码示例

示例一:黄俪使用Date/Calendar处理飞翔科技传统考勤数据

飞翔科技的老考勤系统仍在使用java.util.Date存储打卡记录。黄俪需要解析这些历史数据,计算员工的工作天数。

/** 飞翔科技 - 使用传统Date/Calendar API处理历史考勤数据 */
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

public class LegacyAttendanceDemo {
    public static void main(String[] args) throws Exception {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        // 模拟从数据库中读取的员工打卡记录
        Date clockIn10001  = sdf.parse("2024-03-18 08:55:30");  // 工号10001 赵鸣
        Date clockOut10001 = sdf.parse("2024-03-18 18:05:12");
        Date clockIn10002  = sdf.parse("2024-03-18 09:12:45");  // 工号10002 孙鹤
        Date clockOut10002 = sdf.parse("2024-03-18 17:48:30");

        System.out.println("========== 飞翔科技考勤记录(旧版Date) ==========");
        System.out.println("工号10001 赵鸣  签到:" + sdf.format(clockIn10001));
        System.out.println("              签退:" + sdf.format(clockOut10001));
        System.out.println("工号10002 孙鹤  签到:" + sdf.format(clockIn10002));
        System.out.println("              签退:" + sdf.format(clockOut10002));

        // 计算工作时长(毫秒差 → 小时)
        long workMs10001 = clockOut10001.getTime() - clockIn10001.getTime();
        double workHours10001 = workMs10001 / (1000.0 * 60 * 60);
        System.out.printf("\n工号10001 赵鸣  工作时长:%.2f 小时\n", workHours10001);

        // 使用Calendar进行日期运算:计算本月剩余工作日
        Calendar cal = Calendar.getInstance();
        cal.setTime(new Date());
        int today = cal.get(Calendar.DAY_OF_MONTH);
        int maxDay = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
        System.out.println("本月已过天数:" + today + " 天,本月共 " + maxDay + " 天");
        System.out.println("本月剩余天数:" + (maxDay - today) + " 天");

        // Calendar的陷阱:月份从0开始!
        cal.set(2024, 2, 18);  // 注意:2代表3月!
        System.out.println("\n【注意】Calendar月份从0开始:");
        System.out.println("  cal.set(2024, 2, 18) → 实际日期:" + sdf.format(cal.getTime()));
    }
}
========== 飞翔科技考勤记录(旧版Date) ==========
工号10001 赵鸣  签到:2024-03-18 08:55:30
              签退:2024-03-18 18:05:12
工号10002 孙鹤  签到:2024-03-18 09:12:45
              签退:2024-03-18 17:48:30

工号10001 赵鸣  工作时长:9.16 小时
本月已过天数:18 天,本月共 31 天
本月剩余天数:13 天

【注意】Calendar月份从0开始:
  cal.set(2024, 2, 18) → 实际日期:2024-03-18 00:00:00

示例二:大翔用java.time实现飞翔科技项目截止日期倒计时

飞翔科技的核心项目"云鹤平台"定于2025年6月30日交付。大翔需要精确计算剩余天数,并为每个里程碑生成倒计时。

/** 飞翔科技 - 使用JDK 8 java.time实现项目截止日期倒计时 */
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Period;
import java.time.Duration;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;

public class ProjectDeadlineDemo {
    public static void main(String[] args) {
        // 项目交付日期
        LocalDate deadline = LocalDate.of(2025, 6, 30);
        LocalDate today = LocalDate.now();

        System.out.println("========== 飞翔科技「云鹤平台」项目倒计时 ==========");
        System.out.println("项目交付日期:" + deadline);
        System.out.println("当前日期    :" + today);

        // 使用Period计算日期差
        Period period = Period.between(today, deadline);
        System.out.println("\n距交付还剩:"
                + period.getYears()  + " 年 "
                + period.getMonths() + " 月 "
                + period.getDays()   + " 天");

        // 使用ChronoUnit精确计算总天数
        long totalDays = ChronoUnit.DAYS.between(today, deadline);
        long totalWeeks = ChronoUnit.WEEKS.between(today, deadline);
        System.out.println("精确剩余:" + totalDays + " 天(约 " + totalWeeks + " 周)");

        // 里程碑时间计算
        LocalDate milestoneCodeFreeze = deadline.minusWeeks(4);   // 代码冻结
        LocalDate milestoneTestStart  = deadline.minusMonths(2);  // 测试启动
        LocalDate milestoneDevStart    = LocalDate.of(2024, 3, 1); // 开发启动

        System.out.println("\n--- 里程碑节点 ---");
        System.out.println("开发启动:" + milestoneDevStart + "  (" 
                + ChronoUnit.DAYS.between(milestoneDevStart, deadline) + "天总周期)");
        System.out.println("测试启动:" + milestoneTestStart);
        System.out.println("代码冻结:" + milestoneCodeFreeze);
        System.out.println("正式交付:" + deadline);

        // 使用DateTimeFormatter格式化输出
        DateTimeFormatter cnFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
        System.out.println("\n--- 格式化的里程碑 ---");
        System.out.println("开发启动:" + milestoneDevStart.format(cnFormatter));
        System.out.println("测试启动:" + milestoneTestStart.format(cnFormatter));
        System.out.println("代码冻结:" + milestoneCodeFreeze.format(cnFormatter));
        System.out.println("正式交付:" + deadline.format(cnFormatter));
    }
}
========== 飞翔科技「云鹤平台」项目倒计时 ==========
项目交付日期:2025-06-30
当前日期    :2024-12-20

距交付还剩:0 年 6 月 10 天
精确剩余:192 天(约 27 周)

--- 里程碑节点 ---
开发启动:2024-03-01  (486天总周期)
测试启动:2025-04-30
代码冻结:2025-06-02
正式交付:2025-06-30

--- 格式化的里程碑 ---
开发启动:2024年03月01日
测试启动:2025年04月30日
代码冻结:2025年06月02日
正式交付:2025年06月30日

示例三:白歌处理飞翔科技员工入职周年纪念

白歌需要为每位员工计算入职周年纪念日,并判断当天是否需要提醒HR准备庆祝活动。

/** 飞翔科技 - 使用java.time计算员工入职周年纪念 */
import java.time.LocalDate;
import java.time.Month;
import java.time.Period;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;

class EmployeeAnniversary {
    long    employeeId;
    String  name;
    LocalDate hireDate;

    EmployeeAnniversary(long employeeId, String name, LocalDate hireDate) {
        this.employeeId = employeeId;
        this.name = name;
        this.hireDate = hireDate;
    }
}

public class AnniversaryDemo {
    public static void main(String[] args) {
        List<EmployeeAnniversary> employees = new ArrayList<>();
        employees.add(new EmployeeAnniversary(10001, "赵鸣", LocalDate.of(2015, 4, 15)));
        employees.add(new EmployeeAnniversary(10002, "孙鹤", LocalDate.of(2018, 12, 1)));
        employees.add(new EmployeeAnniversary(10003, "高英", LocalDate.of(2020, 6, 30)));
        employees.add(new EmployeeAnniversary(10004, "杨英", LocalDate.of(2022, 3, 10)));

        LocalDate today = LocalDate.of(2024, 12, 20);

        System.out.println("========== 飞翔科技员工入职周年纪念 ==========");
        System.out.println("当前日期:" + today);
        System.out.println();

        DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy年MM月dd日");

        for (EmployeeAnniversary emp : employees) {
            Period service = Period.between(emp.hireDate, today);
            long totalMonths = ChronoUnit.MONTHS.between(emp.hireDate, today);
            LocalDate nextAnniversary = emp.hireDate.withYear(today.getYear());

            // 如果今年的纪念日已过,计算明年的
            if (nextAnniversary.isBefore(today)) {
                nextAnniversary = nextAnniversary.plusYears(1);
            }
            long daysUntil = ChronoUnit.DAYS.between(today, nextAnniversary);

            System.out.println("工号 " + emp.employeeId + " " + emp.name);
            System.out.println("  入职日期:" + emp.hireDate.format(fmt));
            System.out.println("  服务年限:" + service.getYears() + "年"
                    + service.getMonths() + "个月(共" + totalMonths + "个月)");
            System.out.println("  下次周年纪念:" + nextAnniversary.format(fmt)
                    + "(还有 " + daysUntil + " 天)");

            // 判断是否需要提前准备
            if (daysUntil <= 30 && daysUntil > 0) {
                System.out.println("  【提醒】距离纪念日不足30天,请HR准备庆祝活动!");
            }
            System.out.println();
        }
    }
}
========== 飞翔科技员工入职周年纪念 ==========
当前日期:2024-12-20

工号 10001 赵鸣
  入职日期:2015年04月15日
  服务年限:9年8个月(共116个月)
  下次周年纪念:2025年04月15日(还有 116 天)

工号 10002 孙鹤
  入职日期:2018年12月01日
  服务年限:6年0个月(共72个月)
  下次周年纪念:2024年12月01日(还有 -19 天)

工号 10003 高英
  入职日期:2020年06月30日
  服务年限:4年5个月(共53个月)
  下次周年纪念:2025年06月30日(还有 192 天)

工号 10004 杨英
  入职日期:2022年03月10日
  服务年限:2年9个月(共33个月)
  下次周年纪念:2025年03月10日(还有 80 天)

示例四:李眉用Instant和Duration处理飞翔科技API超时监控

李眉负责飞翔科技微服务网关,需要对API调用进行精确的时间度量。Instant和Duration提供了纳秒精度的时间计算能力。

/** 飞翔科技 - 使用Instant和Duration进行API调用时间度量 */
import java.time.Instant;
import java.time.Duration;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ThreadLocalRandom;

public class ApiTimeoutMonitor {
    public static void main(String[] args) throws Exception {
        DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");

        System.out.println("========== 飞翔科技 API 超时监控 ==========");

        // 模拟三次API调用
        for (int i = 1; i <= 3; i++) {
            Instant start = Instant.now();

            // 模拟API处理耗时(100-800毫秒随机)
            long simulatedMs = ThreadLocalRandom.current().nextLong(100, 801);
            Thread.sleep(simulatedMs);

            Instant end = Instant.now();
            Duration elapsed = Duration.between(start, end);

            // 转换为北京时间显示
            ZonedDateTime beijingStart = start.atZone(ZoneId.of("Asia/Shanghai"));
            ZonedDateTime beijingEnd   = end.atZone(ZoneId.of("Asia/Shanghai"));

            System.out.println("\n--- API调用 #" + i + " ---");
            System.out.println("  开始时间(UTC):" + start);
            System.out.println("  开始时间(北京):" + beijingStart.format(fmt));
            System.out.println("  结束时间(北京):" + beijingEnd.format(fmt));
            System.out.println("  耗时:" + elapsed.toMillis() + " 毫秒");
            System.out.println("  耗时(纳秒):" + elapsed.toNanos());

            // 超时判定(阈值500毫秒)
            if (elapsed.toMillis() > 500) {
                System.out.println("  【警告】调用超时!阈值500ms,实际" + elapsed.toMillis() + "ms");
            } else {
                System.out.println("  【正常】耗时在阈值范围内");
            }
        }

        // Duration的解析与运算
        Duration timeout = Duration.ofSeconds(3).plusMillis(500);
        System.out.println("\n--- 系统超时配置 ---");
        System.out.println("  全局超时时间:" + timeout.toMillis() + " 毫秒");
        System.out.println("  即 " + timeout.getSeconds() + " 秒 + "
                + timeout.getNano() / 1_000_000 + " 毫秒");
    }
}
========== 飞翔科技 API 超时监控 ==========

--- API调用 #1 ---
  开始时间(UTC):2024-12-20T10:30:15.123456Z
  开始时间(北京):2024-12-20 18:30:15.123
  结束时间(北京):2024-12-20 18:30:15.389
  耗时:266 毫秒
  耗时(纳秒):266000000
  【正常】耗时在阈值范围内

--- API调用 #2 ---
  开始时间(UTC):2024-12-20T10:30:15.390Z
  ...
  耗时:612 毫秒
  【警告】调用超时!阈值500ms,实际612ms

--- 系统超时配置 ---
  全局超时时间:3500 毫秒
  即 3 秒 + 500 毫秒

深度原理分析

一、java.time类体系架构

JDK 8的java.time包设计精良,类职责清晰。以下是核心类的层次关系:

关键设计原则:

  • 不可变性:所有核心类都是final且不可变的,线程安全,可放心共享。
  • 职责分离:LocalDate只管日期,LocalTime只管时间,LocalDateTime管两者,各司其职。
  • 工厂方法:统一使用now()和of(...)创建实例,不用new(构造器私有)。
  • 流畅API:plusDays().minusMonths().withYear() 链式调用,返回新实例。

二、Date/Calendar与java.time互操作

在JDK 8项目中,新旧API互操作是必备技能:

三、线程安全性对比

这是新旧API最关键的差异之一:

// ❌ SimpleDateFormat线程不安全——多线程下可能崩溃或产生错误结果
// 典型错误:java.lang.NumberFormatException: multiple points

// ✅ DateTimeFormatter线程安全——可在多线程中安全共享
private static final DateTimeFormatter FMT =
    DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 可以放心作为静态常量在所有线程中使用

四、Period vs Duration 的选择

  • Period:基于日期(年/月/日),用于LocalDate之间的差值。如"距离入职纪念还有3个月5天"。
  • Duration:基于时间(秒/纳秒),用于Instant、LocalTime之间的差值。如"API调用耗时350毫秒"。

两者不可混用:Duration.between(localDate1, localDate2) 会抛异常,因为LocalDate不包含时间信息。


易错场景

反例一:❌ 使用Calendar的月份直接赋值

// ❌ Calendar月份从0开始!2代表3月
Calendar cal = Calendar.getInstance();
cal.set(2024, 2, 15);  // 你以为设置的是2月15日,实际是3月15日!
System.out.println(cal.getTime());  // 输出:Sat Mar 15 ...
// ✅ 使用Calendar.MARCH常量,或直接用LocalDate
Calendar cal = Calendar.getInstance();
cal.set(2024, Calendar.MARCH, 15);  // 明确使用常量

// ✅ 更好的方式:使用LocalDate
LocalDate date = LocalDate.of(2024, 3, 15);  // 月份就是3,直观明了

反例二:❌ SimpleDateFormat多线程共享

// ❌ 致命错误!SimpleDateFormat不是线程安全的
public class DateUtils {
    private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd");
    // 多线程同时调用 → 数据错乱、NumberFormatException异常!
}
// ✅ 方案一:每次new(简单但不高效)
public static String format(Date date) {
    return new SimpleDateFormat("yyyy-MM-dd").format(date);
}

// ✅ 方案二:使用ThreadLocal
private static final ThreadLocal<SimpleDateFormat> THREAD_SDF =
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

// ✅ 方案三(推荐):使用DateTimeFormatter
private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");

反例三:❌ LocalDateTime与Instant的时区混淆

// ❌ 错误:试图直接比较或混合LocalDateTime和Instant
LocalDateTime local = LocalDateTime.now();
Instant instant = Instant.now();
// local.equals(instant);  // 编译错误:不同类型!

// ❌ 常见误解:以为LocalDateTime.now()是UTC时间
LocalDateTime now = LocalDateTime.now();  // 实际是系统默认时区的本地时间
// ✅ 正确处理时区转换
// LocalDateTime → Instant:需要指定时区
ZonedDateTime zoned = LocalDateTime.now().atZone(ZoneId.of("Asia/Shanghai"));
Instant instant = zoned.toInstant();

// Instant → LocalDateTime:需要转换为某个时区
LocalDateTime local = LocalDateTime.ofInstant(instant, ZoneId.of("Asia/Shanghai"));

// 直接获取UTC时间
Instant utcNow = Instant.now();  // 这才是UTC瞬时点

反例四:❌ Period的规范化陷阱

// ❌ Period.between的结果不是规范化的!
LocalDate start = LocalDate.of(2024, 1, 31);
LocalDate end   = LocalDate.of(2024, 3, 1);
Period p = Period.between(start, end);
// 结果:P1M-29D(1个月负29天),而非期望的P1M1D
System.out.println(p.getMonths() + "月" + p.getDays() + "天");  // 输出"1月-29天"
// ✅ 使用ChronoUnit精确计算总天数
long days = ChronoUnit.DAYS.between(start, end);  // 30天
long months = ChronoUnit.MONTHS.between(start, end);  // 1个月(向下取整)

// ✅ 如果需要规范化的Period,先规范化
Period normalized = p.normalized();  // 但normalized()只规范化月份到年,不规范化天
// 最佳实践:使用ChronoUnit进行精确计算,Period主要用于"人类可读"的场景

面试考点

Q1: SimpleDateFormat是线程安全的吗?为什么?如何解决?

答:SimpleDateFormat不是线程安全的。原因是其内部维护了一个Calendar对象作为实例变量,在format/parse过程中会修改这个共享的Calendar状态。多线程并发调用时,A线程的format操作可能被B线程的parse操作打断,导致数据错乱甚至抛出NumberFormatException。

解决方案(三选一):

  1. 每次new:在方法内创建局部SimpleDateFormat实例(简单但有GC压力)。
  2. ThreadLocal:为每个线程维护独立的SimpleDateFormat实例。
  3. 最佳方案:使用JDK 8的DateTimeFormatter,它是不可变且线程安全的。

Q2: LocalDateTime和Instant有什么区别?什么场景用哪个?

答:

  • LocalDateTime:表示本地日期时间,不包含时区信息。适用于不需要关心时区的场景,如"系统日志时间"、"本地业务截止日期"。它是"墙上时钟时间"的抽象。
  • Instant:表示UTC时间线上的一个瞬时点,精确到纳秒。适用于需要跨时区统一时间戳的场景,如"数据库timestamp"、"API调用时间记录"、"消息队列时间戳"。

转换关系:LocalDateTime + ZoneId = ZonedDateTime → toInstant() → Instant。反过来,Instant + ZoneId → ZonedDateTime → toLocalDateTime()。

Q3: Period和Duration的核心区别是什么?

答:

  • Period 是以日期单位(年、月、日)度量的时间量,基于ISO-8601的日期系统。使用Period.between(LocalDate, LocalDate)计算,适合计算"两个日期之间隔了多少年多少月多少天"。
  • Duration 是以时间单位(秒、纳秒)度量的时间量,精确到纳秒。使用Duration.between(Temporal, Temporal)计算(参数需支持秒精度),适合计算"两个时间点之间精确间隔了多少毫秒/纳秒"。
  • 记忆口诀:Period管"年月日"(日历概念),Duration管"时分秒"(时钟概念)。给Period一个LocalDate,给Duration一个Instant。

Q4: JDK 8的java.time相比Date/Calendar有哪些改进?

答:

  1. 不可变性:所有对象不可变,线程安全,无需防御性拷贝。
  2. API清晰:Date既表示日期又表示时间,职责混淆;java.time严格分离LocalDate/LocalTime/LocalDateTime。
  3. 月份从1开始:LocalDate.of(2024, 1, 15)就是1月,告别Calendar的"月份从0开始"的痛苦。
  4. 时间运算流畅:date.plusDays(7).minusMonths(1) 链式调用,清晰直观。
  5. 时区处理:ZoneId/ZonedDateTime体系远比TimeZone设计优雅。
  6. 格式化线程安全:DateTimeFormatter不可变,替代了不安全的SimpleDateFormat。
  7. ISO-8601标准:全面遵循国际标准,减少歧义。

黄俪总结道:"掌握了java.time,就告别了Date的泥潭。但现实项目中新老API并存,互操作的桥梁方法——Date.toInstant()和Date.from(Instant)——也必须熟记于心。"

上一页
大数值运算 详解