🚀 Java 巩固进阶 · 第 22 天

主题:日期时间 API(JDK8 新版)—— 告别 Date 的线程不安全

📅 进度概览:今天学习 Java 8 引入的全新日期时间 API。彻底抛弃老旧的 DateCalendar,掌握线程安全、设计优雅的 java.time 包。这是现代 Java 开发的标准规范。

💡 核心价值

  • 线程安全DateTimeFormatterLocalDateTime 不可变且线程安全,解决 SimpleDateFormat 并发痛点。
  • 语义清晰LocalDate(日期)、LocalTime(时间)、LocalDateTime(日期时间)分离,API 设计更直观。
  • 框架集成:SpringBoot 默认支持 JDK8 时间类型,JSON 序列化需配置 @JsonFormat
  • 面试高频SimpleDateFormat 线程不安全原因、JDK8 新 API 优势是必考题。

一、为什么抛弃旧 API?历史包袱太重 🎒

1. 旧版 API 的三大痛点

// ❌ 痛点 1:设计混乱(月份从 0 开始!)
Date date = new Date();
Calendar cal = Calendar.getInstance();
int month = cal.get(Calendar.MONTH);  // 返回 0-11,1 月是 0!极易出错

// ❌ 痛点 2:线程不安全(高并发必炸!)
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 多线程共享 sdf 对象 → 日期解析错乱/抛异常
// 原因:内部维护了 Calendar 对象,状态可变

// ❌ 痛点 3:API 繁琐
// 计算两个日期相差天数,需要手动转毫秒再计算,易忽略时区/闰年

2. JDK8 新 API 的核心优势

┌─────────────────────────────────────┐
│  ✅ java.time 包(JSR-310 标准)     │
│                                      │
│  • 不可变Immutable:线程安全        │
│  • 语义清晰:日期/时间/时区分离     │
│  • 计算方便:plus/minus/daysBetween │
│  • 格式化安全:DateTimeFormatter    │
└─────────────────────────────────────┘

💡 一句话原则
“新项目禁止使用 Date/Calendar/SimpleDateFormat,统一用 JDK8 java.time


二、核心类详解:LocalDate / LocalTime / LocalDateTime 📅

1. 常用类对比

含义 示例 适用场景
LocalDate 日期(年月日) 2024-04-02 生日、入职日期、保质期
LocalTime 时间(时分秒) 14:30:00 开门时间、会议时间
LocalDateTime 日期 + 时间 2024-04-02T14:30:00 订单创建时间、日志时间戳
Instant 时间戳(秒/纳秒) 1712034600 数据库存储、分布式 ID

2. 基本用法(⭐ 背下来!)

// 🎯 1. 获取当前时间
LocalDate today = LocalDate.now();
LocalDateTime now = LocalDateTime.now();

// 🎯 2. 指定时间
LocalDate birthday = LocalDate.of(2000, 1, 1);  // 2000-01-01
LocalDateTime meeting = LocalDateTime.of(2024, 4, 2, 14, 30);

// 🎯 3. 获取字段(月份从 1 开始!✅ 修复旧版坑)
int year = now.getYear();
int month = now.getMonthValue();  // 1-12
int day = now.getDayOfMonth();

// 🎯 4. 时间计算(不可变对象,返回新实例)
LocalDateTime tomorrow = now.plusDays(1);
LocalDateTime nextMonth = now.plusMonths(1);
LocalDateTime lastYear = now.minusYears(1);

// 🎯 5. 比较大小
boolean isBefore = today.isBefore(birthday);
boolean isAfter = today.isAfter(birthday);

3. ⚠️ 关键特性:不可变性

LocalDateTime dt = LocalDateTime.now();
dt.plusHours(1);  // ❌ 无效!LocalDateTime 是不可变的
// ✅ 正确:接收返回值
LocalDateTime newDt = dt.plusHours(1);

💡 记忆口诀
“JDK8 时间皆不可变,修改操作必接收返回值”


三、格式化与解析:DateTimeFormatter(线程安全)🔤

1. 标准用法

// ✅ 推荐:使用预定义格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

// 格式化:时间 → 字符串
String str = LocalDateTime.now().format(formatter);
System.out.println(str);  // 2024-04-02 14:30:00

// 解析:字符串 → 时间
LocalDateTime dt = LocalDateTime.parse("2024-04-02 14:30:00", formatter);

2. 🛡️ 线程安全验证(对比 SimpleDateFormat)

// ❌ 旧版:多线程共享 SimpleDateFormat 会报错
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 多线程调用 sdf.parse() → NumberFormatException / 日期错乱

// ✅ 新版:DateTimeFormatter 不可变,线程安全
static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// 多线程共享 formatter → 安全!无需 ThreadLocal

3. SpringBoot 集成:JSON 序列化

// 实体类字段
public class Order {
    
    // ✅ 方式 1:字段注解(推荐)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;
    
    // ✅ 方式 2:全局配置(application.yml)
    // spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
    // spring.jackson.time-zone=GMT+8
}

💡 生产坑点

  • 默认 LocalDateTime 序列化可能是数组 [2024,4,2,14,30,0],必须配 @JsonFormat
  • 时区问题:数据库存 UTC,展示转 GMT+8,建议统一用 timezone = "GMT+8"

四、时间计算:Period & Duration ⏱️

