JDK8新特性-Date-Time API

发布时间:2026/7/30 18:19:06
JDK8新特性-Date-Time API Java 8 Date-Time API 详解从 JDK7 的混乱到新 API 的清晰一、JDK7 及之前我们处理日期时间有多痛苦在 JDK7 时代处理日期时间主要依赖java.util.Date和java.util.Calendar。这两个类的设计存在严重缺陷导致开发中频繁出错。1.1java.util.Date的设计缺陷缺陷一命名和实际功能不符Date这个名字听起来只应该表示日期但它实际上存储的是日期 时间 毫秒的完整时间戳。// JDK7 Date now new Date(); // 这里面有年月日时分秒毫秒不只是日期缺陷二年份计算反人类Date的构造函数要求年份减去 1900// 创建 2024年1月1日却要这样写 Date date new Date(2024 - 1900, 0, 1); // 第一个参数是减去1900后的值为什么要这样因为Date内部用一个long毫秒值存储为了兼容旧系统构造函数保留了这种从 1900 年偏移的设计。这导致每个开发者都得在心里做减法。缺陷三月份从 0 开始Date date new Date(2024 - 1900, 0, 1); // 0 代表一月11 代表十二月计算机里数组从 0 开始可以理解但日历月份从 0 开始完全违背人类直觉。每年因为new Date(2024, 1, 1)被误当成1月1日实际上是2月1日而产生的 bug 不计其数。缺陷四对象是可变的Mutable—— 现成不安全的Date date new Date(); date.setTime(System.currentTimeMillis() 86400000); // 直接修改了对象内部状态这意味着你把一个Date对象传给另一个方法后那个方法可能偷偷修改它。在多线程环境下这会导致不可预期的结果。不可预测的可变状态是并发 bug 的主要来源之一。缺陷五toString()输出混乱且带隐式时区Date now new Date(); System.out.println(now); // 输出类似Wed Jul 30 10:44:00 CST 2026注意这个CST中国标准时间。但Date对象内部存储的其实是 UTC 毫秒数toString()却偷偷用了系统默认时区来格式化。这导致很多开发者误以为Date对象包含时区信息——实际上它并不包含。1.2java.util.Calendar的问题Calendar是为了弥补Date的不足而设计的但它同样糟糕// 获取当前年份 Calendar cal Calendar.getInstance(); int year cal.get(Calendar.YEAR); // 还好年份是正常的 // 获取月份 int month cal.get(Calendar.MONTH); // 又是从 0 开始0一月 // 设置日期 cal.set(Calendar.DAY_OF_MONTH, 32); // 如果当月没有32号会自动进位到下个月 // 但很多时候这不是你想要的行为它静默地纠正了你的错误输入Calendar也是可变的同样存在线程安全问题。而且它的 API 设计非常啰嗦完成一个简单的日期加减需要写很多行代码。1.3SimpleDateFormat的线程安全灾难格式化日期在 JDK7 里用SimpleDateFormatSimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd); // 单线程下没问题 String str sdf.format(new Date()); // 但如果你把它定义成 static 共享变量多线程同时访问时 // 会抛出各种异常或者输出完全错误的结果SimpleDateFormat内部维护了一个共享的日历对象多线程并发修改时就会互相干扰。这意味着每个线程都得自己创建实例或者用ThreadLocal包装增加了不必要的复杂度。1.4 时区处理几乎没有清晰的 APIJDK7 的Date不保存时区信息但它却依赖系统默认时区来显示。如果你需要处理纽约时间 2024年1月1日 12:00这样的概念没有直接的类可以表达只能手动计算毫秒偏移量极易出错。二、Java 8 Date-Time API 的设计思路Java 8 引入了全新的java.time包JSR-310它完全抛弃了Date和Calendar的设计基于以下核心原则重新构建原则 1不可变性Immutable所有核心类都是不可变的。一旦创建任何修改操作都会返回一个全新的对象原对象保持不变。LocalDate date LocalDate.of(2024, 7, 30); LocalDate nextDay date.plusDays(1); // date 本身还是 7月30日nextDay 是新的 7月31日为什么这样设计因为日期时间是一种值类型就像String或Integer一样修改它的值在语义上应该是创建一个新值而不是改变原来的值。不可变性天然保证了线程安全不需要额外的同步措施。原则 2职责分离不再用一个类包揽一切而是把不同的概念拆分成不同的类类职责LocalDate只表示日期年月日没有时间没有时区LocalTime只表示时间时分秒纳秒没有日期没有时区LocalDateTime日期 时间不带时区Instant时间戳距离 1970-01-01T00:00:00Z 的纳秒数机器视角ZonedDateTime日期 时间 时区完整的时间点Duration两个时间点之间的间隔基于时间的量如秒、纳秒Period两个日期之间的间隔基于日期的量如年、月、日原则 3符合人类直觉的索引月份从1开始1一月12十二月。年份就是实际年份不需要减去 1900。原则 4显式处理时区时区不再是隐式的系统默认设置而是显式的ZoneId对象你必须明确指定时区或者明确声明我不关心时区。三、核心类详解3.1LocalDate纯日期表示一个不带时间的日期比如2024年7月30日。// 创建方式 LocalDate today LocalDate.now(); // 当前日期 LocalDate specific LocalDate.of(2024, 7, 30); // 指定日期 LocalDate parsed LocalDate.parse(2024-07-30); // 从字符串解析默认格式 ISO-8601 // 获取字段 int year today.getYear(); // 2024 int month today.getMonthValue(); // 7从1开始 int day today.getDayOfMonth(); // 30 // 没有 setYear()不可变对象不允许修改 // 要用 withXxx() 创建新对象 LocalDate modified today.withYear(2025); // 返回 2025-07-30today 本身不变 // 日期加减 LocalDate tomorrow today.plusDays(1); LocalDate lastMonth today.minusMonths(1); LocalDate nextWeek today.plusWeeks(1); // 比较 boolean isBefore today.isBefore(tomorrow); // true boolean isAfter today.isAfter(tomorrow); // false设计意图LocalDate专门处理生日、保质期、合同签订日这类只关心日期、不关心几点几分的场景。它明确告诉你这里没有时间也没有时区。3.2LocalTime纯时间表示一天中的一个时刻比如14:30:00不带日期不带时区。LocalTime now LocalTime.now(); // 当前时间 LocalTime specific LocalTime.of(14, 30, 0); // 14:30:00 LocalTime parsed LocalTime.parse(14:30:00); LocalTime later now.plusHours(2); // 加2小时 LocalTime truncated now.withMinute(0).withSecond(0); // 整点3.3LocalDateTime日期 时间但仍不带时区LocalDateTime now LocalDateTime.now(); // 当前日期时间 LocalDateTime specific LocalDateTime.of(2024, 7, 30, 14, 30, 0); // 从 LocalDate 和 LocalTime 组合 LocalDate date LocalDate.of(2024, 7, 30); LocalTime time LocalTime.of(14, 30); LocalDateTime dt LocalDateTime.of(date, time);关键点LocalDateTime仍然不携带时区信息。它表示的是2024年7月30日14点30分这个本地视角的概念就像你在日历上写下的一个约定但没有说明这是北京时间、纽约时间还是伦敦时间。如果你需要明确的全球唯一时间点比如记录服务器日志、记录事件发生时刻不能用LocalDateTime要用Instant或ZonedDateTime。3.4Instant机器时间戳Instant表示的是 UTC 时间轴上的一个绝对点精确到纳秒。它等价于旧的System.currentTimeMillis()但更精确API 更清晰。Instant now Instant.now(); // 获取当前 UTC 时间戳 // 加减 Instant later now.plusSeconds(3600); // 1小时后 Instant earlier now.minusMillis(500); // 500毫秒前 // 获取毫秒数兼容旧系统 long millis now.toEpochMilli(); // 从旧 Date 转换 Date oldDate new Date(); Instant instant oldDate.toInstant(); // 转回旧 Date Date back Date.from(instant);设计意图Instant是没有人类可读格式的纯机器时间。它最适合记录事件发生顺序、计算超时、打日志时间戳。因为它基于 UTC所以全球任何服务器上的Instant.now()在理论上应该是相同的忽略时钟差异。3.5ZonedDateTime带时区的完整时间当你确实需要表达北京时间 2024年7月30日 14:30这个完整概念时// 当前系统默认时区的日期时间 ZonedDateTime now ZonedDateTime.now(); // 指定时区 ZonedDateTime nyTime ZonedDateTime.of( 2024, 7, 30, 14, 30, 0, 0, ZoneId.of(America/New_York) ); // 时区转换把纽约时间转成东京时间 ZonedDateTime tokyoTime nyTime.withZoneSameInstant(ZoneId.of(Asia/Tokyo));设计意图ZonedDateTime明确回答了三个问题什么时候日期时间、在哪里时区、对应全球时间轴上的哪个点通过 Instant 可以获取。时区转换也变得直接withZoneSameInstant()会自动处理夏令时等复杂规则。3.6Duration和Period两种不同的时间间隔这是 JDK7 最混乱的地方之一日期加减和时间段计算没有区分清楚。Java 8 把它们分开了Duration基于时间的精确间隔适合机器计算Instant start Instant.now(); // ... 执行一些操作 Instant end Instant.now(); Duration elapsed Duration.between(start, end); long millis elapsed.toMillis(); // 间隔多少毫秒 // 也可以直接构造 Duration twoHours Duration.ofHours(2);Period基于日历的日期间隔适合人类概念LocalDate startDate LocalDate.of(2024, 1, 1); LocalDate endDate LocalDate.of(2024, 7, 30); Period period Period.between(startDate, endDate); int months period.getMonths(); // 6 int days period.getDays(); // 29 // 表示1年2个月3天 Period p Period.of(1, 2, 3);为什么要分开因为1个月后对人类来说很明确但对机器来说不精确。1月31日加1个月是2月28日或29日这种日历逻辑和加 2,592,000 秒完全不同。Period保留年月日的语义Duration保留秒和纳秒的精确语义。3.7DateTimeFormatter线程安全的格式化替代了SimpleDateFormat而且设计为不可变、线程安全。// 预定义格式 LocalDate date LocalDate.now(); String iso date.format(DateTimeFormatter.ISO_LOCAL_DATE); // 2024-07-30 // 自定义格式 DateTimeFormatter formatter DateTimeFormatter.ofPattern(yyyy年MM月dd日); String str date.format(formatter); // 2024年07月30日 // 解析 LocalDate parsed LocalDate.parse(2024年07月30日, formatter); // 带时区的格式 DateTimeFormatter zFormatter DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss z); String zStr ZonedDateTime.now().format(zFormatter);因为DateTimeFormatter是不可变的你可以把它定义成static final全局共享多线程并发使用也不会出问题。四、完整对比JDK7 vs JDK8场景计算今天是星期几30天后是什么日期并格式化输出JDK7 写法import java.util.Date; import java.util.Calendar; import java.text.SimpleDateFormat; public class DateDemo { public static void main(String[] args) { // 获取今天 Calendar cal Calendar.getInstance(); // 星期几1周日2周一... 非常反直觉 int dayOfWeek cal.get(Calendar.DAY_OF_WEEK); System.out.println(今天是星期 (dayOfWeek - 1)); // 需要手动转换 // 加30天 cal.add(Calendar.DAY_OF_MONTH, 30); Date future cal.getTime(); // 格式化注意SimpleDateFormat 不能共享 SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd); String result sdf.format(future); System.out.println(30天后是 result); } }JDK8 写法import java.time.LocalDate; import java.time.DayOfWeek; import java.time.format.DateTimeFormatter; public class DateDemo { public static void main(String[] args) { // 获取今天 LocalDate today LocalDate.now(); // 星期几枚举类型语义清晰 DayOfWeek dayOfWeek today.getDayOfWeek(); System.out.println(今天是 dayOfWeek); // 直接输出 MONDAY, TUESDAY 等 // 加30天 LocalDate future today.plusDays(30); // 格式化formatter 可以安全地定义为 static final String result future.format(DateTimeFormatter.ISO_LOCAL_DATE); System.out.println(30天后是 result); } }对比总结方面JDK7JDK8获取今天Calendar.getInstance()LocalDate.now()星期几数字 1-7含义模糊DayOfWeek枚举语义明确日期加减cal.add(field, amount)可变对象plusDays()返回新对象格式化类SimpleDateFormat线程不安全DateTimeFormatter线程安全代码清晰度需要理解 Calendar 的常量体系方法名自解释五、新旧 API 的互操作Java 8 没有删除旧的Date和Calendar而是提供了桥接方法方便你逐步迁移// Date → Instant → LocalDateTime Date oldDate new Date(); Instant instant oldDate.toInstant(); LocalDateTime ldt LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); // LocalDateTime → Instant → Date LocalDateTime newDateTime LocalDateTime.now(); Instant instant2 newDateTime.atZone(ZoneId.systemDefault()).toInstant(); Date backToOld Date.from(instant2); // TimestampSQL↔ LocalDateTime java.sql.Timestamp timestamp java.sql.Timestamp.valueOf(localDateTime); LocalDateTime fromTimestamp timestamp.toLocalDateTime();六、总结Date-Time API 解决了什么问题JDK7 的弊端Java 8 的解决方案Date命名误导实际存储日期时间LocalDate、LocalTime、LocalDateTime职责分离年份要减 1900月份从 0 开始年份就是实际值月份从 1 开始对象可变线程不安全所有对象不可变天然线程安全SimpleDateFormat线程不安全DateTimeFormatter不可变且线程安全时区隐式、混乱ZonedDateTime显式处理时区Instant提供全球统一时间戳日期加减没有区分日历语义和时间语义Period和Duration分开处理格式化模式字符串容易写错且难以调试格式化器设计更严谨API 更一致核心设计思想日期时间不是一个值而是一组不同维度的概念日期、时间、时区、间隔。旧 API 试图用一个类解决所有问题结果一团糟。新 API 承认这些概念的独立性给每个概念一个专门的类每个类只做一件事但把它做好。