跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava

Java 日期时间 API 详解:从 Date、Calendar 到 java.time

Java 日期时间 API 演进,涵盖第一代 Date、第二代 Calendar 及第三代 java.time。重点分析各代 API 的设计缺陷(如月份偏移、线程不安全),对比优缺点,并提供最佳实践与新旧 API 转换指南。推荐使用不可变且线程安全的 java.time 包处理日期时间。

CryptoLab发布于 2026/3/24更新于 2026/8/1952 浏览
Java 日期时间 API 详解:从 Date、Calendar 到 java.time

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

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

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

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

  • 第一代:Date、DateFormat、SimpleDateFormat
  • 第二代:Calendar、GregorianCalendar、TimeZone
  • 第三代:LocalDate、LocalTime、LocalDateTime、ZonedDateTime、Instant、DateTimeFormatter 等

我们将从源码层面剖析其设计原理,探讨线程安全问题,提供最佳实践,并给出新旧 API 的转换指南。


第一章:时间的基础概念

在深入 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 到当前时间的毫秒数。

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 为例),可以看到其核心实现:

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 对象
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 日期比较
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 获取/设置毫秒数
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(北京时间,因为东八区)
        now.setTime(0L);
        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 年开始
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 开始
@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:可变性导致的线程安全问题
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:国际化支持薄弱
// 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"

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

缺陷 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 类
// 推荐的使用方式:仅将 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.FULL0完整格式,如"2026 年 2 月 20 日 星期五"
DateFormat.LONG1长格式,如"2026 年 2 月 20 日"
DateFormat.MEDIUM2中等格式,如"2026-2-20"(默认风格)
DateFormat.SHORT3短格式,如"26-2-20"
3.1.2 获取实例的工厂方法
// 获取日期格式化器
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 核心方法
// Date -> String 格式化
public final String format(Date date)

// String -> Date 解析
public Date parse(String source) throws ParseException
3.1.4 使用示例
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.SimpleDateFormat 是 DateFormat 的具体子类,允许通过模式字符串自定义日期时间格式。

3.2.1 构造方法
// 使用默认模式
SimpleDateFormat()
// 使用指定模式
SimpleDateFormat(String pattern)
// 使用指定模式和区域
SimpleDateFormat(String pattern, Locale locale)
3.2.2 常用模式字母
字母日期/时间元素示例
y年yyyy -> 2026
M月份MM -> 02, MMM -> 二月
d月份中的天数dd -> 20
E星期几EEEE -> 星期五
aAM/PM 标记a -> 下午
H小时(0-23)HH -> 15
h小时(1-12)hh -> 03
m分钟mm -> 30
s秒ss -> 45
S毫秒SSS -> 123
z时区z -> CST
ZRFC 822 时区Z -> +0800
3.2.3 基本使用示例
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 模式匹配规则详解
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 中定义了一个 protected 的 Calendar 对象:

// 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;
    }
}

问题分析:

  • calendar 是 DateFormat 类的成员变量,被 SimpleDateFormat 继承
  • 在 format() 方法中,首先调用 calendar.setTime(date) 修改 calendar 的状态
  • 在多线程环境下,如果多个线程共享同一个 SimpleDateFormat 实例,线程 A 执行 calendar.setTime() 后可能被暂停,线程 B 开始执行并再次修改 calendar,导致线程 A 后续使用的 calendar 状态已被改变,产生错误结果甚至程序崩溃
3.3.2 问题复现
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:每次使用时创建新实例

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

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:同步加锁

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(最佳方案)

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 类的源码结构
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(),如果 isTimeSet 为 false,则调用 computeTime() 从 fields[] 计算 time;如果 fields[] 不完整,则调用 computeFields() 从 time 计算 fields[]。这种延迟计算提高了性能。
  3. 17 个常量:这些常量作为 fields[] 数组的索引,每个常量代表一个日历字段。理解这些常量是使用 Calendar 的基础。
4.3 获取 Calendar 实例

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

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)
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)
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() 方法按照日历规则,对指定字段增加/减少指定值,会进位到更高字段。

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() 类似,但不会进位到更高字段。

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 清空与设置宽松模式
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 的转换
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:计算两个日期之间的天数差
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:获取某月的第一天和最后一天
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:判断闰年
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时区 IDAsia/Shanghai
ZoneOffset时区偏移量+08:00
5.3 LocalDate——只处理日期

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