1. Period(日期间隔)

LocalDate start = LocalDate.of(2024, 1, 1);
LocalDate end = LocalDate.of(2024, 12, 31);

// 计算间隔
Period period = Period.between(start, end);
System.out.println("相差:" + period.getYears() + "年" + 
                   period.getMonths() + "月" + 
                   period.getDays() + "天");

// 直接获取天数
long days = ChronoUnit.DAYS.between(start, end);  // ✅ 推荐,更直观

2. Duration(时间间隔)

LocalDateTime start = LocalDateTime.now();
// 模拟业务
Thread.sleep(1000);
LocalDateTime end = LocalDateTime.now();

// 计算秒数/毫秒数
Duration duration = Duration.between(start, end);
long seconds = duration.getSeconds();
long millis = duration.toMillis();

3. 🎯 实战:计算入职天数

public long getWorkDays(LocalDate入职日期) {
    return ChronoUnit.DAYS.between(入职日期,LocalDate.now());
}

五、时区与时间戳:Instant & ZonedDateTime 🌍

1. Instant(时间戳)

// 获取当前时间戳(秒)
long epochSecond = Instant.now().getEpochSecond();

// 时间戳 → 时间
LocalDateTime dt = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());

2. ZonedDateTime(带时区)

// 指定时区
ZonedDateTime beijingTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDateTime tokyoTime = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));

💡 最佳实践

  • 数据库存储:用 InstantLocalDateTime(统一存 UTC 或统一存北京时间)
  • 业务展示:转为用户本地时区
  • 微服务交互:统一用 Instant 时间戳,避免时区歧义

六、🎯 今日实战任务:时间工具类封装

任务 1:封装"日期格式化工具类"

/**
 * 要求:
 * 1. 创建 DateUtils 类,所有方法静态
 * 2. 定义常用格式常量:YYYY_MM_DD, YYYY_MM_DD_HH_MM_SS
 * 3. 实现 format(LocalDateTime, pattern) 和 parse(String, pattern)
 * 4. 确保 DateTimeFormatter 线程安全(可定义为 static final)
 * 
 * 💡 提示:
 * - 不要每次调用都 new DateTimeFormatter
 * - 处理解析异常,返回 null 或抛自定义异常
 */

任务 2:实现"优惠券过期判断"逻辑

/**
 * 业务场景:判断优惠券是否可用
 * 
 * 要求:
 * 1. 输入:开始时间、结束时间、当前时间
 * 2. 逻辑:当前时间在 [开始,结束] 区间内则有效
 * 3. 输出:有效/已过期/未开始
 * 4. 边界测试:刚好等于开始/结束时间是否算有效?
 * 
 * 💡 关键 API:
 * - !now.isBefore(start) && !now.isAfter(end)
 * 或:!now.isBefore(start) && now.isBefore(end.plusDays(1))
 */
public class CouponChecker {
    public static String checkStatus(LocalDateTime start, LocalDateTime end, LocalDateTime now) {
        // TODO: 实现逻辑
    }
}

任务 3:计算"工龄/司龄"(精确到年月)

/**
 * 要求:
 * 1. 输入:入职日期(LocalDate)
 * 2. 输出:X 年 Y 月 Z 天
 * 3. 使用 Period.between 计算
 * 4. 处理闰年/大小月差异(Period 已自动处理)
 * 
 * 💡 挑战:
 * - 如果入职日期是 2 月 29 日,明年没有 2 月 29 日怎么办?
 * - 测试:2024-02-29 入职,2025-02-28 算满 1 年吗?
 */

任务 4:SpringBoot 集成测试

/**
 * 要求:
 * 1. 创建 Entity 类,包含 LocalDateTime 字段
 * 2. 创建 Controller,返回 JSON 数据
 * 3. 验证:JSON 中时间字段是否为字符串格式(而非数组)
 * 4. 验证:时区是否正确(GMT+8)
 * 
 * 💡 配置:
 * - 添加 @JsonFormat(pattern = "...", timezone = "GMT+8")
 * - 或配置 application.yml 全局 Jackson 格式
 */

📝 第 22 天 · 核心总结(极简背诵版)

  1. 新旧 API 对比

    ❌ 旧:Date/Calendar/SimpleDateFormat(可变、线程不安全、月份从 0 开始)
    ✅ 新:LocalDate/LocalDateTime/DateTimeFormatter(不可变、线程安全、语义清晰)
    
  2. 核心类选型

    只需日期 → LocalDate
    只需时间 → LocalTime
    日期 + 时间 → LocalDateTime(最常用)
    时间戳 → Instant
    
  3. 格式化铁律

    // ✅ 线程安全,可静态共享
    private static final DateTimeFormatter formatter = 
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    
    String str = dt.format(formatter);
    LocalDateTime dt = LocalDateTime.parse(str, formatter);
    
  4. SpringBoot 集成

    • ✅ 实体类字段加 @JsonFormat(pattern = "...", timezone = "GMT+8")
    • ✅ 数据库字段类型:datetimeLocalDateTime
    • ❌ 避免返回 [2024,4,2...] 数组格式
  5. 时间计算

    // 日期间隔
    Period.between(start, end);
    // 时间间隔
    Duration.between(start, end);
    // 直接算天数(推荐)
    ChronoUnit.DAYS.between(start, end);
    

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