引言: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());
}
// ... 其他方法
}
关键点分析:
- 核心字段
fastTime:这是一个long类型的变量,存储从 1970-01-01 UTC 开始的毫秒数。整个Date对象本质上就是这个数字的封装。 - 构造方法:只有两个构造方法被保留推荐使用——无参构造(获取当前时间)和带毫秒参数的构造。其他构造方法都被
@Deprecated标记,不再推荐使用。 - 主要方法:
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 的建议
鉴于上述缺陷,官方建议:
- 只使用两个推荐的方法:
new Date()和new Date(long)构造、getTime()、setTime()、before()、after()、compareTo() - 不要使用任何
@Deprecated标记的方法 - 将 Date 作为"时间戳"的载体,不直接操作其日历字段
- 在需要日历字段操作时,使用 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.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 获取实例的工厂方法
// 获取日期格式化器
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 -> 星期五 |
| 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 基本使用示例
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());
}
}
关键设计解析:
- 双重表示:
Calendar内部同时维护两种表示——time(毫秒数)和fields[](字段数组)。当修改字段时,isTimeSet标记设为false,表示time需要重新计算;当设置时间时,fields[]会被重新计算。 - 延迟计算:
get(int field)方法调用complete(),如果isTimeSet为false,则调用computeTime()从fields[]计算time;如果fields[]不完整,则调用computeFields()从time计算fields[]。这种延迟计算提高了性能。 - 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 的优缺点分析
优点
- 丰富的字段操作:提供了 17 个日历字段,可以精确获取和设置各个部分
- 强大的计算能力:
add()和roll()方法支持灵活的日期运算 - 国际化支持:内置时区和 Locale 处理
- 历法扩展性:可以支持不同的历法系统
缺点
- API 复杂繁琐:使用时需要记住大量常量,代码冗长
- 月份偏移陷阱:月份从 0 开始,极易出错
- 线程不安全:内部状态可变,多线程共享需同步
- 可变性:方法调用会修改对象内部状态,不符合函数式编程思想
- 性能开销:相对重量级,频繁创建有性能问题
- 设计缺陷:某些方法行为不够直观(如
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 的设计遵循以下核心原则:
- 不可变性:所有核心类都是
final的,且内部状态不可变,任何修改操作都返回新对象,天然线程安全 - 清晰分离:将日期、时间、日期时间、带时区的日期时间等概念清晰分离,通过不同类表示
- 流畅 API:方法命名直观,支持链式调用
- ISO 标准:默认采用 ISO-8601 国际标准
- 线程安全:所有类都是不可变的,可在多线程环境下安全共享
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
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, 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)。下面是一个完整的转换工具类:
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?
决策树:
- 项目 JDK 版本 ≥ 8? → 使用
java.time包 - 需要与遗留代码/库交互? → 在边界处转换,核心逻辑仍用
java.time - 需要数据库操作? → 使用
java.time类型与 JPA 2.2+(支持LocalDate等) - 需要高性能、线程安全的格式化? → 使用
DateTimeFormatter - 维护 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 性能考虑
java.time性能优于Calendar:Calendar内部有复杂的字段计算和同步开销DateTimeFormatter重用:由于线程安全,可以定义为static final常量重用- 避免频繁创建
Instant/LocalDateTime:除非必要,否则使用now()获取当前时间即可 - 大量日期计算时考虑使用
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 的演进之路回顾
- 第一代(Date/DateFormat):JDK 1.0 诞生,设计简陋,存在年份偏移、月份从 0 开始、线程不安全等问题,大部分方法已废弃
- 第二代(Calendar):JDK 1.1 引入,试图弥补 Date 的缺陷,但 API 复杂、月份偏移问题依旧、线程不安全,且性能较差
- 第三代(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 给开发者的建议
- 新项目一律使用
java.time包,告别旧 API 的烦恼 - 维护旧项目时,在边界层(如 Controller、DAO)进行新旧 API 转换,核心业务逻辑尽量使用
java.time - 注意线程安全,
SimpleDateFormat在多线程环境下必须采取保护措施 - 理解时区概念,区分本地时间、UTC 时间、带时区时间
- 善用
DateTimeFormatter,它是线程安全的,可以定义为常量复用 - 阅读官方文档和
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"));


