Java中的日期时间API详解:从Date、Calendar到现代时间体系

文章目录

    • 引言:Java日期时间处理的演进之路
    • 第一章:时间的基础概念
      • [1.1 时间原点:1970-01-01 UTC](#1.1 时间原点:1970-01-01 UTC)
      • [1.2 时间表示的两种模型](#1.2 时间表示的两种模型)
      • [1.3 时区与历法](#1.3 时区与历法)
    • 第二章:第一代日期时间API------Date
    • 第三章:日期格式化------DateFormat与SimpleDateFormat
      • [3.1 DateFormat抽象类](#3.1 DateFormat抽象类)
        • [3.1.1 预定义样式常量](#3.1.1 预定义样式常量)
        • [3.1.2 获取实例的工厂方法](#3.1.2 获取实例的工厂方法)
        • [3.1.3 核心方法](#3.1.3 核心方法)
        • [3.1.4 使用示例](#3.1.4 使用示例)
      • [3.2 SimpleDateFormat------更灵活的格式化器](#3.2 SimpleDateFormat——更灵活的格式化器)
        • [3.2.1 构造方法](#3.2.1 构造方法)
        • [3.2.2 常用模式字母](#3.2.2 常用模式字母)
        • [3.2.3 基本使用示例](#3.2.3 基本使用示例)
        • [3.2.4 模式匹配规则详解](#3.2.4 模式匹配规则详解)
      • [3.3 SimpleDateFormat的线程安全问题](#3.3 SimpleDateFormat的线程安全问题)
        • [3.3.1 问题根源------源码分析](#3.3.1 问题根源——源码分析)
        • [3.3.2 问题复现](#3.3.2 问题复现)
        • [3.3.3 解决方案](#3.3.3 解决方案)
    • 第四章:第二代日期时间API------Calendar
      • [4.1 Calendar类的设计思想](#4.1 Calendar类的设计思想)
      • [4.2 Calendar类的源码结构](#4.2 Calendar类的源码结构)
      • [4.3 获取Calendar实例](#4.3 获取Calendar实例)
      • [4.4 Calendar核心方法详解](#4.4 Calendar核心方法详解)
        • [4.4.1 获取字段值:get(int field)](#4.4.1 获取字段值:get(int field))
        • [4.4.2 设置字段值:set(int field, int value) 和 set(int year, int month, int date)](#4.4.2 设置字段值:set(int field, int value) 和 set(int year, int month, int date))
        • [4.4.3 日期计算:add(int field, int amount)](#4.4.3 日期计算:add(int field, int amount))
        • [4.4.4 日期滚动:roll(int field, int amount)](#4.4.4 日期滚动:roll(int field, int amount))
        • [4.4.5 清空与设置宽松模式](#4.4.5 清空与设置宽松模式)
      • [4.5 Calendar的优缺点分析](#4.5 Calendar的优缺点分析)
      • [4.6 Calendar与Date的转换](#4.6 Calendar与Date的转换)
      • [4.7 Calendar常用场景示例](#4.7 Calendar常用场景示例)
    • [第五章:第三代日期时间API------java.time(JSR 310)](#第五章:第三代日期时间API——java.time(JSR 310))
      • [5.1 设计哲学与核心原则](#5.1 设计哲学与核心原则)
      • [5.2 核心类概览](#5.2 核心类概览)
      • [5.3 LocalDate------只处理日期](#5.3 LocalDate——只处理日期)
        • [5.3.1 创建LocalDate](#5.3.1 创建LocalDate)
        • [5.3.2 获取字段值](#5.3.2 获取字段值)
        • [5.3.3 日期运算(加减)](#5.3.3 日期运算(加减))
        • [5.3.4 日期比较](#5.3.4 日期比较)
      • [5.4 LocalTime------只处理时间](#5.4 LocalTime——只处理时间)
      • [5.5 LocalDateTime------日期+时间(无时区)](#5.5 LocalDateTime——日期+时间(无时区))
      • [5.6 Instant------机器时间(时间戳)](#5.6 Instant——机器时间(时间戳))
      • [5.7 ZonedDateTime------带时区的日期时间](#5.7 ZonedDateTime——带时区的日期时间)
      • [5.8 Period与Duration------时间间隔](#5.8 Period与Duration——时间间隔)
      • [5.9 DateTimeFormatter------线程安全的格式化器](#5.9 DateTimeFormatter——线程安全的格式化器)
      • [5.10 时区处理------ZoneId与ZoneOffset](#5.10 时区处理——ZoneId与ZoneOffset)
      • [5.11 日期时间调整------TemporalAdjusters](#5.11 日期时间调整——TemporalAdjusters)
    • 第六章:新旧API对比与转换指南
      • [6.1 三代API对比总结](#6.1 三代API对比总结)
      • [6.2 何时使用旧API,何时使用新API](#6.2 何时使用旧API,何时使用新API)
      • [6.3 新旧API转换工具类](#6.3 新旧API转换工具类)
    • 第七章:最佳实践与常见陷阱
    • 第八章:总结与展望
      • [8.1 三代API的演进之路回顾](#8.1 三代API的演进之路回顾)
      • [8.2 核心要点总结](#8.2 核心要点总结)
      • [8.3 未来展望](#8.3 未来展望)
      • [8.4 给开发者的建议](#8.4 给开发者的建议)
    • 附录:常用代码片段速查

引言:Java日期时间处理的演进之路

在Java应用程序开发中,日期和时间的处理是极其常见的需求------记录操作时间、计算时间差、格式化输出、时区转换、订单超时计算、报表统计等场景都离不开日期时间API。然而,Java的日期时间API经历了一条曲折的演进道路。

Java 1.0时代,设计者简单粗暴地推出了java.util.Date类,但它存在诸多设计缺陷。Java 1.1引入了Calendar类试图弥补,却又带来了新的复杂性。直到Java 8,吸取了Joda-Time库的精华,推出了全新的java.time包(JSR 310),才真正解决了长久以来的痛点。

本文将深入剖析Java日期时间处理的三个时代:

  • 第一代DateDateFormatSimpleDateFormat
  • 第二代CalendarGregorianCalendarTimeZone
  • 第三代LocalDateLocalTimeLocalDateTimeZonedDateTimeInstantDateTimeFormatter

我们将从源码层面剖析其设计原理,探讨线程安全问题,提供最佳实践,并给出新旧API的转换指南。全文预计12000字以上,力求让你彻底掌握Java日期时间处理的方方面面。


第一章:时间的基础概念

在深入Java API之前,我们需要理解计算机系统中时间的基本表示方法。

1.1 时间原点:1970-01-01 UTC

几乎所有计算机系统都采用一个共同的时间原点------1970年1月1日 00:00:00 UTC(协调世界时)。这个时间点被称为Unix纪元(Unix Epoch)。

为什么选择这个时间?这主要源于Unix操作系统的历史原因。1970年左右,Unix系统诞生,设计者选择了一个相对"干净"的时间起点。此后,几乎所有类Unix系统(包括Linux、macOS)以及Java等编程语言都沿用了这一约定。

1.2 时间表示的两种模型

计算机系统中有两种表示时间的模型:

1. 面向人类的模型

  • 包含年、月、日、时、分、秒等字段
  • 受时区、历法、夏令时等因素影响
  • 例如:"2026年2月20日 星期五 下午3:30"

2. 面向机器的模型

  • 用一个整数表示时间线上的一个点
  • 通常是从时间原点开始的毫秒数或秒数
  • 不受时区影响,便于计算和比较
  • 例如:1773084600000L(毫秒数)

Java中,System.currentTimeMillis()返回的就是面向机器的模型------从1970-01-01 UTC到当前时间的毫秒数。

java 复制代码
public class TimeConcept {
    public static void main(String[] args) {
        long currentTimeMillis = System.currentTimeMillis();
        System.out.println("当前时间戳(毫秒): " + currentTimeMillis);
        
        // 计算某段代码的耗时
        long start = System.currentTimeMillis();
        // 模拟耗时操作
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        long end = System.currentTimeMillis();
        System.out.println("耗时: " + (end - start) + "ms");
    }
}

1.3 时区与历法

时区(TimeZone):由于地球的自转,不同经度的地区时间不同。时区将地球划分为24个区域,每个区域相差1小时。UTC(协调世界时)是时间基准,北京时间为UTC+8。

历法(Calendar) :不同文化使用不同的历法系统,如公历(GregorianCalendar)、农历、伊斯兰历等。Java主要支持公历(ISO-8601标准),但Calendar设计时考虑了扩展性,可以支持其他历法。


第二章:第一代日期时间API------Date

java.util.Date是Java中最早出现的日期时间类,自JDK 1.0起就存在。

2.1 Date类的源码剖析

查看Date类的源码(以JDK 8为例),可以看到其核心实现:

java 复制代码
package java.util;

public class Date implements java.io.Serializable, Cloneable, Comparable<Date> {
    // 核心:存储从1970-01-01 00:00:00 GMT开始的毫秒数
    private transient long fastTime;
    
    // 还有一些过时的方法使用的内部字段
    private transient long cdate;
    
    // 无参构造:获取当前时间
    public Date() {
        this(System.currentTimeMillis());
    }
    
    // 带参构造:根据毫秒数创建Date对象
    public Date(long date) {
        fastTime = date;
    }
    
    // 已废弃的构造方法(年份从1900开始,月份从0开始)
    @Deprecated
    public Date(int year, int month, int date) {
        this(year, month, date, 0, 0, 0);
    }
    
    // 获取毫秒数
    public long getTime() {
        return getTimeImpl();
    }
    
    // 设置毫秒数
    public void setTime(long time) {
        fastTime = time;
        cdate = null;
    }
    
    // 比较日期
    public boolean before(Date when) {
        return getMillisOf(this) < getMillisOf(when);
    }
    
    public boolean after(Date when) {
        return getMillisOf(this) > getMillisOf(when);
    }
    
    // 转换为字符串:dow mon dd hh:mm:ss zzz yyyy
    public String toString() {
        // 实际实现依赖于系统时区
        return toString(ZoneId.systemDefault());
    }
    
    // ... 其他方法
}

关键点分析

  1. 核心字段fastTime :这是一个long类型的变量,存储从1970-01-01 UTC开始的毫秒数。整个Date对象本质上就是这个数字的封装。

  2. 构造方法 :只有两个构造方法被保留推荐使用------无参构造(获取当前时间)和带毫秒参数的构造。其他构造方法都被@Deprecated标记,不再推荐使用。

  3. 主要方法getTime()setTime()用于获取和设置毫秒数;before()after()compareTo()用于日期比较;toString()用于输出。

2.2 Date类的核心方法详解

2.2.1 创建Date对象
java 复制代码
import java.util.Date;

public class DateCreation {
    public static void main(String[] args) {
        // 方式1:无参构造,表示当前时间
        Date now = new Date();
        System.out.println("当前时间: " + now);
        
        // 方式2:带毫秒参数
        Date date = new Date(1773084600000L); // 2026-02-20 具体时间取决于时区
        System.out.println("指定毫秒数的时间: " + date);
        
        // 方式3:不推荐!从字符串解析(已废弃)
        @SuppressWarnings("deprecation")
        Date deprecated = new Date("2026/02/20");
        System.out.println("已废弃方式: " + deprecated);
    }
}
2.2.2 日期比较
java 复制代码
import java.util.Date;

public class DateComparison {
    public static void main(String[] args) throws InterruptedException {
        Date date1 = new Date();
        Thread.sleep(100); // 暂停100毫秒
        Date date2 = new Date();
        
        // 使用before/after方法
        System.out.println("date1 before date2? " + date1.before(date2)); // true
        System.out.println("date2 after date1? " + date2.after(date1));   // true
        
        // 使用compareTo方法(实现Comparable接口)
        int result = date1.compareTo(date2);
        System.out.println("compareTo结果: " + result); // 负数表示date1 < date2
        
        // 使用equals方法
        System.out.println("是否相等? " + date1.equals(date2)); // false
        
        // 直接比较毫秒数
        System.out.println("毫秒比较: " + (date1.getTime() < date2.getTime())); // true
    }
}
2.2.3 获取/设置毫秒数
java 复制代码
import java.util.Date;

public class DateMillis {
    public static void main(String[] args) {
        Date now = new Date();
        
        // 获取毫秒数
        long millis = now.getTime();
        System.out.println("当前毫秒数: " + millis);
        
        // 设置新的毫秒数
        now.setTime(0L); // 设置为1970-01-01 08:00:00(北京时间,因为东八区)
        System.out.println("重置后: " + now);
        
        // 通过毫秒数计算时间差
        Date start = new Date();
        // 模拟操作...
        Date end = new Date();
        long elapsed = end.getTime() - start.getTime();
        System.out.println("耗时: " + elapsed + "ms");
    }
}

2.3 Date类的设计缺陷(为什么被废弃)

Date类的大部分方法在JDK 1.1之后就被标记为@Deprecated,主要原因如下:

缺陷1:年份从1900年开始
java 复制代码
import java.util.Date;

public class DatePitfall {
    public static void main(String[] args) {
        // 本意是2026年2月20日
        @SuppressWarnings("deprecation")
        Date date = new Date(2026, 2, 20); // 年份参数是2026吗?
        
        // 实际输出:3926-03-20(年份 = 2026 + 1900,月份2表示3月)
        System.out.println("实际结果: " + date);
        
        // 正确的写法应该是:
        Date correct = new Date(126, 2, 20); // 年份 = 2026 - 1900 = 126
        System.out.println("正确结果: " + correct);
    }
}

解释Date(int year, int month, int date)构造方法中,year参数表示"year - 1900",所以传入2026相当于1900+2026=3926年。这完全是反直觉的设计。

缺陷2:月份从0开始
java 复制代码
@SuppressWarnings("deprecation")
Date date = new Date(126, 2, 20); // 月份2实际表示3月
System.out.println(date); // 输出:Fri Mar 20 00:00:00 CST 2026

解释:月份常量中,0代表1月,1代表2月,...,11代表12月。这与日常习惯(1-12月)完全不符,极易导致月份错误。

缺陷3:可变性导致的线程安全问题
java 复制代码
import java.util.Date;

public class DateMutableProblem {
    public static void main(String[] args) {
        Date date = new Date();
        System.out.println("原始日期: " + date);
        
        // setTime方法可以修改Date对象内部状态
        date.setTime(0L);
        System.out.println("被修改后: " + date); // 原始对象被改变!
        
        // 在多线程环境下,这种可变性会导致数据不一致
    }
}

解释Date是可变的,其setTime()等方法可以修改内部fastTime字段。在多线程环境中,如果多个线程共享同一个Date实例且进行修改,会产生竞态条件。

缺陷4:国际化支持薄弱
java 复制代码
// Date的toString()输出格式固定,不考虑Locale
Date now = new Date();
System.out.println(now); // 总是输出:Fri Feb 20 15:30:45 CST 2026
// 无法直接输出中文格式的"2026年2月20日 星期五 下午3:30:45"

解释DatetoString()方法格式固定,不考虑不同国家和语言的日期表示习惯。

缺陷5:命名混淆

java.util.Date实际上既包含日期也包含时间,但类名却只叫"Date",容易让人误以为它只处理日期部分。

2.4 正确使用Date的建议

鉴于上述缺陷,官方建议:

  1. 只使用两个推荐的方法new Date()new Date(long)构造、getTime()setTime()before()after()compareTo()
  2. 不要使用任何@Deprecated标记的方法
  3. 将Date作为"时间戳"的载体,不直接操作其日历字段
  4. 在需要日历字段操作时,使用Calendar类
java 复制代码
// 推荐的使用方式:仅将Date作为时间戳对象
Date now = new Date(); // 当前时刻
long timestamp = now.getTime(); // 获取毫秒数

// 如果需要操作年月日,使用Calendar
Calendar cal = Calendar.getInstance();
cal.setTime(now);
int year = cal.get(Calendar.YEAR); // 正确获取年份

第三章:日期格式化------DateFormat与SimpleDateFormat

Date类本身无法控制输出的格式,因此Java提供了专门的格式化类DateFormat及其子类SimpleDateFormat,用于在Date对象和字符串之间进行转换。

3.1 DateFormat抽象类

java.text.DateFormat是一个抽象类,用于格式化/解析日期时间。

3.1.1 预定义样式常量

DateFormat定义了四种内置的显示风格:

常量 说明
DateFormat.FULL 0 完整格式,如"2026年2月20日 星期五"
DateFormat.LONG 1 长格式,如"2026年2月20日"
DateFormat.MEDIUM 2 中等格式,如"2026-2-20"(默认风格)
DateFormat.SHORT 3 短格式,如"26-2-20"
3.1.2 获取实例的工厂方法
java 复制代码
// 获取日期格式化器
DateFormat.getDateInstance();          // 默认风格(MEDIUM)的日期格式化
DateFormat.getDateInstance(int style); // 指定风格的日期格式化
DateFormat.getDateInstance(int style, Locale locale); // 指定风格和区域

// 获取时间格式化器
DateFormat.getTimeInstance();          // 默认风格的时间格式化
DateFormat.getTimeInstance(int style); // 指定风格的时间格式化

// 获取日期时间格式化器
DateFormat.getDateTimeInstance();       // 默认风格的日期时间格式化
DateFormat.getDateTimeInstance(int dateStyle, int timeStyle); // 指定风格
3.1.3 核心方法
java 复制代码
// Date -> String 格式化
public final String format(Date date)

// String -> Date 解析
public Date parse(String source) throws ParseException
3.1.4 使用示例
java 复制代码
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;

public class DateFormatDemo {
    public static void main(String[] args) {
        Date now = new Date();
        
        // 不同风格的日期格式化
        DateFormat dfFull = DateFormat.getDateInstance(DateFormat.FULL, Locale.CHINA);
        DateFormat dfLong = DateFormat.getDateInstance(DateFormat.LONG, Locale.CHINA);
        DateFormat dfMedium = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.CHINA);
        DateFormat dfShort = DateFormat.getDateInstance(DateFormat.SHORT, Locale.CHINA);
        
        System.out.println("FULL格式: " + dfFull.format(now));   // 2026年2月20日 星期五
        System.out.println("LONG格式: " + dfLong.format(now));   // 2026年2月20日
        System.out.println("MEDIUM格式: " + dfMedium.format(now)); // 2026-2-20
        System.out.println("SHORT格式: " + dfShort.format(now));  // 26-2-20
        
        // 时间格式化
        DateFormat tf = DateFormat.getTimeInstance(DateFormat.MEDIUM, Locale.CHINA);
        System.out.println("时间: " + tf.format(now)); // 15:30:45
        
        // 日期时间格式化
        DateFormat dtf = DateFormat.getDateTimeInstance(
            DateFormat.LONG, DateFormat.MEDIUM, Locale.CHINA);
        System.out.println("日期时间: " + dtf.format(now)); // 2026年2月20日 15:30:45
        
        // 解析字符串为Date
        try {
            String dateStr = "2026-02-20";
            DateFormat parser = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.CHINA);
            Date parsed = parser.parse(dateStr);
            System.out.println("解析结果: " + parsed);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3.2 SimpleDateFormat------更灵活的格式化器

java.text.SimpleDateFormatDateFormat的具体子类,允许通过模式字符串自定义日期时间格式。

3.2.1 构造方法
java 复制代码
// 使用默认模式
SimpleDateFormat()

// 使用指定模式
SimpleDateFormat(String pattern)

// 使用指定模式和区域
SimpleDateFormat(String pattern, Locale locale)
3.2.2 常用模式字母
字母 日期/时间元素 示例
y yyyy -> 2026
M 月份 MM -> 02, MMM -> 二月
d 月份中的天数 dd -> 20
E 星期几 EEEE -> 星期五
a AM/PM标记 a -> 下午
H 小时(0-23) HH -> 15
h 小时(1-12) hh -> 03
m 分钟 mm -> 30
s ss -> 45
S 毫秒 SSS -> 123
z 时区 z -> CST
Z RFC 822时区 Z -> +0800
3.2.3 基本使用示例
java 复制代码
import java.text.SimpleDateFormat;
import java.util.Date;

public class SimpleDateFormatDemo {
    public static void main(String[] args) {
        Date now = new Date();
        
        // 1. 常用格式:yyyy-MM-dd HH:mm:ss
        SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("格式1: " + sdf1.format(now)); // 2026-02-20 15:30:45
        
        // 2. 中文格式
        SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy年MM月dd日 EEEE HH时mm分ss秒");
        System.out.println("格式2: " + sdf2.format(now)); // 2026年02月20日 星期五 15时30分45秒
        
        // 3. 带毫秒
        SimpleDateFormat sdf3 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        System.out.println("格式3: " + sdf3.format(now)); // 2026-02-20 15:30:45.123
        
        // 4. 12小时制带AM/PM
        SimpleDateFormat sdf4 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss a");
        System.out.println("格式4: " + sdf4.format(now)); // 2026-02-20 03:30:45 下午
        
        // 5. 只显示日期
        SimpleDateFormat sdf5 = new SimpleDateFormat("yyyy/MM/dd");
        System.out.println("格式5: " + sdf5.format(now)); // 2026/02/20
        
        // 6. 字符串解析为Date
        try {
            String dateStr = "2026-02-20 15:30:45";
            SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Date parsed = parser.parse(dateStr);
            System.out.println("解析结果: " + parsed);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
3.2.4 模式匹配规则详解
java 复制代码
import java.text.SimpleDateFormat;
import java.util.Date;

public class PatternDetail {
    public static void main(String[] args) {
        Date now = new Date();
        
        // 年份:y个数影响显示位数
        System.out.println("yyyy: " + new SimpleDateFormat("yyyy").format(now)); // 2026
        System.out.println("yy: " + new SimpleDateFormat("yy").format(now));   // 26
        
        // 月份:M个数影响格式
        System.out.println("M: " + new SimpleDateFormat("M").format(now));     // 2
        System.out.println("MM: " + new SimpleDateFormat("MM").format(now));   // 02
        System.out.println("MMM: " + new SimpleDateFormat("MMM").format(now)); // 二月
        System.out.println("MMMM: " + new SimpleDateFormat("MMMM").format(now)); // 二月
        
        // 日期:d个数影响格式
        System.out.println("d: " + new SimpleDateFormat("d").format(now));     // 20
        System.out.println("dd: " + new SimpleDateFormat("dd").format(now));   // 20
        
        // 星期:E个数影响格式
        System.out.println("E: " + new SimpleDateFormat("E").format(now));     // 星期五
        System.out.println("EEEE: " + new SimpleDateFormat("EEEE").format(now)); // 星期五
        
        // 小时:H(0-23) vs h(1-12)
        System.out.println("H: " + new SimpleDateFormat("H").format(now));     // 15
        System.out.println("HH: " + new SimpleDateFormat("HH").format(now));   // 15
        System.out.println("h: " + new SimpleDateFormat("h").format(now));     // 3
        System.out.println("hh: " + new SimpleDateFormat("hh").format(now));   // 03
    }
}

3.3 SimpleDateFormat的线程安全问题

核心问题SimpleDateFormat线程不安全的。

3.3.1 问题根源------源码分析

查看SimpleDateFormat的源码,可以发现它继承自DateFormat,而DateFormat中定义了一个protectedCalendar对象:

java 复制代码
// DateFormat.java
public abstract class DateFormat extends Format {
    protected Calendar calendar; // 共享的Calendar实例
    // ...
}

// SimpleDateFormat.java
public class SimpleDateFormat extends DateFormat {
    // 在format方法中会使用calendar
    private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) {
        // 关键点:这里修改了calendar的状态!
        calendar.setTime(date);
        // ... 后续使用calendar进行格式化
        return toAppendTo;
    }
}

问题分析

  • calendarDateFormat类的成员变量,被SimpleDateFormat继承
  • format()方法中,首先调用calendar.setTime(date)修改calendar的状态
  • 在多线程环境下,如果多个线程共享同一个SimpleDateFormat实例,线程A执行calendar.setTime()后可能被暂停,线程B开始执行并再次修改calendar,导致线程A后续使用的calendar状态已被改变,产生错误结果甚至程序崩溃
3.3.2 问题复现
java 复制代码
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SimpleDateFormatThreadProblem {
    // 共享的SimpleDateFormat实例(线程不安全)
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        
        for (int i = 0; i < 100; i++) {
            final int num = i;
            executor.submit(() -> {
                try {
                    // 多个线程同时调用format方法
                    String dateStr = sdf.format(new Date());
                    System.out.println("Thread " + num + ": " + dateStr);
                    // 可能会输出错误结果,或抛出异常
                } catch (Exception e) {
                    System.out.println("Thread " + num + " exception: " + e);
                }
            });
        }
        
        executor.shutdown();
    }
}

运行上述代码,可能出现:

  • 日期时间错误(如月份变为0)
  • 抛出NumberFormatException等异常
  • 程序挂死
3.3.3 解决方案

方案1:每次使用时创建新实例

java 复制代码
public class SafeDateFormat1 {
    public static String formatDate(Date date) {
        // 每次方法调用都创建新实例,避免共享
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.format(date);
    }
    
    public static Date parse(String dateStr) throws ParseException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.parse(dateStr);
    }
}

优点:简单可靠

缺点:频繁创建对象,有一定性能开销,但在大多数应用中可接受

方案2:使用ThreadLocal

java 复制代码
import java.text.SimpleDateFormat;
import java.util.Date;

public class SafeDateFormat2 {
    // 每个线程持有自己的SimpleDateFormat实例
    private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = 
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    
    public static String formatDate(Date date) {
        return DATE_FORMAT.get().format(date);
    }
    
    public static Date parse(String dateStr) throws Exception {
        return DATE_FORMAT.get().parse(dateStr);
    }
    
    // 清理(通常在web请求结束时调用)
    public static void remove() {
        DATE_FORMAT.remove();
    }
}

优点:线程安全,避免了重复创建的开销

缺点:需要注意在线程池环境中及时清理,防止内存泄漏

方案3:同步加锁

java 复制代码
public class SafeDateFormat3 {
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
    public static synchronized String formatDate(Date date) {
        return sdf.format(date);
    }
    
    public static synchronized Date parse(String dateStr) throws Exception {
        return sdf.parse(dateStr);
    }
}

优点:简单

缺点:并发性能差,多个线程需要排队

方案4:使用Java 8的DateTimeFormatter(最佳方案)

java 复制代码
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class SafeDateFormat4 {
    // DateTimeFormatter是不可变且线程安全的
    private static final DateTimeFormatter formatter = 
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    
    public static String formatDate(LocalDateTime dateTime) {
        return dateTime.format(formatter);
    }
    
    public static LocalDateTime parse(String dateStr) {
        return LocalDateTime.parse(dateStr, formatter);
    }
}

推荐方案 :在新项目中,直接使用Java 8的DateTimeFormatter;在维护旧项目时,使用ThreadLocal包装SimpleDateFormat


第四章:第二代日期时间API------Calendar

为了解决Date类的缺陷,JDK 1.1引入了java.util.Calendar类,它是一个抽象类,提供了更强大的日期字段操作能力。

4.1 Calendar类的设计思想

Calendar的设计目标是:

  • 字段化:将日期时间分解为年、月、日、时、分、秒等独立字段
  • 国际化:支持不同时区和语言环境
  • 计算能力:支持日期的加减、滚动等操作
  • 扩展性:可以支持不同的历法系统(如公历、农历、日本历等)

4.2 Calendar类的源码结构

java 复制代码
public abstract class Calendar implements Serializable, Cloneable, Comparable<Calendar> {
    // 核心字段:存储时间毫秒数
    protected long time;
    
    // 标记time是否被设置过
    protected boolean isTimeSet;
    
    // 存储各个日历字段的值(如YEAR、MONTH等)
    protected int fields[];
    
    // 标记fields中哪些字段被设置过
    protected boolean isSet[];
    
    // 时区
    private TimeZone zone;
    
    // 17个静态常量,作为fields数组的索引
    public static final int ERA = 0;
    public static final int YEAR = 1;
    public static final int MONTH = 2;
    public static final int WEEK_OF_YEAR = 3;
    public static final int WEEK_OF_MONTH = 4;
    public static final int DATE = 5;          // 同DAY_OF_MONTH
    public static final int DAY_OF_MONTH = 5;
    public static final int DAY_OF_YEAR = 6;
    public static final int DAY_OF_WEEK = 7;
    public static final int DAY_OF_WEEK_IN_MONTH = 8;
    public static final int AM_PM = 9;
    public static final int HOUR = 10;          // 12小时制(0-11)
    public static final int HOUR_OF_DAY = 11;   // 24小时制(0-23)
    public static final int MINUTE = 12;
    public static final int SECOND = 13;
    public static final int MILLISECOND = 14;
    public static final int ZONE_OFFSET = 15;
    public static final int DST_OFFSET = 16;
    
    // 共17个字段,索引0-16
    public static final int FIELD_COUNT = 17;
    
    // 获取实例的工厂方法
    public static Calendar getInstance() {
        return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));
    }
    
    // 抽象方法:子类实现具体的历法计算
    protected abstract void computeTime();
    protected abstract void computeFields();
    
    // 核心方法:获取字段值
    public int get(int field) {
        complete(); // 确保fields数组是最新的
        return internalGet(field);
    }
    
    // 设置字段值
    public void set(int field, int value) {
        // 如果设置了某个字段,time就不再准确,需要重新计算
        isTimeSet = false;
        fields[field] = value;
        isSet[field] = true;
    }
    
    // 日期计算:增加/减少指定字段的值(会进位)
    public abstract void add(int field, int amount);
    
    // 日期滚动:增加/减少指定字段的值(不会进位)
    public void roll(int field, int amount);
    
    // 获取对应的Date对象
    public final Date getTime() {
        return new Date(getTimeInMillis());
    }
    
    // 设置时间
    public final void setTime(Date date) {
        setTimeInMillis(date.getTime());
    }
}

关键设计解析

  1. 双重表示Calendar内部同时维护两种表示------time(毫秒数)和fields[](字段数组)。当修改字段时,isTimeSet标记设为false,表示time需要重新计算;当设置时间时,fields[]会被重新计算。

  2. 延迟计算get(int field)方法调用complete(),如果isTimeSetfalse,则调用computeTime()fields[]计算time;如果fields[]不完整,则调用computeFields()time计算fields[]。这种延迟计算提高了性能。

  3. 17个常量 :这些常量作为fields[]数组的索引,每个常量代表一个日历字段。理解这些常量是使用Calendar的基础。

4.3 获取Calendar实例

Calendar是抽象类,不能直接new。通过工厂方法获取实例:

java 复制代码
import java.util.Calendar;
import java.util.TimeZone;
import java.util.Locale;

public class CalendarInstance {
    public static void main(String[] args) {
        // 1. 默认时区和语言环境(通常使用系统默认值)
        Calendar cal1 = Calendar.getInstance();
        System.out.println("默认: " + cal1.getTime());
        
        // 2. 指定时区
        Calendar cal2 = Calendar.getInstance(TimeZone.getTimeZone("America/New_York"));
        System.out.println("纽约时区: " + cal2.getTime());
        
        // 3. 指定语言环境
        Calendar cal3 = Calendar.getInstance(Locale.US);
        System.out.println("美国语言环境: " + cal3.getTime());
        
        // 4. 同时指定时区和语言环境
        Calendar cal4 = Calendar.getInstance(
            TimeZone.getTimeZone("Asia/Shanghai"), 
            Locale.CHINA
        );
        System.out.println("上海时区+中国: " + cal4.getTime());
    }
}

getInstance()内部调用createCalendar(),默认返回GregorianCalendar(公历)实例。

4.4 Calendar核心方法详解

4.4.1 获取字段值:get(int field)
java 复制代码
import java.util.Calendar;

public class CalendarGet {
    public static void main(String[] args) {
        Calendar cal = Calendar.getInstance();
        
        // 获取各个字段的值
        int year = cal.get(Calendar.YEAR);
        int month = cal.get(Calendar.MONTH);      // 注意:0代表1月!
        int day = cal.get(Calendar.DAY_OF_MONTH);
        int hour12 = cal.get(Calendar.HOUR);       // 12小时制
        int hour24 = cal.get(Calendar.HOUR_OF_DAY); // 24小时制
        int minute = cal.get(Calendar.MINUTE);
        int second = cal.get(Calendar.SECOND);
        int millis = cal.get(Calendar.MILLISECOND);
        int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK); // 1=周日, 2=周一, ..., 7=周六
        int dayOfYear = cal.get(Calendar.DAY_OF_YEAR);
        int weekOfYear = cal.get(Calendar.WEEK_OF_YEAR);
        int weekOfMonth = cal.get(Calendar.WEEK_OF_MONTH);
        int ampm = cal.get(Calendar.AM_PM);         // 0=上午, 1=下午
        
        System.out.printf("年: %d%n", year);
        System.out.printf("月: %d (实际月份=%d)%n", month, month + 1); // 记得+1
        System.out.printf("日: %d%n", day);
        System.out.printf("24小时制: %d%n", hour24);
        System.out.printf("12小时制: %d %s%n", hour12, ampm == 0 ? "AM" : "PM");
        System.out.printf("分: %d%n", minute);
        System.out.printf("秒: %d%n", second);
        System.out.printf("毫秒: %d%n", millis);
        System.out.printf("星期: %d (1=周日, 2=周一)%n", dayOfWeek);
        System.out.printf("一年中的第几天: %d%n", dayOfYear);
        System.out.printf("一年中的第几周: %d%n", weekOfYear);
    }
}

重要提醒

  • Calendar.MONTH从0开始(0=一月,11=十二月)
  • Calendar.DAY_OF_WEEK:1=周日,2=周一,...,7=周六
4.4.2 设置字段值:set(int field, int value) 和 set(int year, int month, int date)
java 复制代码
import java.util.Calendar;

public class CalendarSet {
    public static void main(String[] args) {
        Calendar cal = Calendar.getInstance();
        
        // 方法1:逐个字段设置
        cal.set(Calendar.YEAR, 2026);
        cal.set(Calendar.MONTH, 1);      // 1 = 二月
        cal.set(Calendar.DAY_OF_MONTH, 20);
        cal.set(Calendar.HOUR_OF_DAY, 15);
        cal.set(Calendar.MINUTE, 30);
        cal.set(Calendar.SECOND, 45);
        cal.set(Calendar.MILLISECOND, 123);
        
        System.out.println("设置后: " + cal.getTime());
        
        // 方法2:一次设置年月日
        Calendar cal2 = Calendar.getInstance();
        cal2.set(2026, 1, 20); // 年, 月, 日 (月从0开始)
        System.out.println("年月日: " + cal2.getTime());
        
        // 方法3:一次设置年月日时分秒
        Calendar cal3 = Calendar.getInstance();
        cal3.set(2026, 1, 20, 15, 30, 45); // 年,月,日,时,分,秒
        System.out.println("完整设置: " + cal3.getTime());
        
        // 注意:月份偏移问题
        Calendar wrong = Calendar.getInstance();
        wrong.set(2026, 2, 20); // 本意是2026年2月20日?不对,2表示3月!
        System.out.println("错误设置(月份2): " + wrong.getTime()); // 实际是3月20日
    }
}
4.4.3 日期计算:add(int field, int amount)

add()方法按照日历规则,对指定字段增加/减少指定值,会进位到更高字段。

java 复制代码
import java.util.Calendar;
import java.text.SimpleDateFormat;

public class CalendarAdd {
    public static void main(String[] args) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Calendar cal = Calendar.getInstance();
        cal.set(2026, 1, 20, 23, 59, 59); // 2026-02-20 23:59:59
        
        System.out.println("原始时间: " + sdf.format(cal.getTime()));
        
        // 加10秒(会进位到分钟)
        cal.add(Calendar.SECOND, 10);
        System.out.println("加10秒后: " + sdf.format(cal.getTime())); // 2026-02-21 00:00:09
        
        // 重置
        cal.set(2026, 1, 20, 23, 59, 59);
        
        // 加1分钟(会进位到小时)
        cal.add(Calendar.MINUTE, 1);
        System.out.println("加1分钟后: " + sdf.format(cal.getTime())); // 2026-02-21 00:00:59
        
        // 加1小时(会进位到天)
        cal.set(2026, 1, 20, 23, 59, 59);
        cal.add(Calendar.HOUR_OF_DAY, 1);
        System.out.println("加1小时后: " + sdf.format(cal.getTime())); // 2026-02-21 00:59:59
        
        // 加1个月(会进位到年)
        cal.set(2026, 11, 20); // 2026-12-20
        cal.add(Calendar.MONTH, 1);
        System.out.println("加1个月后: " + sdf.format(cal.getTime())); // 2027-01-20
        
        // 负数表示减去
        cal.set(2026, 0, 1); // 2026-01-01
        cal.add(Calendar.DAY_OF_MONTH, -1);
        System.out.println("减1天后: " + sdf.format(cal.getTime())); // 2025-12-31
    }
}

典型应用:计算30天后、3个月前、5年后等。

4.4.4 日期滚动:roll(int field, int amount)

roll()add()类似,但不会进位到更高字段。

java 复制代码
import java.util.Calendar;
import java.text.SimpleDateFormat;

public class CalendarRoll {
    public static void main(String[] args) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Calendar cal = Calendar.getInstance();
        
        // 示例1:月份滚动
        cal.set(2026, 11, 20); // 2026-12-20
        System.out.println("原始: " + sdf.format(cal.getTime()));
        
        cal.roll(Calendar.MONTH, 1); // 月份+1,但不进位
        System.out.println("roll +1月: " + sdf.format(cal.getTime())); // 2026-01-20!
        // 解释:12月roll 1个月,按照月份范围1-12,12+1=13,但不会进位到年,所以回到1月
        
        // 示例2:日期滚动(月份不同长度)
        cal.set(2026, 0, 31); // 2026-01-31
        System.out.println("原始: " + sdf.format(cal.getTime()));
        
        cal.roll(Calendar.DAY_OF_MONTH, 1); // 日期+1,但不进位
        System.out.println("roll +1天: " + sdf.format(cal.getTime())); // 2026-01-01
        // 解释:1月31日加1天,日期范围1-31,31+1=32,不会进位到2月,而是回到1月1日
        
        // 对比add的行为
        cal.set(2026, 0, 31);
        cal.add(Calendar.DAY_OF_MONTH, 1);
        System.out.println("add +1天: " + sdf.format(cal.getTime())); // 2026-02-01(进位)
    }
}

适用场景:当你只想在字段范围内循环(如调整日期但不想改变月份)时使用。

4.4.5 清空与设置宽松模式
java 复制代码
import java.util.Calendar;

public class CalendarClearLenient {
    public static void main(String[] args) {
        Calendar cal = Calendar.getInstance();
        
        // 1. clear():清空所有字段,设置为1970-01-01 00:00:00
        cal.clear();
        System.out.println("clear后: " + cal.getTime());
        
        // 2. clear(int field):清空指定字段
        cal.set(2026, 1, 20);
        cal.clear(Calendar.HOUR_OF_DAY);
        cal.clear(Calendar.MINUTE);
        cal.clear(Calendar.SECOND);
        System.out.println("清空时间后: " + cal.getTime()); // 日期不变,时间为00:00:00
        
        // 3. setLenient:设置宽松/严格模式
        cal.setLenient(true); // 默认:宽松模式,允许非法字段值自动修正
        
        cal.set(2026, 1, 31); // 2月31日不存在
        System.out.println("宽松模式: " + cal.getTime()); // 自动修正为2026-03-03或03-02(取决于具体实现)
        
        cal.setLenient(false); // 严格模式
        cal.set(2026, 1, 31);
        try {
            System.out.println(cal.getTime()); // 抛出IllegalArgumentException
        } catch (IllegalArgumentException e) {
            System.out.println("严格模式下非法日期抛出异常");
        }
    }
}

宽松模式:自动将非法字段值调整为合法值,如2月31日变为3月2日或3月3日(具体规则依赖于实现)。

严格模式:字段值非法时抛出异常,适合需要严格校验的场景。

4.5 Calendar的优缺点分析

优点
  1. 丰富的字段操作:提供了17个日历字段,可以精确获取和设置各个部分
  2. 强大的计算能力add()roll()方法支持灵活的日期运算
  3. 国际化支持:内置时区和Locale处理
  4. 历法扩展性:可以支持不同的历法系统
缺点
  1. API复杂繁琐:使用时需要记住大量常量,代码冗长
  2. 月份偏移陷阱:月份从0开始,极易出错
  3. 线程不安全:内部状态可变,多线程共享需同步
  4. 可变性:方法调用会修改对象内部状态,不符合函数式编程思想
  5. 性能开销:相对重量级,频繁创建有性能问题
  6. 设计缺陷 :某些方法行为不够直观(如roll

4.6 Calendar与Date的转换

java 复制代码
import java.util.Calendar;
import java.util.Date;

public class CalendarDateConversion {
    public static void main(String[] args) {
        // Calendar -> Date
        Calendar cal = Calendar.getInstance();
        Date dateFromCal = cal.getTime();
        System.out.println("Calendar转Date: " + dateFromCal);
        
        // Date -> Calendar
        Date now = new Date();
        Calendar calFromDate = Calendar.getInstance();
        calFromDate.setTime(now);
        System.out.println("Date转Calendar: " + calFromDate.getTime());
        
        // 获取毫秒数
        long millisFromCal = cal.getTimeInMillis();
        long millisFromDate = now.getTime();
        System.out.println("毫秒数相等: " + (millisFromCal == millisFromDate));
    }
}

4.7 Calendar常用场景示例

场景1:计算两个日期之间的天数差
java 复制代码
import java.util.Calendar;
import java.util.Date;

public class DateDiff {
    public static int daysBetween(Date startDate, Date endDate) {
        Calendar startCal = Calendar.getInstance();
        startCal.setTime(startDate);
        
        Calendar endCal = Calendar.getInstance();
        endCal.setTime(endDate);
        
        // 重置时间到午夜,避免时分秒影响
        startCal.set(Calendar.HOUR_OF_DAY, 0);
        startCal.set(Calendar.MINUTE, 0);
        startCal.set(Calendar.SECOND, 0);
        startCal.set(Calendar.MILLISECOND, 0);
        
        endCal.set(Calendar.HOUR_OF_DAY, 0);
        endCal.set(Calendar.MINUTE, 0);
        endCal.set(Calendar.SECOND, 0);
        endCal.set(Calendar.MILLISECOND, 0);
        
        long millis1 = startCal.getTimeInMillis();
        long millis2 = endCal.getTimeInMillis();
        
        long diff = millis2 - millis1;
        return (int) (diff / (24 * 60 * 60 * 1000));
    }
    
    public static void main(String[] args) {
        Calendar cal1 = Calendar.getInstance();
        cal1.set(2026, 0, 1); // 2026-01-01
        
        Calendar cal2 = Calendar.getInstance();
        cal2.set(2026, 11, 31); // 2026-12-31
        
        int days = daysBetween(cal1.getTime(), cal2.getTime());
        System.out.println("2026年共有: " + (days + 1) + "天"); // 365天
    }
}
场景2:获取某月的第一天和最后一天
java 复制代码
import java.util.Calendar;

public class MonthBoundary {
    public static void main(String[] args) {
        Calendar cal = Calendar.getInstance();
        cal.set(2026, 1, 15); // 2026-02-15
        
        // 第一天
        cal.set(Calendar.DAY_OF_MONTH, 1);
        System.out.println("本月第一天: " + cal.getTime());
        
        // 最后一天
        cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
        System.out.println("本月最后一天: " + cal.getTime());
    }
}
场景3:判断闰年
java 复制代码
import java.util.Calendar;
import java.util.GregorianCalendar;

public class LeapYearCheck {
    public static boolean isLeapYear(int year) {
        GregorianCalendar cal = new GregorianCalendar();
        return cal.isLeapYear(year);
    }
    
    public static void main(String[] args) {
        System.out.println("2024是闰年? " + isLeapYear(2024)); // true
        System.out.println("2026是闰年? " + isLeapYear(2026)); // false
        System.out.println("2000是闰年? " + isLeapYear(2000)); // true(世纪年规则)
    }
}

第五章:第三代日期时间API------java.time(JSR 310)

鉴于第一代Date和第二代Calendar的种种缺陷,Java 8引入了全新的日期时间API------java.time包(JSR 310),它深受Joda-Time库的启发,提供了更优雅、更安全、更强大的日期时间处理能力。

5.1 设计哲学与核心原则

Java 8日期时间API的设计遵循以下核心原则:

  1. 不可变性 :所有核心类都是final的,且内部状态不可变,任何修改操作都返回新对象,天然线程安全

  2. 清晰分离:将日期、时间、日期时间、带时区的日期时间等概念清晰分离,通过不同类表示

  3. 流畅API:方法命名直观,支持链式调用

  4. ISO标准:默认采用ISO-8601国际标准

  5. 线程安全:所有类都是不可变的,可在多线程环境下安全共享

5.2 核心类概览

类名 用途 示例
LocalDate 只处理日期(年、月、日) 2026-02-20
LocalTime 只处理时间(时、分、秒、纳秒) 15:30:45.123
LocalDateTime 处理日期+时间(无时区) 2026-02-20T15:30:45
ZonedDateTime 带时区的日期时间 2026-02-20T15:30:45+08:00[Asia/Shanghai]
OffsetDateTime 带偏移量的日期时间 2026-02-20T15:30:45+08:00
OffsetTime 带偏移量的时间 15:30:45+08:00
Instant 时间戳(机器时间) 2026-02-20T07:30:45Z
Year 年份 2026
YearMonth 年月 2026-02
MonthDay 月日 --02-20
Period 日期期间隔(年、月、日) P1Y2M3D
Duration 时间间隔(时、分、秒、纳秒) PT15H30M
DateTimeFormatter 格式化/解析(线程安全)
ZoneId 时区ID Asia/Shanghai
ZoneOffset 时区偏移量 +08:00

5.3 LocalDate------只处理日期

LocalDate是一个不可变的日期对象,表示年-月-日,不包含时间和时区信息。

5.3.1 创建LocalDate
java 复制代码
import java.time.LocalDate;
import java.time.Month;
import java.time.format.DateTimeFormatter;

public class LocalDateCreation {
    public static void main(String[] args) {
        // 1. 当前日期
        LocalDate now = LocalDate.now();
        System.out.println("当前日期: " + now); // 2026-02-20
        
        // 2. 指定年月日
        LocalDate date1 = LocalDate.of(2026, 2, 20);
        LocalDate date2 = LocalDate.of(2026, Month.FEBRUARY, 20); // 使用Month枚举
        System.out.println("指定日期: " + date1);
        
        // 3. 从字符串解析(ISO格式:yyyy-MM-dd)
        LocalDate parsed1 = LocalDate.parse("2026-02-20");
        System.out.println("解析ISO: " + parsed1);
        
        // 4. 从字符串解析(自定义格式)
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
        LocalDate parsed2 = LocalDate.parse("2026/02/20", formatter);
        System.out.println("解析自定义: " + parsed2);
        
        // 5. 从年日获取
        LocalDate fromYearDay = LocalDate.ofYearDay(2026, 51); // 2026年的第51天 = 2月20日
        System.out.println("年日转换: " + fromYearDay);
        
        // 6. 从纪元日获取
        LocalDate fromEpoch = LocalDate.ofEpochDay(20489); // 从1970-01-01开始的天数
        System.out.println("纪元日转换: " + fromEpoch);
    }
}

重要LocalDate的月份从1开始,符合人类直觉,再也不需要month+1了!

5.3.2 获取字段值
java 复制代码
import java.time.LocalDate;
import java.time.DayOfWeek;

public class LocalDateGet {
    public static void main(String[] args) {
        LocalDate date = LocalDate.of(2026, 2, 20);
        
        int year = date.getYear();              // 2026
        int month = date.getMonthValue();       // 2(1-12)
        Month monthEnum = date.getMonth();      // FEBRUARY
        int day = date.getDayOfMonth();         // 20
        int dayOfYear = date.getDayOfYear();    // 51
        DayOfWeek dayOfWeek = date.getDayOfWeek(); // FRIDAY
        
        System.out.printf("年: %d%n", year);
        System.out.printf("月: %d (%s)%n", month, monthEnum);
        System.out.printf("日: %d%n", day);
        System.out.printf("一年中的第几天: %d%n", dayOfYear);
        System.out.printf("星期: %s%n", dayOfWeek);
        
        // 检查某些特征
        System.out.println("是否闰年? " + date.isLeapYear()); // false
        System.out.println("本月长度: " + date.lengthOfMonth()); // 28(2026年2月)
        System.out.println("本年长度: " + date.lengthOfYear()); // 365
    }
}
5.3.3 日期运算(加减)

LocalDate的运算返回新的 LocalDate对象,原对象不变。

java 复制代码
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;

public class LocalDatePlusMinus {
    public static void main(String[] args) {
        LocalDate date = LocalDate.of(2026, 2, 20);
        System.out.println("原始日期: " + date);
        
        // 加天数
        LocalDate plusDays = date.plusDays(10);
        System.out.println("加10天: " + plusDays); // 2026-03-02
        
        // 加周数
        LocalDate plusWeeks = date.plusWeeks(2);
        System.out.println("加2周: " + plusWeeks); // 2026-03-06
        
        // 加月数
        LocalDate plusMonths = date.plusMonths(1);
        System.out.println("加1月: " + plusMonths); // 2026-03-20
        
        // 加年数
        LocalDate plusYears = date.plusYears(5);
        System.out.println("加5年: " + plusYears); // 2031-02-20
        
        // 使用通用方法(ChronoUnit枚举)
        LocalDate plus = date.plus(3, ChronoUnit.WEEKS);
        System.out.println("加3周(ChronoUnit): " + plus); // 2026-03-13
        
        // 减法
        LocalDate minusDays = date.minusDays(5);
        System.out.println("减5天: " + minusDays); // 2026-02-15
        
        // 链式调用
        LocalDate result = date.plusYears(1).plusMonths(2).minusDays(3);
        System.out.println("链式运算: " + result); // 2027-04-17
    }
}
5.3.4 日期比较
java 复制代码
import java.time.LocalDate;

public class LocalDateCompare {
    public static void main(String[] args) {
        LocalDate date1 = LocalDate.of(2026, 2, 20);
        LocalDate date2 = LocalDate.of(2026, 3, 15);
        LocalDate date3 = LocalDate.of(2026, 2, 20);
        
        // 比较方法
        System.out.println("date1 before date2? " + date1.isBefore(date2)); // true
        System.out.println("date1 after date2? " + date1.isAfter(date2));   // false
        System.out.println("date1 equals date3? " + date1.equals(date3));   // true
        
        // compareTo方法(实现Comparable)
        int cmp = date1.compareTo(date2);
        System.out.println("compareTo结果: " + cmp); // 负数(-1或更小)
        
        // 与当前日期比较
        LocalDate now = LocalDate.now();
        System.out.println("date1是否早于今天? " + date1.isBefore(now));
    }
}

5.4 LocalTime------只处理时间

LocalTime表示时间(时、分、秒、纳秒),不包含日期和时区。

java 复制代码
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;

public class LocalTimeDemo {
    public static void main(String[] args) {
        // 1. 创建
        LocalTime now = LocalTime.now();
        System.out.println("当前时间: " + now); // 15:30:45.123
        
        LocalTime time1 = LocalTime.of(15, 30);           // 15:30
        LocalTime time2 = LocalTime.of(15, 30, 45);       // 15:30:45
        LocalTime time3 = LocalTime.of(15, 30, 45, 123456789); // 15:30:45.123456789
        
        LocalTime parsed = LocalTime.parse("15:30:45");
        LocalTime parsedCustom = LocalTime.parse("15-30-45", 
            DateTimeFormatter.ofPattern("HH-mm-ss"));
        
        // 2. 获取字段
        System.out.println("小时: " + time2.getHour());       // 15
        System.out.println("分钟: " + time2.getMinute());     // 30
        System.out.println("秒: " + time2.getSecond());       // 45
        System.out.println("纳秒: " + time2.getNano());       // 0
        
        // 3. 运算
        LocalTime plusHours = time2.plusHours(2);
        LocalTime plusMinutes = time2.plusMinutes(30);
        LocalTime plusSeconds = time2.plus(30, ChronoUnit.SECONDS);
        LocalTime minus = time2.minusHours(1);
        
        System.out.println("加2小时: " + plusHours);      // 17:30:45
        System.out.println("加30分: " + plusMinutes);     // 16:00:45
        System.out.println("减1小时: " + minus);           // 14:30:45
        
        // 4. 比较
        LocalTime timeA = LocalTime.of(10, 0);
        LocalTime timeB = LocalTime.of(14, 0);
        System.out.println("timeA before timeB? " + timeA.isBefore(timeB)); // true
    }
}

5.5 LocalDateTime------日期+时间(无时区)

LocalDateTime组合了LocalDateLocalTime,表示不带时区的完整日期时间。

java 复制代码
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;

public class LocalDateTimeDemo {
    public static void main(String[] args) {
        // 1. 创建
        LocalDateTime now = LocalDateTime.now();
        System.out.println("当前日期时间: " + now); // 2026-02-20T15:30:45.123
        
        // 从年月日时分秒
        LocalDateTime dt1 = LocalDateTime.of(2026, 2, 20, 15, 30);
        LocalDateTime dt2 = LocalDateTime.of(2026, 2, 20, 15, 30, 45);
        LocalDateTime dt3 = LocalDateTime.of(2026, 2, 20, 15, 30, 45, 123456789);
        
        // 组合LocalDate和LocalTime
        LocalDate date = LocalDate.of(2026, 2, 20);
        LocalTime time = LocalTime.of(15, 30, 45);
        LocalDateTime dt4 = LocalDateTime.of(date, time);
        
        // 从字符串解析
        LocalDateTime parsed = LocalDateTime.parse("2026-02-20T15:30:45");
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        LocalDateTime parsedCustom = LocalDateTime.parse("2026-02-20 15:30:45", formatter);
        
        System.out.println("解析结果: " + parsedCustom);
        
        // 2. 获取各部分
        LocalDate datePart = dt4.toLocalDate();
        LocalTime timePart = dt4.toLocalTime();
        System.out.println("日期部分: " + datePart);
        System.out.println("时间部分: " + timePart);
        
        // 获取字段
        System.out.println("年: " + dt4.getYear());
        System.out.println("月: " + dt4.getMonthValue());
        System.out.println("日: " + dt4.getDayOfMonth());
        System.out.println("时: " + dt4.getHour());
        
        // 3. 运算
        LocalDateTime tomorrow = dt4.plusDays(1);
        LocalDateTime nextWeek = dt4.plusWeeks(1);
        LocalDateTime nextMonth = dt4.plusMonths(1);
        LocalDateTime nextYear = dt4.plusYears(1);
        LocalDateTime plusHours = dt4.plusHours(2);
        LocalDateTime minusMinutes = dt4.minusMinutes(30);
        
        System.out.println("明天此刻: " + tomorrow);
        System.out.println("加2小时: " + plusHours);
        
        // 链式调用
        LocalDateTime result = dt4.plusYears(1)
                                   .plusMonths(2)
                                   .minusDays(3)
                                   .plusHours(4);
        System.out.println("链式结果: " + result);
        
        // 4. 使用TemporalAdjusters(更复杂的调整)
        // 例如:获取当月的最后一天
        LocalDateTime lastDayOfMonth = dt4.with(
            java.time.temporal.TemporalAdjusters.lastDayOfMonth()
        );
        System.out.println("当月最后一天: " + lastDayOfMonth);
    }
}

5.6 Instant------机器时间(时间戳)

Instant表示时间线上的一个瞬时点,从1970-01-01T00:00:00Z开始计算的秒数和纳秒数,是面向机器的表示。

java 复制代码
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;

public class InstantDemo {
    public static void main(String[] args) {
        // 1. 获取当前Instant(UTC时间)
        Instant now = Instant.now();
        System.out.println("当前Instant: " + now); // 2026-02-20T07:30:45.123Z(Z表示UTC)
        
        // 2. 从时间戳创建
        Instant fromEpochMilli = Instant.ofEpochMilli(1773084600000L); // 毫秒
        Instant fromEpochSecond = Instant.ofEpochSecond(1773084600L);  // 秒
        Instant fromEpochSecondWithNano = Instant.ofEpochSecond(1773084600L, 123456789); // 秒+纳秒
        
        System.out.println("毫秒创建: " + fromEpochMilli);
        
        // 3. 获取时间戳
        long epochSecond = now.getEpochSecond(); // 秒
        long epochMilli = now.toEpochMilli();    // 毫秒
        int nano = now.getNano();                 // 纳秒
        
        System.out.println("秒: " + epochSecond);
        System.out.println("毫秒: " + epochMilli);
        System.out.println("纳秒: " + nano);
        
        // 4. 运算
        Instant plusSeconds = now.plusSeconds(3600); // 加1小时
        Instant minusMillis = now.minusMillis(5000); // 减5秒
        
        // 5. 比较
        Instant earlier = Instant.now().minusSeconds(10);
        Instant later = Instant.now().plusSeconds(10);
        System.out.println("earlier before later? " + earlier.isBefore(later));
        
        // 6. 与Date转换
        Date date = Date.from(now);                    // Instant -> Date
        Instant instantFromDate = date.toInstant();     // Date -> Instant
        System.out.println("Date转回Instant: " + instantFromDate);
        
        // 7. 转换为带时区的日期时间
        ZonedDateTime beijingTime = now.atZone(ZoneId.of("Asia/Shanghai"));
        System.out.println("北京时间: " + beijingTime);
    }
}

关键理解

  • Instant总是UTC时间,不附带时区信息
  • 适合用于时间戳记录、计算时间差、跨时区传输等场景
  • Date可以互相转换,是连接新旧API的桥梁

5.7 ZonedDateTime------带时区的日期时间

ZonedDateTime包含日期、时间和时区信息,解决了跨时区应用的需求。

java 复制代码
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class ZonedDateTimeDemo {
    public static void main(String[] args) {
        // 1. 获取当前时区的日期时间
        ZonedDateTime now = ZonedDateTime.now();
        System.out.println("当前时区时间: " + now); // 2026-02-20T15:30:45.123+08:00[Asia/Shanghai]
        
        // 2. 指定时区
        ZonedDateTime nyNow = ZonedDateTime.now(ZoneId.of("America/New_York"));
        System.out.println("纽约时间: " + nyNow);
        
        // 3. 从LocalDateTime加时区
        LocalDateTime localDateTime = LocalDateTime.of(2026, 2, 20, 15, 30);
        ZonedDateTime zoned1 = localDateTime.atZone(ZoneId.of("Asia/Shanghai"));
        ZonedDateTime zoned2 = ZonedDateTime.of(localDateTime, ZoneId.of("Asia/Shanghai"));
        
        // 4. 直接指定
        ZonedDateTime zoned3 = ZonedDateTime.of(2026, 2, 20, 15, 30, 45, 0, 
                                                ZoneId.of("Asia/Shanghai"));
        
        // 5. 时区转换
        ZonedDateTime beijing = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
        ZonedDateTime newYork = beijing.withZoneSameInstant(ZoneId.of("America/New_York"));
        ZonedDateTime london = beijing.withZoneSameInstant(ZoneId.of("Europe/London"));
        
        System.out.println("北京时间: " + beijing);
        System.out.println("纽约时间: " + newYork);
        System.out.println("伦敦时间: " + london);
        
        // 6. 获取时区信息
        ZoneId zone = beijing.getZone();
        String zoneId = zone.getId(); // "Asia/Shanghai"
        System.out.println("时区ID: " + zoneId);
        
        // 7. 偏移量
        java.time.ZoneOffset offset = beijing.getOffset();
        System.out.println("偏移量: " + offset); // +08:00
        
        // 8. 格式化输出
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss zzzz");
        System.out.println("格式化: " + beijing.format(formatter)); // 2026-02-20 15:30:45 中国标准时间
    }
}

5.8 Period与Duration------时间间隔

Period用于日期之间的间隔(年、月、日),Duration用于时间之间的间隔(时、分、秒、纳秒)。

java 复制代码
import java.time.*;
import java.time.temporal.ChronoUnit;

public class PeriodDurationDemo {
    public static void main(String[] args) {
        // ========== Period(日期间隔)==========
        LocalDate startDate = LocalDate.of(2020, 1, 1);
        LocalDate endDate = LocalDate.of(2026, 2, 20);
        
        Period period = Period.between(startDate, endDate);
        System.out.println("日期差: " + period); // P6Y1M19D(6年1个月19天)
        System.out.println("年差: " + period.getYears());
        System.out.println("月差: " + period.getMonths());
        System.out.println("日差: " + period.getDays());
        
        // 创建Period
        Period ofYears = Period.ofYears(5);               // 5年
        Period ofMonths = Period.ofMonths(3);             // 3个月
        Period ofWeeks = Period.ofWeeks(2);                // 2周(即14天)
        Period ofDays = Period.ofDays(10);                 // 10天
        Period custom = Period.of(2, 6, 15);               // 2年6个月15天
        
        // 应用Period
        LocalDate newDate = startDate.plus(period);
        System.out.println("加上间隔后: " + newDate); // 2026-02-20
        
        // ========== Duration(时间间隔)==========
        LocalTime startTime = LocalTime.of(10, 0, 0);
        LocalTime endTime = LocalTime.of(15, 30, 45);
        
        Duration duration = Duration.between(startTime, endTime);
        System.out.println("时间差: " + duration); // PT5H30M45S
        System.out.println("小时差: " + duration.toHours()); // 5
        System.out.println("分钟差: " + duration.toMinutes()); // 330
        System.out.println("秒差: " + duration.getSeconds()); // 19845
        
        // 创建Duration
        Duration ofHours = Duration.ofHours(3);                // 3小时
        Duration ofMinutes = Duration.ofMinutes(45);           // 45分钟
        Duration ofSeconds = Duration.ofSeconds(120);          // 120秒
        Duration ofMillis = Duration.ofMillis(5000);           // 5秒
        Duration ofNanos = Duration.ofNanos(1000000);          // 1毫秒
        Duration of = Duration.of(2, ChronoUnit.HOURS);        // 2小时
        
        // 应用Duration
        LocalTime newTime = startTime.plus(duration);
        System.out.println("加上间隔后: " + newTime); // 15:30:45
        
        // 更精确的计算(考虑纳秒)
        Instant startInstant = Instant.now();
        // 模拟操作
        Instant endInstant = Instant.now().plusSeconds(3);
        Duration elapsed = Duration.between(startInstant, endInstant);
        System.out.println("耗时(秒): " + elapsed.getSeconds());
        System.out.println("耗时(毫秒): " + elapsed.toMillis());
    }
}

5.9 DateTimeFormatter------线程安全的格式化器

DateTimeFormatter是Java 8提供的格式化类,取代了SimpleDateFormat,并且是线程安全的。

java 复制代码
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;

public class DateTimeFormatterDemo {
    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();
        
        // ========== 1. 预定义格式化器 ==========
        DateTimeFormatter isoDate = DateTimeFormatter.ISO_DATE;
        DateTimeFormatter isoDateTime = DateTimeFormatter.ISO_DATE_TIME;
        
        System.out.println("ISO日期: " + now.format(isoDate));           // 2026-02-20
        System.out.println("ISO日期时间: " + now.format(isoDateTime));   // 2026-02-20T15:30:45.123
        
        // ========== 2. 本地化格式 ==========
        DateTimeFormatter fullDate = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL);
        DateTimeFormatter longDateTime = DateTimeFormatter.ofLocalizedDateTime(
            FormatStyle.LONG, FormatStyle.MEDIUM);
        
        System.out.println("本地化FULL: " + now.format(fullDate)); // 2026年2月20日 星期五
        System.out.println("本地化LONG/MEDIUM: " + now.format(longDateTime)); // 2026年2月20日 15:30:45
        
        // 指定Locale
        DateTimeFormatter usFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
                                                         .withLocale(Locale.US);
        System.out.println("美国格式: " + now.format(usFormatter)); // Friday, February 20, 2026
        
        // ========== 3. 自定义模式(最常用)==========
        DateTimeFormatter pattern1 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        DateTimeFormatter pattern2 = DateTimeFormatter.ofPattern("yyyy年MM月dd日 EEEE HH时mm分ss秒");
        DateTimeFormatter pattern3 = DateTimeFormatter.ofPattern("yyyy/MM/dd hh:mm:ss a");
        
        System.out.println("模式1: " + now.format(pattern1));
        System.out.println("模式2: " + now.format(pattern2));
        System.out.println("模式3: " + now.format(pattern3));
        
        // ========== 4. 解析字符串 ==========
        String dateStr = "2026-02-20 15:30:45";
        DateTimeFormatter parser = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        LocalDateTime parsed = LocalDateTime.parse(dateStr, parser);
        System.out.println("解析结果: " + parsed);
        
        // ========== 5. 线程安全演示 ==========
        // 可以在多个线程中共享同一个formatter实例
        DateTimeFormatter sharedFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        
        // 创建多个线程使用同一个formatter
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                String result = sharedFormatter.format(LocalDateTime.now());
                System.out.println(Thread.currentThread().getName() + ": " + result);
            }).start();
        }
        // 不会出现线程安全问题
    }
}

5.10 时区处理------ZoneId与ZoneOffset

java 复制代码
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Set;

public class ZoneDemo {
    public static void main(String[] args) {
        // ========== ZoneId(时区ID)==========
        // 1. 系统默认时区
        ZoneId defaultZone = ZoneId.systemDefault();
        System.out.println("默认时区: " + defaultZone); // Asia/Shanghai
        
        // 2. 通过ID获取
        ZoneId shanghai = ZoneId.of("Asia/Shanghai");
        ZoneId newYork = ZoneId.of("America/New_York");
        ZoneId utc = ZoneId.of("UTC");
        
        // 3. 所有可用时区
        Set<String> zoneIds = ZoneId.getAvailableZoneIds();
        System.out.println("总时区数: " + zoneIds.size()); // 约600个
        // 打印前10个
        zoneIds.stream().limit(10).forEach(System.out::println);
        
        // ========== ZoneOffset(偏移量)==========
        ZoneOffset offset1 = ZoneOffset.of("+08:00");
        ZoneOffset offset2 = ZoneOffset.ofHours(8);
        ZoneOffset offset3 = ZoneOffset.ofHoursMinutes(8, 30);
        ZoneOffset utcOffset = ZoneOffset.UTC; // 零偏移
        
        System.out.println("偏移量+8: " + offset1); // +08:00
        
        // 使用偏移量创建ZonedDateTime
        ZonedDateTime zonedWithOffset = ZonedDateTime.now(offset1);
        System.out.println("偏移量时间: " + zonedWithOffset);
        
        // 时区转换示例:东京时间转纽约时间
        ZonedDateTime tokyoTime = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
        ZonedDateTime nyTime = tokyoTime.withZoneSameInstant(ZoneId.of("America/New_York"));
        System.out.println("东京时间: " + tokyoTime);
        System.out.println("对应纽约时间: " + nyTime);
    }
}

5.11 日期时间调整------TemporalAdjusters

TemporalAdjusters提供了许多常用的日期调整工具。

java 复制代码
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;

public class TemporalAdjustersDemo {
    public static void main(String[] args) {
        LocalDate date = LocalDate.of(2026, 2, 20); // 星期五
        
        // 下一个/上一个/本月第一个/最后一个
        LocalDate nextMonday = date.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
        LocalDate previousSunday = date.with(TemporalAdjusters.previous(DayOfWeek.SUNDAY));
        LocalDate firstDayOfMonth = date.with(TemporalAdjusters.firstDayOfMonth());
        LocalDate lastDayOfMonth = date.with(TemporalAdjusters.lastDayOfMonth());
        LocalDate firstDayOfNextMonth = date.with(TemporalAdjusters.firstDayOfNextMonth());
        LocalDate firstDayOfYear = date.with(TemporalAdjusters.firstDayOfYear());
        LocalDate lastDayOfYear = date.with(TemporalAdjusters.lastDayOfYear());
        
        System.out.println("当前日期: " + date);
        System.out.println("下周一: " + nextMonday);
        System.out.println("上周日: " + previousSunday);
        System.out.println("本月第一天: " + firstDayOfMonth);
        System.out.println("本月最后一天: " + lastDayOfMonth);
        
        // 本月第几个星期几
        LocalDate thirdFriday = date.with(TemporalAdjusters.dayOfWeekInMonth(3, DayOfWeek.FRIDAY));
        System.out.println("本月第三个星期五: " + thirdFriday);
        
        // 下个月的今天(如果存在)
        LocalDate nextMonthSameDay = date.plusMonths(1);
        System.out.println("下个月今天: " + nextMonthSameDay);
    }
}

第六章:新旧API对比与转换指南

6.1 三代API对比总结

维度 第一代 (Date/DateFormat) 第二代 (Calendar) 第三代 (java.time)
核心类 Date, SimpleDateFormat Calendar, GregorianCalendar LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant
不可变性 可变 可变 不可变
线程安全 不安全 不安全 安全
月份偏移 0-11 0-11 1-12
年份偏移 year-1900 正常 正常
API设计 混乱 繁琐 清晰流畅
时区支持 强大
性能 一般 较差(重量级) 优秀
可读性

6.2 何时使用旧API,何时使用新API

使用旧API的场景(仅限于维护遗留代码):

  • 正在维护使用JDK 7及以下版本的项目
  • 与某些旧框架集成(如Hibernate早期版本)
  • 需要与遗留数据库交互(部分JDBC驱动仍使用java.sql.Date/Timestamp

强烈推荐使用新API的场景(所有新项目):

  • JDK 8及以上版本的新开发
  • 需要复杂的日期计算
  • 涉及时区转换
  • 多线程环境下的日期处理
  • 希望代码更清晰、更易维护

6.3 新旧API转换工具类

在实际开发中,经常需要在新旧API之间转换(例如,数据库操作可能返回java.sql.Date)。下面是一个完整的转换工具类:

java 复制代码
import java.time.*;
import java.util.Date;
import java.util.Calendar;
import java.util.GregorianCalendar;

/**
 * 新旧日期时间API转换工具类
 */
public class DateTimeConversionUtil {
    
    // ========== java.util.Date <-> java.time ==========
    
    /**
     * Date -> Instant
     */
    public static Instant toInstant(Date date) {
        return date == null ? null : date.toInstant();
    }
    
    /**
     * Instant -> Date
     */
    public static Date toDate(Instant instant) {
        return instant == null ? null : Date.from(instant);
    }
    
    /**
     * Date -> LocalDate(系统默认时区)
     */
    public static LocalDate toLocalDate(Date date) {
        if (date == null) return null;
        return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
    }
    
    /**
     * LocalDate -> Date(系统默认时区)
     */
    public static Date toDate(LocalDate localDate) {
        if (localDate == null) return null;
        return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
    }
    
    /**
     * Date -> LocalDateTime(系统默认时区)
     */
    public static LocalDateTime toLocalDateTime(Date date) {
        if (date == null) return null;
        return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
    }
    
    /**
     * LocalDateTime -> Date(系统默认时区)
     */
    public static Date toDate(LocalDateTime localDateTime) {
        if (localDateTime == null) return null;
        return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
    }
    
    /**
     * Date -> ZonedDateTime(系统默认时区)
     */
    public static ZonedDateTime toZonedDateTime(Date date) {
        if (date == null) return null;
        return date.toInstant().atZone(ZoneId.systemDefault());
    }
    
    /**
     * ZonedDateTime -> Date
     */
    public static Date toDate(ZonedDateTime zonedDateTime) {
        if (zonedDateTime == null) return null;
        return Date.from(zonedDateTime.toInstant());
    }
    
    // ========== java.util.Calendar <-> java.time ==========
    
    /**
     * Calendar -> Instant
     */
    public static Instant toInstant(Calendar calendar) {
        if (calendar == null) return null;
        return calendar.toInstant();
    }
    
    /**
     * Instant -> GregorianCalendar
     */
    public static GregorianCalendar toCalendar(Instant instant) {
        if (instant == null) return null;
        return GregorianCalendar.from(instant.atZone(ZoneId.systemDefault()));
    }
    
    /**
     * Calendar -> ZonedDateTime
     */
    public static ZonedDateTime toZonedDateTime(Calendar calendar) {
        if (calendar == null) return null;
        return ZonedDateTime.ofInstant(calendar.toInstant(), calendar.getTimeZone().toZoneId());
    }
    
    /**
     * ZonedDateTime -> GregorianCalendar
     */
    public static GregorianCalendar toCalendar(ZonedDateTime zonedDateTime) {
        if (zonedDateTime == null) return null;
        return GregorianCalendar.from(zonedDateTime);
    }
    
    /**
     * Calendar -> LocalDateTime
     */
    public static LocalDateTime toLocalDateTime(Calendar calendar) {
        if (calendar == null) return null;
        return LocalDateTime.ofInstant(calendar.toInstant(), calendar.getTimeZone().toZoneId());
    }
    
    /**
     * LocalDateTime -> GregorianCalendar(指定时区)
     */
    public static GregorianCalendar toCalendar(LocalDateTime localDateTime, ZoneId zoneId) {
        if (localDateTime == null || zoneId == null) return null;
        return GregorianCalendar.from(localDateTime.atZone(zoneId));
    }
    
    // ========== java.sql 相关 ==========
    
    /**
     * java.sql.Date -> LocalDate
     */
    public static LocalDate toLocalDate(java.sql.Date sqlDate) {
        return sqlDate == null ? null : sqlDate.toLocalDate();
    }
    
    /**
     * LocalDate -> java.sql.Date
     */
    public static java.sql.Date toSqlDate(LocalDate localDate) {
        return localDate == null ? null : java.sql.Date.valueOf(localDate);
    }
    
    /**
     * java.sql.Timestamp -> LocalDateTime
     */
    public static LocalDateTime toLocalDateTime(java.sql.Timestamp timestamp) {
        return timestamp == null ? null : timestamp.toLocalDateTime();
    }
    
    /**
     * LocalDateTime -> java.sql.Timestamp
     */
    public static java.sql.Timestamp toTimestamp(LocalDateTime localDateTime) {
        return localDateTime == null ? null : java.sql.Timestamp.valueOf(localDateTime);
    }
    
    /**
     * java.sql.Time -> LocalTime
     */
    public static LocalTime toLocalTime(java.sql.Time sqlTime) {
        return sqlTime == null ? null : sqlTime.toLocalTime();
    }
    
    /**
     * LocalTime -> java.sql.Time
     */
    public static java.sql.Time toSqlTime(LocalTime localTime) {
        return localTime == null ? null : java.sql.Time.valueOf(localTime);
    }
    
    // ========== 使用示例 ==========
    public static void main(String[] args) {
        // Date <-> LocalDateTime
        Date now = new Date();
        LocalDateTime ldt = toLocalDateTime(now);
        Date backToDate = toDate(ldt);
        System.out.println("原始Date: " + now);
        System.out.println("转LocalDateTime: " + ldt);
        System.out.println("转回Date: " + backToDate);
        System.out.println("是否相等: " + now.equals(backToDate));
        
        // Calendar <-> ZonedDateTime
        Calendar calendar = Calendar.getInstance();
        ZonedDateTime zdt = toZonedDateTime(calendar);
        GregorianCalendar backToCal = toCalendar(zdt);
        System.out.println("原始Calendar: " + calendar.getTime());
        System.out.println("转ZonedDateTime: " + zdt);
        System.out.println("转回Calendar: " + backToCal.getTime());
    }
}

第七章:最佳实践与常见陷阱

7.1 开发中应该选择哪个API?

决策树

  1. 项目JDK版本 ≥ 8? → 使用 java.time
  2. 需要与遗留代码/库交互? → 在边界处转换,核心逻辑仍用 java.time
  3. 需要数据库操作? → 使用 java.time 类型与JPA 2.2+(支持LocalDate等)
  4. 需要高性能、线程安全的格式化? → 使用 DateTimeFormatter
  5. 维护JDK 7及以下项目? → 只能使用 Calendar + SimpleDateFormat(注意线程安全)

7.2 常见陷阱与解决方案

陷阱1:月份从0开始(Calendar/Date)
java 复制代码
// 错误
Calendar cal = Calendar.getInstance();
cal.set(2026, 2, 20); // 以为是2月20日,实际是3月20日

// 正确
cal.set(2026, Calendar.FEBRUARY, 20); // 使用Calendar常量
// 或者
cal.set(2026, 1, 20); // 月份0=1月,1=2月

// 最佳:使用Java 8
LocalDate date = LocalDate.of(2026, 2, 20); // 直接使用2
陷阱2:SimpleDateFormat线程不安全
java 复制代码
// 错误
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 多个线程共用,导致数据错乱

// 正确
private static final ThreadLocal<SimpleDateFormat> sdfHolder = 
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

// 最佳
private static final DateTimeFormatter formatter = 
    DateTimeFormatter.ofPattern("yyyy-MM-dd");
陷阱3:时区混淆
java 复制代码
// 错误:认为LocalDateTime有时区
LocalDateTime now = LocalDateTime.now(); // 实际上只是系统默认时区的本地时间,不包含时区信息

// 需要时区时应使用ZonedDateTime
ZonedDateTime zonedNow = ZonedDateTime.now();

// 跨时区转换正确做法
ZonedDateTime beijing = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDateTime newYork = beijing.withZoneSameInstant(ZoneId.of("America/New_York"));
陷阱4:时间精度丢失
java 复制代码
// Date只能精确到毫秒
Date date = new Date();
long millis = date.getTime(); // 毫秒

// Instant可以精确到纳秒
Instant instant = Instant.now();
long seconds = instant.getEpochSecond();
int nanos = instant.getNano(); // 纳秒部分

// 互相转换时可能丢失精度
Instant nanoInstant = Instant.now();
Date dateFromInstant = Date.from(nanoInstant); // 纳秒部分被截断为毫秒
陷阱5:Period与Duration混淆
java 复制代码
// Period用于日期(年、月、日)
LocalDate start = LocalDate.of(2026, 1, 1);
LocalDate end = LocalDate.of(2026, 2, 20);
Period period = Period.between(start, end);
System.out.println(period.getDays()); // 19?不对,是19天,但月份部分也是1个月

// Duration用于时间(时、分、秒)
LocalTime startTime = LocalTime.of(10, 0);
LocalTime endTime = LocalTime.of(15, 30);
Duration duration = Duration.between(startTime, endTime);
System.out.println(duration.toMinutes()); // 330分钟

// 不要用Period计算时间差,用Duration

7.3 性能考虑

  1. java.time 性能优于 CalendarCalendar内部有复杂的字段计算和同步开销
  2. DateTimeFormatter 重用 :由于线程安全,可以定义为static final常量重用
  3. 避免频繁创建 Instant/LocalDateTime :除非必要,否则使用now()获取当前时间即可
  4. 大量日期计算时考虑使用 java.time:API设计更高效

7.4 代码示例:业务场景实战

场景1:计算年龄
java 复制代码
import java.time.LocalDate;
import java.time.Period;

public class AgeCalculator {
    public static int calculateAge(LocalDate birthDate) {
        LocalDate today = LocalDate.now();
        return Period.between(birthDate, today).getYears();
    }
    
    public static void main(String[] args) {
        LocalDate birth = LocalDate.of(1990, 5, 15);
        int age = calculateAge(birth);
        System.out.println("年龄: " + age); // 根据当前日期计算
    }
}
场景2:订单超时判断(30分钟未支付取消)
java 复制代码
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;

public class OrderTimeout {
    public static boolean isTimeout(LocalDateTime orderTime, int timeoutMinutes) {
        LocalDateTime now = LocalDateTime.now();
        long minutesElapsed = ChronoUnit.MINUTES.between(orderTime, now);
        return minutesElapsed >= timeoutMinutes;
    }
    
    public static void main(String[] args) {
        LocalDateTime orderTime = LocalDateTime.now().minusMinutes(25);
        boolean timeout = isTimeout(orderTime, 30);
        System.out.println("订单是否超时: " + timeout); // false
        
        orderTime = LocalDateTime.now().minusMinutes(35);
        timeout = isTimeout(orderTime, 30);
        System.out.println("订单是否超时: " + timeout); // true
    }
}
场景3:获取某月的所有周末
java 复制代码
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.YearMonth;
import java.util.ArrayList;
import java.util.List;

public class WeekendsInMonth {
    public static List<LocalDate> getWeekends(int year, int month) {
        List<LocalDate> weekends = new ArrayList<>();
        YearMonth yearMonth = YearMonth.of(year, month);
        LocalDate firstOfMonth = yearMonth.atDay(1);
        LocalDate lastOfMonth = yearMonth.atEndOfMonth();
        
        LocalDate date = firstOfMonth;
        while (!date.isAfter(lastOfMonth)) {
            DayOfWeek dow = date.getDayOfWeek();
            if (dow == DayOfWeek.SATURDAY || dow == DayOfWeek.SUNDAY) {
                weekends.add(date);
            }
            date = date.plusDays(1);
        }
        return weekends;
    }
    
    public static void main(String[] args) {
        List<LocalDate> weekends = getWeekends(2026, 2);
        System.out.println("2026年2月周末:");
        weekends.forEach(System.out::println);
    }
}
场景4:国际化日期显示
java 复制代码
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;

public class I18nDateDemo {
    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();
        
        // 中文显示
        DateTimeFormatter chineseFormatter = DateTimeFormatter
            .ofLocalizedDateTime(FormatStyle.FULL, FormatStyle.MEDIUM)
            .withLocale(Locale.CHINA);
        System.out.println("中文: " + now.format(chineseFormatter));
        
        // 英文显示
        DateTimeFormatter usFormatter = DateTimeFormatter
            .ofLocalizedDateTime(FormatStyle.FULL, FormatStyle.MEDIUM)
            .withLocale(Locale.US);
        System.out.println("英文: " + now.format(usFormatter));
        
        // 日文显示
        DateTimeFormatter japanFormatter = DateTimeFormatter
            .ofLocalizedDateTime(FormatStyle.FULL, FormatStyle.MEDIUM)
            .withLocale(Locale.JAPAN);
        System.out.println("日文: " + now.format(japanFormatter));
    }
}

第八章:总结与展望

8.1 三代API的演进之路回顾

  1. 第一代(Date/DateFormat):JDK 1.0诞生,设计简陋,存在年份偏移、月份从0开始、线程不安全等问题,大部分方法已废弃

  2. 第二代(Calendar):JDK 1.1引入,试图弥补Date的缺陷,但API复杂、月份偏移问题依旧、线程不安全,且性能较差

  3. 第三代(java.time):JDK 8引入,基于JSR 310,设计优雅,不可变、线程安全、API清晰、月份从1开始,是处理日期时间的首选

8.2 核心要点总结

核心类 用途 关键特性
Date 表示时间戳(已过时) 可变、线程不安全、月份0-11
Calendar 日历字段操作(已过时) 可变、线程不安全、月份0-11、API繁琐
LocalDate 日期(无时间) 不可变、线程安全、月份1-12
LocalTime 时间(无日期) 不可变、线程安全
LocalDateTime 日期+时间(无时区) 不可变、线程安全
ZonedDateTime 带时区的日期时间 不可变、线程安全、时区转换
Instant 时间戳(机器时间) 不可变、线程安全、UTC
DateTimeFormatter 格式化/解析 线程安全、取代SimpleDateFormat
Period/Duration 时间间隔 不可变、线程安全

8.3 未来展望

随着Java的持续发展,日期时间API也在不断完善:

  • JDK 9+ :对java.time包进行了细微优化和bug修复
  • 未来版本 :可能会引入更多便捷的日期时间操作方法,与RecordPattern Matching等新特性更好地集成

8.4 给开发者的建议

  1. 新项目一律使用 java.time,告别旧API的烦恼
  2. 维护旧项目时 ,在边界层(如Controller、DAO)进行新旧API转换,核心业务逻辑尽量使用java.time
  3. 注意线程安全SimpleDateFormat在多线程环境下必须采取保护措施
  4. 理解时区概念,区分本地时间、UTC时间、带时区时间
  5. 善用 DateTimeFormatter,它是线程安全的,可以定义为常量复用
  6. 阅读官方文档java.time包的Javadoc,掌握更多高级特性(如TemporalQueryTemporalAdjuster等)

附录:常用代码片段速查

获取当前时间

java 复制代码
// 旧
Date now = new Date();
Calendar cal = Calendar.getInstance();

// 新
LocalDate today = LocalDate.now();
LocalTime nowTime = LocalTime.now();
LocalDateTime nowDateTime = LocalDateTime.now();
Instant instant = Instant.now();

创建指定日期

java 复制代码
// 旧
Calendar cal = Calendar.getInstance();
cal.set(2026, Calendar.FEBRUARY, 20); // 注意月份常量

// 新
LocalDate date = LocalDate.of(2026, 2, 20);
LocalDateTime dt = LocalDateTime.of(2026, 2, 20, 15, 30, 45);

日期加减

java 复制代码
// 旧
cal.add(Calendar.DAY_OF_MONTH, 5);
cal.add(Calendar.MONTH, -2);

// 新
LocalDate newDate = date.plusDays(5).minusMonths(2);

格式化

java 复制代码
// 旧(注意线程安全问题)
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String formatted = sdf.format(new Date());

// 新
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted = LocalDateTime.now().format(dtf);

解析字符串

java 复制代码
// 旧
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = sdf.parse("2026-02-20");

// 新
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate date = LocalDate.parse("2026-02-20", dtf);

计算两个日期的天数差

java 复制代码
// 旧(繁琐)
long days = (date2.getTime() - date1.getTime()) / (24*60*60*1000);

// 新
long days = ChronoUnit.DAYS.between(date1, date2);

时区转换

java 复制代码
// 旧(麻烦)
TimeZone tz = TimeZone.getTimeZone("America/New_York");
Calendar cal = Calendar.getInstance(tz);

// 新
ZonedDateTime nyTime = ZonedDateTime.now(ZoneId.of("America/New_York"));
ZonedDateTime shanghaiTime = nyTime.withZoneSameInstant(ZoneId.of("Asia/Shanghai"));
相关推荐
A懿轩A1 小时前
【Java 基础编程】Java 枚举与注解从零到一:Enum 用法 + 常用注解 + 自定义注解实战
java·开发语言·python
mjhcsp2 小时前
C++ 树形 DP解析
开发语言·c++·动态规划·代理模式
树码小子2 小时前
图书管理系统(2)图书列表接口
spring boot·mybatis·图书管理系统
tuokuac2 小时前
MyBatis-Plus调用getEntity()触发异常
java·mybatis
_但为君故_2 小时前
优化Tomcat的JVM内存
java·jvm·tomcat
yaoxin5211232 小时前
328. Java Stream API - 使用 Optional 的正确姿势:为何、何时、如何使用
java·开发语言
岱宗夫up2 小时前
从代码模式到智能模式:AI时代的设计模式进化论
开发语言·python·深度学习·神经网络·自然语言处理·知识图谱
我命由我123452 小时前
Visual Studio 文件的编码格式不一致问题:错误 C2001 常量中有换行符
c语言·开发语言·c++·ide·学习·学习方法·visual studio
MR_Promethus2 小时前
【C++类型转换】static_cast、dynamic_cast、const_cast、reinterpret_cast
开发语言·c++