5.3.1 创建 LocalDate
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 获取字段值
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 对象,原对象不变。

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 日期比较
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 表示时间(时、分、秒、纳秒),不包含日期和时区。

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 组合了 LocalDate 和 LocalTime,表示不带时区的完整日期时间。

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); // 2026-02-20 15:30:45

        // 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 开始计算的秒数和纳秒数,是面向机器的表示。

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)); // true

        // 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 包含日期、时间和时区信息,解决了跨时区应用的需求。

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 用于时间之间的间隔(时、分、秒、纳秒)。

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,并且是线程安全的。

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
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 提供了许多常用的日期调整工具。

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, SimpleDateFormatCalendar, GregorianCalendarLocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant
不可变性可变可变不可变
线程安全不安全不安全安全
月份偏移0-110-111-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)。下面是一个完整的转换工具类:

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)
// 错误
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 线程不安全
// 错误
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:时区混淆
// 错误:认为 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:时间精度丢失
// 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 混淆
// 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 性能优于 Calendar:Calendar 内部有复杂的字段计算和同步开销
  2. DateTimeFormatter 重用:由于线程安全,可以定义为 static final 常量重用
  3. 避免频繁创建 Instant/LocalDateTime:除非必要,否则使用 now() 获取当前时间即可
  4. 大量日期计算时考虑使用 java.time:API 设计更高效
7.4 代码示例:业务场景实战
场景 1:计算年龄
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 分钟未支付取消)
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:获取某月的所有周末
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:国际化日期显示
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 修复
  • 未来版本:可能会引入更多便捷的日期时间操作方法,与 Record、Pattern Matching 等新特性更好地集成
8.4 给开发者的建议
  1. 新项目一律使用 java.time 包,告别旧 API 的烦恼
  2. 维护旧项目时,在边界层(如 Controller、DAO)进行新旧 API 转换,核心业务逻辑尽量使用 java.time
  3. 注意线程安全,SimpleDateFormat 在多线程环境下必须采取保护措施
  4. 理解时区概念,区分本地时间、UTC 时间、带时区时间
  5. 善用 DateTimeFormatter,它是线程安全的,可以定义为常量复用
  6. 阅读官方文档和 java.time 包的 Javadoc,掌握更多高级特性(如 TemporalQuery、TemporalAdjuster 等)

附录:常用代码片段速查

获取当前时间
// 旧
Date now = new Date();
Calendar cal = Calendar.getInstance();

// 新
LocalDate today = LocalDate.now();
LocalTime nowTime = LocalTime.now();
LocalDateTime nowDateTime = LocalDateTime.now();
Instant instant = Instant.now();
创建指定日期
// 旧
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);
日期加减
// 旧
cal.add(Calendar.DAY_OF_MONTH, 5);
cal.add(Calendar.MONTH, -2);

// 新
LocalDate newDate = date.plusDays(5).minusMonths(2);
格式化
// 旧(注意线程安全问题)
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String formatted = sdf.format(newDate());

// 新
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted = LocalDateTime.now().format(dtf);
解析字符串
// 旧
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);
计算两个日期的天数差
// 旧(繁琐)
long days = (date2.getTime() - date1.getTime()) / (24 * 60 * 60 * 1000);

// 新
long days = ChronoUnit.DAYS.between(date1, date2);
时区转换
// 旧(麻烦)
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"));

目录

  1. 引言:Java 日期时间处理的演进之路
  2. 第一章:时间的基础概念
  3. 1.1 时间原点:1970-01-01 UTC
  4. 1.2 时间表示的两种模型
  5. 1.3 时区与历法
  6. 第二章:第一代日期时间 API——Date
  7. 2.1 Date 类的源码剖析
  8. 2.2 Date 类的核心方法详解
  9. 2.2.1 创建 Date 对象
  10. 2.2.2 日期比较
  11. 2.2.3 获取/设置毫秒数
  12. 2.3 Date 类的设计缺陷(为什么被废弃)
  13. 缺陷 1:年份从 1900 年开始
  14. 缺陷 2:月份从 0 开始
  15. 缺陷 3:可变性导致的线程安全问题
  16. 缺陷 4:国际化支持薄弱
  17. 缺陷 5:命名混淆
  18. 2.4 正确使用 Date 的建议
  19. 第三章:日期格式化——DateFormat 与 SimpleDateFormat
  20. 3.1 DateFormat 抽象类
  21. 3.1.1 预定义样式常量
  22. 3.1.2 获取实例的工厂方法
  23. 3.1.3 核心方法
  24. 3.1.4 使用示例
  25. 3.2 SimpleDateFormat——更灵活的格式化器
  26. 3.2.1 构造方法
  27. 3.2.2 常用模式字母
  28. 3.2.3 基本使用示例
  29. 3.2.4 模式匹配规则详解
  30. 3.3 SimpleDateFormat 的线程安全问题
  31. 3.3.1 问题根源——源码分析
  32. 3.3.2 问题复现
  33. 3.3.3 解决方案
  34. 第四章:第二代日期时间 API——Calendar
  35. 4.1 Calendar 类的设计思想
  36. 4.2 Calendar 类的源码结构
  37. 4.3 获取 Calendar 实例
  38. 4.4 Calendar 核心方法详解
  39. 4.4.1 获取字段值:get(int field)
  40. 4.4.2 设置字段值:set(int field, int value) 和 set(int year, int month, int date)
  41. 4.4.3 日期计算:add(int field, int amount)
  42. 4.4.4 日期滚动:roll(int field, int amount)
  43. 4.4.5 清空与设置宽松模式
  44. 4.5 Calendar 的优缺点分析
  45. 优点
  46. 缺点
  47. 4.6 Calendar 与 Date 的转换
  48. 4.7 Calendar 常用场景示例
  49. 场景 1:计算两个日期之间的天数差
  50. 场景 2:获取某月的第一天和最后一天
  51. 场景 3:判断闰年
  52. 第五章:第三代日期时间 API——java.time(JSR 310)
  53. 5.1 设计哲学与核心原则
  54. 5.2 核心类概览
  55. 5.3 LocalDate——只处理日期
  56. 5.3.1 创建 LocalDate
  57. 5.3.2 获取字段值
  58. 5.3.3 日期运算(加减)
  59. 5.3.4 日期比较
  60. 5.4 LocalTime——只处理时间
  61. 5.5 LocalDateTime——日期 + 时间(无时区)
  62. 5.6 Instant——机器时间(时间戳)
  63. 5.7 ZonedDateTime——带时区的日期时间
  64. 5.8 Period 与 Duration——时间间隔
  65. 5.9 DateTimeFormatter——线程安全的格式化器
  66. 5.10 时区处理——ZoneId 与 ZoneOffset
  67. 5.11 日期时间调整——TemporalAdjusters
  68. 第六章:新旧 API 对比与转换指南
  69. 6.1 三代 API 对比总结
  70. 6.2 何时使用旧 API,何时使用新 API
  71. 6.3 新旧 API 转换工具类
  72. 第七章:最佳实践与常见陷阱
  73. 7.1 开发中应该选择哪个 API?
  74. 7.2 常见陷阱与解决方案
  75. 陷阱 1:月份从 0 开始(Calendar/Date)
  76. 陷阱 2:SimpleDateFormat 线程不安全
  77. 陷阱 3:时区混淆
  78. 陷阱 4:时间精度丢失
  79. 陷阱 5:Period 与 Duration 混淆
  80. 7.3 性能考虑
  81. 7.4 代码示例:业务场景实战
  82. 场景 1:计算年龄
  83. 场景 2:订单超时判断(30 分钟未支付取消)
  84. 场景 3:获取某月的所有周末
  85. 场景 4:国际化日期显示
  86. 第八章:总结与展望
  87. 8.1 三代 API 的演进之路回顾
  88. 8.2 核心要点总结
  89. 8.3 未来展望
  90. 8.4 给开发者的建议
  91. 附录:常用代码片段速查
  92. 获取当前时间
  93. 创建指定日期
  94. 日期加减
  95. 格式化
  96. 解析字符串
  97. 计算两个日期的天数差
  98. 时区转换
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Ubuntu 虚拟机部署 OpenClaw 个人 AI 助手
  • 9 种降低论文 AIGC 检测率的工具推荐
  • 基于 DevUI 与 MateChat 构建企业级 AI 智能助手
  • Matplotlib 图例进阶:plt.legend() 深度定制与实战技巧
  • 网络安全工程师面试真题整理(116 道)
  • 基于 JeecgBoot 低代码平台构建请假审批系统实战
  • 扫频信号的时频特性与测试实践
  • 牛客 NC221681 dd 爱框框 滑动窗口解法
  • 程序员进阶技术专家的核心能力与源码深度解析
  • Linux 线程池封装与实现详解
  • System Verilog 教程:从基础概念到高级验证
  • Android 开发中创建 AVD 时指定 SD 卡镜像的方法
  • 数据结构与算法:递归原理及 Java 实现案例
  • VibeVoice Pro 结合 Whisper+Llama3 构建语音交互链教程
  • C++ 实战:构建基于对话框的搜索引擎
  • 利用 Anonymous GitHub 创建双盲评审匿名代码链接
  • Whisper 语音识别微调及多端部署方案
  • 2026 AI 办公实战:3 个真实案例与效率提升指南
  • 自动化 B 站直播间搭建与神奇弹幕机器人配置指南
  • 上下文协议(MCP)Java SDK 指南

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online