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

引言:Java日期时间处理的演进之路
在Java应用程序开发中,日期和时间的处理是极其常见的需求------记录操作时间、计算时间差、格式化输出、时区转换、订单超时计算、报表统计等场景都离不开日期时间API。然而,Java的日期时间API经历了一条曲折的演进道路。
Java 1.0时代,设计者简单粗暴地推出了java.util.Date类,但它存在诸多设计缺陷。Java 1.1引入了Calendar类试图弥补,却又带来了新的复杂性。直到Java 8,吸取了Joda-Time库的精华,推出了全新的java.time包(JSR 310),才真正解决了长久以来的痛点。
本文将深入剖析Java日期时间处理的三个时代:
- 第一代 :
Date、DateFormat、SimpleDateFormat - 第二代 :
Calendar、GregorianCalendar、TimeZone - 第三代 :
LocalDate、LocalTime、LocalDateTime、ZonedDateTime、Instant、DateTimeFormatter等
我们将从源码层面剖析其设计原理,探讨线程安全问题,提供最佳实践,并给出新旧API的转换指南。全文预计12000字以上,力求让你彻底掌握Java日期时间处理的方方面面。
第一章:时间的基础概念
在深入Java API之前,我们需要理解计算机系统中时间的基本表示方法。
1.1 时间原点:1970-01-01 UTC
几乎所有计算机系统都采用一个共同的时间原点------1970年1月1日 00:00:00 UTC(协调世界时)。这个时间点被称为Unix纪元(Unix Epoch)。
为什么选择这个时间?这主要源于Unix操作系统的历史原因。1970年左右,Unix系统诞生,设计者选择了一个相对"干净"的时间起点。此后,几乎所有类Unix系统(包括Linux、macOS)以及Java等编程语言都沿用了这一约定。
1.2 时间表示的两种模型
计算机系统中有两种表示时间的模型:
1. 面向人类的模型
- 包含年、月、日、时、分、秒等字段
- 受时区、历法、夏令时等因素影响
- 例如:"2026年2月20日 星期五 下午3:30"
2. 面向机器的模型
- 用一个整数表示时间线上的一个点
- 通常是从时间原点开始的毫秒数或秒数
- 不受时区影响,便于计算和比较
- 例如:
1773084600000L(毫秒数)
Java中,System.currentTimeMillis()返回的就是面向机器的模型------从1970-01-01 UTC到当前时间的毫秒数。
java
public class TimeConcept {
public static void main(String[] args) {
long currentTimeMillis = System.currentTimeMillis();
System.out.println("当前时间戳(毫秒): " + currentTimeMillis);
// 计算某段代码的耗时
long start = System.currentTimeMillis();
// 模拟耗时操作
try { Thread.sleep(1000); } catch (InterruptedException e) {}
long end = System.currentTimeMillis();
System.out.println("耗时: " + (end - start) + "ms");
}
}
1.3 时区与历法
时区(TimeZone):由于地球的自转,不同经度的地区时间不同。时区将地球划分为24个区域,每个区域相差1小时。UTC(协调世界时)是时间基准,北京时间为UTC+8。
历法(Calendar) :不同文化使用不同的历法系统,如公历(GregorianCalendar)、农历、伊斯兰历等。Java主要支持公历(ISO-8601标准),但Calendar设计时考虑了扩展性,可以支持其他历法。
第二章:第一代日期时间API------Date
java.util.Date是Java中最早出现的日期时间类,自JDK 1.0起就存在。
2.1 Date类的源码剖析
查看Date类的源码(以JDK 8为例),可以看到其核心实现:
java
package java.util;
public class Date implements java.io.Serializable, Cloneable, Comparable<Date> {
// 核心:存储从1970-01-01 00:00:00 GMT开始的毫秒数
private transient long fastTime;
// 还有一些过时的方法使用的内部字段
private transient long cdate;
// 无参构造:获取当前时间
public Date() {
this(System.currentTimeMillis());
}
// 带参构造:根据毫秒数创建Date对象
public Date(long date) {
fastTime = date;
}
// 已废弃的构造方法(年份从1900开始,月份从0开始)
@Deprecated
public Date(int year, int month, int date) {
this(year, month, date, 0, 0, 0);
}
// 获取毫秒数
public long getTime() {
return getTimeImpl();
}
// 设置毫秒数
public void setTime(long time) {
fastTime = time;
cdate = null;
}
// 比较日期
public boolean before(Date when) {
return getMillisOf(this) < getMillisOf(when);
}
public boolean after(Date when) {
return getMillisOf(this) > getMillisOf(when);
}
// 转换为字符串:dow mon dd hh:mm:ss zzz yyyy
public String toString() {
// 实际实现依赖于系统时区
return toString(ZoneId.systemDefault());
}
// ... 其他方法
}
关键点分析:
-
核心字段
fastTime:这是一个long类型的变量,存储从1970-01-01 UTC开始的毫秒数。整个Date对象本质上就是这个数字的封装。 -
构造方法 :只有两个构造方法被保留推荐使用------无参构造(获取当前时间)和带毫秒参数的构造。其他构造方法都被
@Deprecated标记,不再推荐使用。 -
主要方法 :
getTime()和setTime()用于获取和设置毫秒数;before()、after()、compareTo()用于日期比较;toString()用于输出。
2.2 Date类的核心方法详解
2.2.1 创建Date对象
java
import java.util.Date;
public class DateCreation {
public static void main(String[] args) {
// 方式1:无参构造,表示当前时间
Date now = new Date();
System.out.println("当前时间: " + now);
// 方式2:带毫秒参数
Date date = new Date(1773084600000L); // 2026-02-20 具体时间取决于时区
System.out.println("指定毫秒数的时间: " + date);
// 方式3:不推荐!从字符串解析(已废弃)
@SuppressWarnings("deprecation")
Date deprecated = new Date("2026/02/20");
System.out.println("已废弃方式: " + deprecated);
}
}
2.2.2 日期比较
java
import java.util.Date;
public class DateComparison {
public static void main(String[] args) throws InterruptedException {
Date date1 = new Date();
Thread.sleep(100); // 暂停100毫秒
Date date2 = new Date();
// 使用before/after方法
System.out.println("date1 before date2? " + date1.before(date2)); // true
System.out.println("date2 after date1? " + date2.after(date1)); // true
// 使用compareTo方法(实现Comparable接口)
int result = date1.compareTo(date2);
System.out.println("compareTo结果: " + result); // 负数表示date1 < date2
// 使用equals方法
System.out.println("是否相等? " + date1.equals(date2)); // false
// 直接比较毫秒数
System.out.println("毫秒比较: " + (date1.getTime() < date2.getTime())); // true
}
}
2.2.3 获取/设置毫秒数
java
import java.util.Date;
public class DateMillis {
public static void main(String[] args) {
Date now = new Date();
// 获取毫秒数
long millis = now.getTime();
System.out.println("当前毫秒数: " + millis);
// 设置新的毫秒数
now.setTime(0L); // 设置为1970-01-01 08:00:00(北京时间,因为东八区)
System.out.println("重置后: " + now);
// 通过毫秒数计算时间差
Date start = new Date();
// 模拟操作...
Date end = new Date();
long elapsed = end.getTime() - start.getTime();
System.out.println("耗时: " + elapsed + "ms");
}
}
2.3 Date类的设计缺陷(为什么被废弃)
Date类的大部分方法在JDK 1.1之后就被标记为@Deprecated,主要原因如下:
缺陷1:年份从1900年开始
java
import java.util.Date;
public class DatePitfall {
public static void main(String[] args) {
// 本意是2026年2月20日
@SuppressWarnings("deprecation")
Date date = new Date(2026, 2, 20); // 年份参数是2026吗?
// 实际输出:3926-03-20(年份 = 2026 + 1900,月份2表示3月)
System.out.println("实际结果: " + date);
// 正确的写法应该是:
Date correct = new Date(126, 2, 20); // 年份 = 2026 - 1900 = 126
System.out.println("正确结果: " + correct);
}
}
解释 :Date(int year, int month, int date)构造方法中,year参数表示"year - 1900",所以传入2026相当于1900+2026=3926年。这完全是反直觉的设计。
缺陷2:月份从0开始
java
@SuppressWarnings("deprecation")
Date date = new Date(126, 2, 20); // 月份2实际表示3月
System.out.println(date); // 输出:Fri Mar 20 00:00:00 CST 2026
解释:月份常量中,0代表1月,1代表2月,...,11代表12月。这与日常习惯(1-12月)完全不符,极易导致月份错误。
缺陷3:可变性导致的线程安全问题
java
import java.util.Date;
public class DateMutableProblem {
public static void main(String[] args) {
Date date = new Date();
System.out.println("原始日期: " + date);
// setTime方法可以修改Date对象内部状态
date.setTime(0L);
System.out.println("被修改后: " + date); // 原始对象被改变!
// 在多线程环境下,这种可变性会导致数据不一致
}
}
解释 :Date是可变的,其setTime()等方法可以修改内部fastTime字段。在多线程环境中,如果多个线程共享同一个Date实例且进行修改,会产生竞态条件。
缺陷4:国际化支持薄弱
java
// Date的toString()输出格式固定,不考虑Locale
Date now = new Date();
System.out.println(now); // 总是输出:Fri Feb 20 15:30:45 CST 2026
// 无法直接输出中文格式的"2026年2月20日 星期五 下午3:30:45"
解释 :Date的toString()方法格式固定,不考虑不同国家和语言的日期表示习惯。
缺陷5:命名混淆
java.util.Date实际上既包含日期也包含时间,但类名却只叫"Date",容易让人误以为它只处理日期部分。
2.4 正确使用Date的建议
鉴于上述缺陷,官方建议:
- 只使用两个推荐的方法 :
new Date()和new Date(long)构造、getTime()、setTime()、before()、after()、compareTo() - 不要使用任何
@Deprecated标记的方法 - 将Date作为"时间戳"的载体,不直接操作其日历字段
- 在需要日历字段操作时,使用Calendar类
java
// 推荐的使用方式:仅将Date作为时间戳对象
Date now = new Date(); // 当前时刻
long timestamp = now.getTime(); // 获取毫秒数
// 如果需要操作年月日,使用Calendar
Calendar cal = Calendar.getInstance();
cal.setTime(now);
int year = cal.get(Calendar.YEAR); // 正确获取年份
第三章:日期格式化------DateFormat与SimpleDateFormat
Date类本身无法控制输出的格式,因此Java提供了专门的格式化类DateFormat及其子类SimpleDateFormat,用于在Date对象和字符串之间进行转换。
3.1 DateFormat抽象类
java.text.DateFormat是一个抽象类,用于格式化/解析日期时间。
3.1.1 预定义样式常量
DateFormat定义了四种内置的显示风格:
| 常量 | 值 | 说明 |
|---|---|---|
DateFormat.FULL |
0 | 完整格式,如"2026年2月20日 星期五" |
DateFormat.LONG |
1 | 长格式,如"2026年2月20日" |
DateFormat.MEDIUM |
2 | 中等格式,如"2026-2-20"(默认风格) |
DateFormat.SHORT |
3 | 短格式,如"26-2-20" |
3.1.2 获取实例的工厂方法
java
// 获取日期格式化器
DateFormat.getDateInstance(); // 默认风格(MEDIUM)的日期格式化
DateFormat.getDateInstance(int style); // 指定风格的日期格式化
DateFormat.getDateInstance(int style, Locale locale); // 指定风格和区域
// 获取时间格式化器
DateFormat.getTimeInstance(); // 默认风格的时间格式化
DateFormat.getTimeInstance(int style); // 指定风格的时间格式化
// 获取日期时间格式化器
DateFormat.getDateTimeInstance(); // 默认风格的日期时间格式化
DateFormat.getDateTimeInstance(int dateStyle, int timeStyle); // 指定风格
3.1.3 核心方法
java
// Date -> String 格式化
public final String format(Date date)
// String -> Date 解析
public Date parse(String source) throws ParseException
3.1.4 使用示例
java
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;
public class DateFormatDemo {
public static void main(String[] args) {
Date now = new Date();
// 不同风格的日期格式化
DateFormat dfFull = DateFormat.getDateInstance(DateFormat.FULL, Locale.CHINA);
DateFormat dfLong = DateFormat.getDateInstance(DateFormat.LONG, Locale.CHINA);
DateFormat dfMedium = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.CHINA);
DateFormat dfShort = DateFormat.getDateInstance(DateFormat.SHORT, Locale.CHINA);
System.out.println("FULL格式: " + dfFull.format(now)); // 2026年2月20日 星期五
System.out.println("LONG格式: " + dfLong.format(now)); // 2026年2月20日
System.out.println("MEDIUM格式: " + dfMedium.format(now)); // 2026-2-20
System.out.println("SHORT格式: " + dfShort.format(now)); // 26-2-20
// 时间格式化
DateFormat tf = DateFormat.getTimeInstance(DateFormat.MEDIUM, Locale.CHINA);
System.out.println("时间: " + tf.format(now)); // 15:30:45
// 日期时间格式化
DateFormat dtf = DateFormat.getDateTimeInstance(
DateFormat.LONG, DateFormat.MEDIUM, Locale.CHINA);
System.out.println("日期时间: " + dtf.format(now)); // 2026年2月20日 15:30:45
// 解析字符串为Date
try {
String dateStr = "2026-02-20";
DateFormat parser = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.CHINA);
Date parsed = parser.parse(dateStr);
System.out.println("解析结果: " + parsed);
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.2 SimpleDateFormat------更灵活的格式化器
java.text.SimpleDateFormat是DateFormat的具体子类,允许通过模式字符串自定义日期时间格式。
3.2.1 构造方法
java
// 使用默认模式
SimpleDateFormat()
// 使用指定模式
SimpleDateFormat(String pattern)
// 使用指定模式和区域
SimpleDateFormat(String pattern, Locale locale)
3.2.2 常用模式字母
| 字母 | 日期/时间元素 | 示例 |
|---|---|---|
| y | 年 | yyyy -> 2026 |
| M | 月份 | MM -> 02, MMM -> 二月 |
| d | 月份中的天数 | dd -> 20 |
| E | 星期几 | EEEE -> 星期五 |
| a | AM/PM标记 | a -> 下午 |
| H | 小时(0-23) | HH -> 15 |
| h | 小时(1-12) | hh -> 03 |
| m | 分钟 | mm -> 30 |
| s | 秒 | ss -> 45 |
| S | 毫秒 | SSS -> 123 |
| z | 时区 | z -> CST |
| Z | RFC 822时区 | Z -> +0800 |
3.2.3 基本使用示例
java
import java.text.SimpleDateFormat;
import java.util.Date;
public class SimpleDateFormatDemo {
public static void main(String[] args) {
Date now = new Date();
// 1. 常用格式:yyyy-MM-dd HH:mm:ss
SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("格式1: " + sdf1.format(now)); // 2026-02-20 15:30:45
// 2. 中文格式
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy年MM月dd日 EEEE HH时mm分ss秒");
System.out.println("格式2: " + sdf2.format(now)); // 2026年02月20日 星期五 15时30分45秒
// 3. 带毫秒
SimpleDateFormat sdf3 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
System.out.println("格式3: " + sdf3.format(now)); // 2026-02-20 15:30:45.123
// 4. 12小时制带AM/PM
SimpleDateFormat sdf4 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss a");
System.out.println("格式4: " + sdf4.format(now)); // 2026-02-20 03:30:45 下午
// 5. 只显示日期
SimpleDateFormat sdf5 = new SimpleDateFormat("yyyy/MM/dd");
System.out.println("格式5: " + sdf5.format(now)); // 2026/02/20
// 6. 字符串解析为Date
try {
String dateStr = "2026-02-20 15:30:45";
SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date parsed = parser.parse(dateStr);
System.out.println("解析结果: " + parsed);
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.2.4 模式匹配规则详解
java
import java.text.SimpleDateFormat;
import java.util.Date;
public class PatternDetail {
public static void main(String[] args) {
Date now = new Date();
// 年份:y个数影响显示位数
System.out.println("yyyy: " + new SimpleDateFormat("yyyy").format(now)); // 2026
System.out.println("yy: " + new SimpleDateFormat("yy").format(now)); // 26
// 月份:M个数影响格式
System.out.println("M: " + new SimpleDateFormat("M").format(now)); // 2
System.out.println("MM: " + new SimpleDateFormat("MM").format(now)); // 02
System.out.println("MMM: " + new SimpleDateFormat("MMM").format(now)); // 二月
System.out.println("MMMM: " + new SimpleDateFormat("MMMM").format(now)); // 二月
// 日期:d个数影响格式
System.out.println("d: " + new SimpleDateFormat("d").format(now)); // 20
System.out.println("dd: " + new SimpleDateFormat("dd").format(now)); // 20
// 星期:E个数影响格式
System.out.println("E: " + new SimpleDateFormat("E").format(now)); // 星期五
System.out.println("EEEE: " + new SimpleDateFormat("EEEE").format(now)); // 星期五
// 小时:H(0-23) vs h(1-12)
System.out.println("H: " + new SimpleDateFormat("H").format(now)); // 15
System.out.println("HH: " + new SimpleDateFormat("HH").format(now)); // 15
System.out.println("h: " + new SimpleDateFormat("h").format(now)); // 3
System.out.println("hh: " + new SimpleDateFormat("hh").format(now)); // 03
}
}
3.3 SimpleDateFormat的线程安全问题
核心问题 :SimpleDateFormat是线程不安全的。
3.3.1 问题根源------源码分析
查看SimpleDateFormat的源码,可以发现它继承自DateFormat,而DateFormat中定义了一个protected的Calendar对象:
java
// DateFormat.java
public abstract class DateFormat extends Format {
protected Calendar calendar; // 共享的Calendar实例
// ...
}
// SimpleDateFormat.java
public class SimpleDateFormat extends DateFormat {
// 在format方法中会使用calendar
private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) {
// 关键点:这里修改了calendar的状态!
calendar.setTime(date);
// ... 后续使用calendar进行格式化
return toAppendTo;
}
}
问题分析:
calendar是DateFormat类的成员变量,被SimpleDateFormat继承- 在
format()方法中,首先调用calendar.setTime(date)修改calendar的状态 - 在多线程环境下,如果多个线程共享同一个
SimpleDateFormat实例,线程A执行calendar.setTime()后可能被暂停,线程B开始执行并再次修改calendar,导致线程A后续使用的calendar状态已被改变,产生错误结果甚至程序崩溃
3.3.2 问题复现
java
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SimpleDateFormatThreadProblem {
// 共享的SimpleDateFormat实例(线程不安全)
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
final int num = i;
executor.submit(() -> {
try {
// 多个线程同时调用format方法
String dateStr = sdf.format(new Date());
System.out.println("Thread " + num + ": " + dateStr);
// 可能会输出错误结果,或抛出异常
} catch (Exception e) {
System.out.println("Thread " + num + " exception: " + e);
}
});
}
executor.shutdown();
}
}
运行上述代码,可能出现:
- 日期时间错误(如月份变为0)
- 抛出
NumberFormatException等异常 - 程序挂死
3.3.3 解决方案
方案1:每次使用时创建新实例
java
public class SafeDateFormat1 {
public static String formatDate(Date date) {
// 每次方法调用都创建新实例,避免共享
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}
public static Date parse(String dateStr) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.parse(dateStr);
}
}
优点:简单可靠
缺点:频繁创建对象,有一定性能开销,但在大多数应用中可接受
方案2:使用ThreadLocal
java
import java.text.SimpleDateFormat;
import java.util.Date;
public class SafeDateFormat2 {
// 每个线程持有自己的SimpleDateFormat实例
private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static String formatDate(Date date) {
return DATE_FORMAT.get().format(date);
}
public static Date parse(String dateStr) throws Exception {
return DATE_FORMAT.get().parse(dateStr);
}
// 清理(通常在web请求结束时调用)
public static void remove() {
DATE_FORMAT.remove();
}
}
优点:线程安全,避免了重复创建的开销
缺点:需要注意在线程池环境中及时清理,防止内存泄漏
方案3:同步加锁
java
public class SafeDateFormat3 {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static synchronized String formatDate(Date date) {
return sdf.format(date);
}
public static synchronized Date parse(String dateStr) throws Exception {
return sdf.parse(dateStr);
}
}
优点:简单
缺点:并发性能差,多个线程需要排队
方案4:使用Java 8的DateTimeFormatter(最佳方案)
java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class SafeDateFormat4 {
// DateTimeFormatter是不可变且线程安全的
private static final DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static String formatDate(LocalDateTime dateTime) {
return dateTime.format(formatter);
}
public static LocalDateTime parse(String dateStr) {
return LocalDateTime.parse(dateStr, formatter);
}
}
推荐方案 :在新项目中,直接使用Java 8的DateTimeFormatter;在维护旧项目时,使用ThreadLocal包装SimpleDateFormat。
第四章:第二代日期时间API------Calendar
为了解决Date类的缺陷,JDK 1.1引入了java.util.Calendar类,它是一个抽象类,提供了更强大的日期字段操作能力。
4.1 Calendar类的设计思想
Calendar的设计目标是:
- 字段化:将日期时间分解为年、月、日、时、分、秒等独立字段
- 国际化:支持不同时区和语言环境
- 计算能力:支持日期的加减、滚动等操作
- 扩展性:可以支持不同的历法系统(如公历、农历、日本历等)
4.2 Calendar类的源码结构
java
public abstract class Calendar implements Serializable, Cloneable, Comparable<Calendar> {
// 核心字段:存储时间毫秒数
protected long time;
// 标记time是否被设置过
protected boolean isTimeSet;
// 存储各个日历字段的值(如YEAR、MONTH等)
protected int fields[];
// 标记fields中哪些字段被设置过
protected boolean isSet[];
// 时区
private TimeZone zone;
// 17个静态常量,作为fields数组的索引
public static final int ERA = 0;
public static final int YEAR = 1;
public static final int MONTH = 2;
public static final int WEEK_OF_YEAR = 3;
public static final int WEEK_OF_MONTH = 4;
public static final int DATE = 5; // 同DAY_OF_MONTH
public static final int DAY_OF_MONTH = 5;
public static final int DAY_OF_YEAR = 6;
public static final int DAY_OF_WEEK = 7;
public static final int DAY_OF_WEEK_IN_MONTH = 8;
public static final int AM_PM = 9;
public static final int HOUR = 10; // 12小时制(0-11)
public static final int HOUR_OF_DAY = 11; // 24小时制(0-23)
public static final int MINUTE = 12;
public static final int SECOND = 13;
public static final int MILLISECOND = 14;
public static final int ZONE_OFFSET = 15;
public static final int DST_OFFSET = 16;
// 共17个字段,索引0-16
public static final int FIELD_COUNT = 17;
// 获取实例的工厂方法
public static Calendar getInstance() {
return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));
}
// 抽象方法:子类实现具体的历法计算
protected abstract void computeTime();
protected abstract void computeFields();
// 核心方法:获取字段值
public int get(int field) {
complete(); // 确保fields数组是最新的
return internalGet(field);
}
// 设置字段值
public void set(int field, int value) {
// 如果设置了某个字段,time就不再准确,需要重新计算
isTimeSet = false;
fields[field] = value;
isSet[field] = true;
}
// 日期计算:增加/减少指定字段的值(会进位)
public abstract void add(int field, int amount);
// 日期滚动:增加/减少指定字段的值(不会进位)
public void roll(int field, int amount);
// 获取对应的Date对象
public final Date getTime() {
return new Date(getTimeInMillis());
}
// 设置时间
public final void setTime(Date date) {
setTimeInMillis(date.getTime());
}
}
关键设计解析:
-
双重表示 :
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。通过工厂方法获取实例:
java
import java.util.Calendar;
import java.util.TimeZone;
import java.util.Locale;
public class CalendarInstance {
public static void main(String[] args) {
// 1. 默认时区和语言环境(通常使用系统默认值)
Calendar cal1 = Calendar.getInstance();
System.out.println("默认: " + cal1.getTime());
// 2. 指定时区
Calendar cal2 = Calendar.getInstance(TimeZone.getTimeZone("America/New_York"));
System.out.println("纽约时区: " + cal2.getTime());
// 3. 指定语言环境
Calendar cal3 = Calendar.getInstance(Locale.US);
System.out.println("美国语言环境: " + cal3.getTime());
// 4. 同时指定时区和语言环境
Calendar cal4 = Calendar.getInstance(
TimeZone.getTimeZone("Asia/Shanghai"),
Locale.CHINA
);
System.out.println("上海时区+中国: " + cal4.getTime());
}
}
getInstance()内部调用createCalendar(),默认返回GregorianCalendar(公历)实例。
4.4 Calendar核心方法详解
4.4.1 获取字段值:get(int field)
java
import java.util.Calendar;
public class CalendarGet {
public static void main(String[] args) {
Calendar cal = Calendar.getInstance();
// 获取各个字段的值
int year = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH); // 注意:0代表1月!
int day = cal.get(Calendar.DAY_OF_MONTH);
int hour12 = cal.get(Calendar.HOUR); // 12小时制
int hour24 = cal.get(Calendar.HOUR_OF_DAY); // 24小时制
int minute = cal.get(Calendar.MINUTE);
int second = cal.get(Calendar.SECOND);
int millis = cal.get(Calendar.MILLISECOND);
int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK); // 1=周日, 2=周一, ..., 7=周六
int dayOfYear = cal.get(Calendar.DAY_OF_YEAR);
int weekOfYear = cal.get(Calendar.WEEK_OF_YEAR);
int weekOfMonth = cal.get(Calendar.WEEK_OF_MONTH);
int ampm = cal.get(Calendar.AM_PM); // 0=上午, 1=下午
System.out.printf("年: %d%n", year);
System.out.printf("月: %d (实际月份=%d)%n", month, month + 1); // 记得+1
System.out.printf("日: %d%n", day);
System.out.printf("24小时制: %d%n", hour24);
System.out.printf("12小时制: %d %s%n", hour12, ampm == 0 ? "AM" : "PM");
System.out.printf("分: %d%n", minute);
System.out.printf("秒: %d%n", second);
System.out.printf("毫秒: %d%n", millis);
System.out.printf("星期: %d (1=周日, 2=周一)%n", dayOfWeek);
System.out.printf("一年中的第几天: %d%n", dayOfYear);
System.out.printf("一年中的第几周: %d%n", weekOfYear);
}
}
重要提醒:
Calendar.MONTH从0开始(0=一月,11=十二月)Calendar.DAY_OF_WEEK:1=周日,2=周一,...,7=周六
4.4.2 设置字段值:set(int field, int value) 和 set(int year, int month, int date)
java
import java.util.Calendar;
public class CalendarSet {
public static void main(String[] args) {
Calendar cal = Calendar.getInstance();
// 方法1:逐个字段设置
cal.set(Calendar.YEAR, 2026);
cal.set(Calendar.MONTH, 1); // 1 = 二月
cal.set(Calendar.DAY_OF_MONTH, 20);
cal.set(Calendar.HOUR_OF_DAY, 15);
cal.set(Calendar.MINUTE, 30);
cal.set(Calendar.SECOND, 45);
cal.set(Calendar.MILLISECOND, 123);
System.out.println("设置后: " + cal.getTime());
// 方法2:一次设置年月日
Calendar cal2 = Calendar.getInstance();
cal2.set(2026, 1, 20); // 年, 月, 日 (月从0开始)
System.out.println("年月日: " + cal2.getTime());
// 方法3:一次设置年月日时分秒
Calendar cal3 = Calendar.getInstance();
cal3.set(2026, 1, 20, 15, 30, 45); // 年,月,日,时,分,秒
System.out.println("完整设置: " + cal3.getTime());
// 注意:月份偏移问题
Calendar wrong = Calendar.getInstance();
wrong.set(2026, 2, 20); // 本意是2026年2月20日?不对,2表示3月!
System.out.println("错误设置(月份2): " + wrong.getTime()); // 实际是3月20日
}
}
4.4.3 日期计算:add(int field, int amount)
add()方法按照日历规则,对指定字段增加/减少指定值,会进位到更高字段。
java
import java.util.Calendar;
import java.text.SimpleDateFormat;
public class CalendarAdd {
public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Calendar cal = Calendar.getInstance();
cal.set(2026, 1, 20, 23, 59, 59); // 2026-02-20 23:59:59
System.out.println("原始时间: " + sdf.format(cal.getTime()));
// 加10秒(会进位到分钟)
cal.add(Calendar.SECOND, 10);
System.out.println("加10秒后: " + sdf.format(cal.getTime())); // 2026-02-21 00:00:09
// 重置
cal.set(2026, 1, 20, 23, 59, 59);
// 加1分钟(会进位到小时)
cal.add(Calendar.MINUTE, 1);
System.out.println("加1分钟后: " + sdf.format(cal.getTime())); // 2026-02-21 00:00:59
// 加1小时(会进位到天)
cal.set(2026, 1, 20, 23, 59, 59);
cal.add(Calendar.HOUR_OF_DAY, 1);
System.out.println("加1小时后: " + sdf.format(cal.getTime())); // 2026-02-21 00:59:59
// 加1个月(会进位到年)
cal.set(2026, 11, 20); // 2026-12-20
cal.add(Calendar.MONTH, 1);
System.out.println("加1个月后: " + sdf.format(cal.getTime())); // 2027-01-20
// 负数表示减去
cal.set(2026, 0, 1); // 2026-01-01
cal.add(Calendar.DAY_OF_MONTH, -1);
System.out.println("减1天后: " + sdf.format(cal.getTime())); // 2025-12-31
}
}
典型应用:计算30天后、3个月前、5年后等。
4.4.4 日期滚动:roll(int field, int amount)
roll()与add()类似,但不会进位到更高字段。
java
import java.util.Calendar;
import java.text.SimpleDateFormat;
public class CalendarRoll {
public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Calendar cal = Calendar.getInstance();
// 示例1:月份滚动
cal.set(2026, 11, 20); // 2026-12-20
System.out.println("原始: " + sdf.format(cal.getTime()));
cal.roll(Calendar.MONTH, 1); // 月份+1,但不进位
System.out.println("roll +1月: " + sdf.format(cal.getTime())); // 2026-01-20!
// 解释:12月roll 1个月,按照月份范围1-12,12+1=13,但不会进位到年,所以回到1月
// 示例2:日期滚动(月份不同长度)
cal.set(2026, 0, 31); // 2026-01-31
System.out.println("原始: " + sdf.format(cal.getTime()));
cal.roll(Calendar.DAY_OF_MONTH, 1); // 日期+1,但不进位
System.out.println("roll +1天: " + sdf.format(cal.getTime())); // 2026-01-01
// 解释:1月31日加1天,日期范围1-31,31+1=32,不会进位到2月,而是回到1月1日
// 对比add的行为
cal.set(2026, 0, 31);
cal.add(Calendar.DAY_OF_MONTH, 1);
System.out.println("add +1天: " + sdf.format(cal.getTime())); // 2026-02-01(进位)
}
}
适用场景:当你只想在字段范围内循环(如调整日期但不想改变月份)时使用。
4.4.5 清空与设置宽松模式
java
import java.util.Calendar;
public class CalendarClearLenient {
public static void main(String[] args) {
Calendar cal = Calendar.getInstance();
// 1. clear():清空所有字段,设置为1970-01-01 00:00:00
cal.clear();
System.out.println("clear后: " + cal.getTime());
// 2. clear(int field):清空指定字段
cal.set(2026, 1, 20);
cal.clear(Calendar.HOUR_OF_DAY);
cal.clear(Calendar.MINUTE);
cal.clear(Calendar.SECOND);
System.out.println("清空时间后: " + cal.getTime()); // 日期不变,时间为00:00:00
// 3. setLenient:设置宽松/严格模式
cal.setLenient(true); // 默认:宽松模式,允许非法字段值自动修正
cal.set(2026, 1, 31); // 2月31日不存在
System.out.println("宽松模式: " + cal.getTime()); // 自动修正为2026-03-03或03-02(取决于具体实现)
cal.setLenient(false); // 严格模式
cal.set(2026, 1, 31);
try {
System.out.println(cal.getTime()); // 抛出IllegalArgumentException
} catch (IllegalArgumentException e) {
System.out.println("严格模式下非法日期抛出异常");
}
}
}
宽松模式:自动将非法字段值调整为合法值,如2月31日变为3月2日或3月3日(具体规则依赖于实现)。
严格模式:字段值非法时抛出异常,适合需要严格校验的场景。
4.5 Calendar的优缺点分析
优点
- 丰富的字段操作:提供了17个日历字段,可以精确获取和设置各个部分
- 强大的计算能力 :
add()和roll()方法支持灵活的日期运算 - 国际化支持:内置时区和Locale处理
- 历法扩展性:可以支持不同的历法系统
缺点
- API复杂繁琐:使用时需要记住大量常量,代码冗长
- 月份偏移陷阱:月份从0开始,极易出错
- 线程不安全:内部状态可变,多线程共享需同步
- 可变性:方法调用会修改对象内部状态,不符合函数式编程思想
- 性能开销:相对重量级,频繁创建有性能问题
- 设计缺陷 :某些方法行为不够直观(如
roll)
4.6 Calendar与Date的转换
java
import java.util.Calendar;
import java.util.Date;
public class CalendarDateConversion {
public static void main(String[] args) {
// Calendar -> Date
Calendar cal = Calendar.getInstance();
Date dateFromCal = cal.getTime();
System.out.println("Calendar转Date: " + dateFromCal);
// Date -> Calendar
Date now = new Date();
Calendar calFromDate = Calendar.getInstance();
calFromDate.setTime(now);
System.out.println("Date转Calendar: " + calFromDate.getTime());
// 获取毫秒数
long millisFromCal = cal.getTimeInMillis();
long millisFromDate = now.getTime();
System.out.println("毫秒数相等: " + (millisFromCal == millisFromDate));
}
}
4.7 Calendar常用场景示例
场景1:计算两个日期之间的天数差
java
import java.util.Calendar;
import java.util.Date;
public class DateDiff {
public static int daysBetween(Date startDate, Date endDate) {
Calendar startCal = Calendar.getInstance();
startCal.setTime(startDate);
Calendar endCal = Calendar.getInstance();
endCal.setTime(endDate);
// 重置时间到午夜,避免时分秒影响
startCal.set(Calendar.HOUR_OF_DAY, 0);
startCal.set(Calendar.MINUTE, 0);
startCal.set(Calendar.SECOND, 0);
startCal.set(Calendar.MILLISECOND, 0);
endCal.set(Calendar.HOUR_OF_DAY, 0);
endCal.set(Calendar.MINUTE, 0);
endCal.set(Calendar.SECOND, 0);
endCal.set(Calendar.MILLISECOND, 0);
long millis1 = startCal.getTimeInMillis();
long millis2 = endCal.getTimeInMillis();
long diff = millis2 - millis1;
return (int) (diff / (24 * 60 * 60 * 1000));
}
public static void main(String[] args) {
Calendar cal1 = Calendar.getInstance();
cal1.set(2026, 0, 1); // 2026-01-01
Calendar cal2 = Calendar.getInstance();
cal2.set(2026, 11, 31); // 2026-12-31
int days = daysBetween(cal1.getTime(), cal2.getTime());
System.out.println("2026年共有: " + (days + 1) + "天"); // 365天
}
}
场景2:获取某月的第一天和最后一天
java
import java.util.Calendar;
public class MonthBoundary {
public static void main(String[] args) {
Calendar cal = Calendar.getInstance();
cal.set(2026, 1, 15); // 2026-02-15
// 第一天
cal.set(Calendar.DAY_OF_MONTH, 1);
System.out.println("本月第一天: " + cal.getTime());
// 最后一天
cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
System.out.println("本月最后一天: " + cal.getTime());
}
}
场景3:判断闰年
java
import java.util.Calendar;
import java.util.GregorianCalendar;
public class LeapYearCheck {
public static boolean isLeapYear(int year) {
GregorianCalendar cal = new GregorianCalendar();
return cal.isLeapYear(year);
}
public static void main(String[] args) {
System.out.println("2024是闰年? " + isLeapYear(2024)); // true
System.out.println("2026是闰年? " + isLeapYear(2026)); // false
System.out.println("2000是闰年? " + isLeapYear(2000)); // true(世纪年规则)
}
}
第五章:第三代日期时间API------java.time(JSR 310)
鉴于第一代Date和第二代Calendar的种种缺陷,Java 8引入了全新的日期时间API------java.time包(JSR 310),它深受Joda-Time库的启发,提供了更优雅、更安全、更强大的日期时间处理能力。
5.1 设计哲学与核心原则
Java 8日期时间API的设计遵循以下核心原则:
-
不可变性 :所有核心类都是
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
java
import java.time.LocalDate;
import java.time.Month;
import java.time.format.DateTimeFormatter;
public class LocalDateCreation {
public static void main(String[] args) {
// 1. 当前日期
LocalDate now = LocalDate.now();
System.out.println("当前日期: " + now); // 2026-02-20
// 2. 指定年月日
LocalDate date1 = LocalDate.of(2026, 2, 20);
LocalDate date2 = LocalDate.of(2026, Month.FEBRUARY, 20); // 使用Month枚举
System.out.println("指定日期: " + date1);
// 3. 从字符串解析(ISO格式:yyyy-MM-dd)
LocalDate parsed1 = LocalDate.parse("2026-02-20");
System.out.println("解析ISO: " + parsed1);
// 4. 从字符串解析(自定义格式)
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
LocalDate parsed2 = LocalDate.parse("2026/02/20", formatter);
System.out.println("解析自定义: " + parsed2);
// 5. 从年日获取
LocalDate fromYearDay = LocalDate.ofYearDay(2026, 51); // 2026年的第51天 = 2月20日
System.out.println("年日转换: " + fromYearDay);
// 6. 从纪元日获取
LocalDate fromEpoch = LocalDate.ofEpochDay(20489); // 从1970-01-01开始的天数
System.out.println("纪元日转换: " + fromEpoch);
}
}
重要 :LocalDate的月份从1开始,符合人类直觉,再也不需要month+1了!
5.3.2 获取字段值
java
import java.time.LocalDate;
import java.time.DayOfWeek;
public class LocalDateGet {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2026, 2, 20);
int year = date.getYear(); // 2026
int month = date.getMonthValue(); // 2(1-12)
Month monthEnum = date.getMonth(); // FEBRUARY
int day = date.getDayOfMonth(); // 20
int dayOfYear = date.getDayOfYear(); // 51
DayOfWeek dayOfWeek = date.getDayOfWeek(); // FRIDAY
System.out.printf("年: %d%n", year);
System.out.printf("月: %d (%s)%n", month, monthEnum);
System.out.printf("日: %d%n", day);
System.out.printf("一年中的第几天: %d%n", dayOfYear);
System.out.printf("星期: %s%n", dayOfWeek);
// 检查某些特征
System.out.println("是否闰年? " + date.isLeapYear()); // false
System.out.println("本月长度: " + date.lengthOfMonth()); // 28(2026年2月)
System.out.println("本年长度: " + date.lengthOfYear()); // 365
}
}
5.3.3 日期运算(加减)
LocalDate的运算返回新的 LocalDate对象,原对象不变。
java
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
public class LocalDatePlusMinus {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2026, 2, 20);
System.out.println("原始日期: " + date);
// 加天数
LocalDate plusDays = date.plusDays(10);
System.out.println("加10天: " + plusDays); // 2026-03-02
// 加周数
LocalDate plusWeeks = date.plusWeeks(2);
System.out.println("加2周: " + plusWeeks); // 2026-03-06
// 加月数
LocalDate plusMonths = date.plusMonths(1);
System.out.println("加1月: " + plusMonths); // 2026-03-20
// 加年数
LocalDate plusYears = date.plusYears(5);
System.out.println("加5年: " + plusYears); // 2031-02-20
// 使用通用方法(ChronoUnit枚举)
LocalDate plus = date.plus(3, ChronoUnit.WEEKS);
System.out.println("加3周(ChronoUnit): " + plus); // 2026-03-13
// 减法
LocalDate minusDays = date.minusDays(5);
System.out.println("减5天: " + minusDays); // 2026-02-15
// 链式调用
LocalDate result = date.plusYears(1).plusMonths(2).minusDays(3);
System.out.println("链式运算: " + result); // 2027-04-17
}
}
5.3.4 日期比较
java
import java.time.LocalDate;
public class LocalDateCompare {
public static void main(String[] args) {
LocalDate date1 = LocalDate.of(2026, 2, 20);
LocalDate date2 = LocalDate.of(2026, 3, 15);
LocalDate date3 = LocalDate.of(2026, 2, 20);
// 比较方法
System.out.println("date1 before date2? " + date1.isBefore(date2)); // true
System.out.println("date1 after date2? " + date1.isAfter(date2)); // false
System.out.println("date1 equals date3? " + date1.equals(date3)); // true
// compareTo方法(实现Comparable)
int cmp = date1.compareTo(date2);
System.out.println("compareTo结果: " + cmp); // 负数(-1或更小)
// 与当前日期比较
LocalDate now = LocalDate.now();
System.out.println("date1是否早于今天? " + date1.isBefore(now));
}
}
5.4 LocalTime------只处理时间
LocalTime表示时间(时、分、秒、纳秒),不包含日期和时区。
java
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
public class LocalTimeDemo {
public static void main(String[] args) {
// 1. 创建
LocalTime now = LocalTime.now();
System.out.println("当前时间: " + now); // 15:30:45.123
LocalTime time1 = LocalTime.of(15, 30); // 15:30
LocalTime time2 = LocalTime.of(15, 30, 45); // 15:30:45
LocalTime time3 = LocalTime.of(15, 30, 45, 123456789); // 15:30:45.123456789
LocalTime parsed = LocalTime.parse("15:30:45");
LocalTime parsedCustom = LocalTime.parse("15-30-45",
DateTimeFormatter.ofPattern("HH-mm-ss"));
// 2. 获取字段
System.out.println("小时: " + time2.getHour()); // 15
System.out.println("分钟: " + time2.getMinute()); // 30
System.out.println("秒: " + time2.getSecond()); // 45
System.out.println("纳秒: " + time2.getNano()); // 0
// 3. 运算
LocalTime plusHours = time2.plusHours(2);
LocalTime plusMinutes = time2.plusMinutes(30);
LocalTime plusSeconds = time2.plus(30, ChronoUnit.SECONDS);
LocalTime minus = time2.minusHours(1);
System.out.println("加2小时: " + plusHours); // 17:30:45
System.out.println("加30分: " + plusMinutes); // 16:00:45
System.out.println("减1小时: " + minus); // 14:30:45
// 4. 比较
LocalTime timeA = LocalTime.of(10, 0);
LocalTime timeB = LocalTime.of(14, 0);
System.out.println("timeA before timeB? " + timeA.isBefore(timeB)); // true
}
}
5.5 LocalDateTime------日期+时间(无时区)
LocalDateTime组合了LocalDate和LocalTime,表示不带时区的完整日期时间。
java
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
public class LocalDateTimeDemo {
public static void main(String[] args) {
// 1. 创建
LocalDateTime now = LocalDateTime.now();
System.out.println("当前日期时间: " + now); // 2026-02-20T15:30:45.123
// 从年月日时分秒
LocalDateTime dt1 = LocalDateTime.of(2026, 2, 20, 15, 30);
LocalDateTime dt2 = LocalDateTime.of(2026, 2, 20, 15, 30, 45);
LocalDateTime dt3 = LocalDateTime.of(2026, 2, 20, 15, 30, 45, 123456789);
// 组合LocalDate和LocalTime
LocalDate date = LocalDate.of(2026, 2, 20);
LocalTime time = LocalTime.of(15, 30, 45);
LocalDateTime dt4 = LocalDateTime.of(date, time);
// 从字符串解析
LocalDateTime parsed = LocalDateTime.parse("2026-02-20T15:30:45");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime parsedCustom = LocalDateTime.parse("2026-02-20 15:30:45", formatter);
System.out.println("解析结果: " + parsedCustom);
// 2. 获取各部分
LocalDate datePart = dt4.toLocalDate();
LocalTime timePart = dt4.toLocalTime();
System.out.println("日期部分: " + datePart);
System.out.println("时间部分: " + timePart);
// 获取字段
System.out.println("年: " + dt4.getYear());
System.out.println("月: " + dt4.getMonthValue());
System.out.println("日: " + dt4.getDayOfMonth());
System.out.println("时: " + dt4.getHour());
// 3. 运算
LocalDateTime tomorrow = dt4.plusDays(1);
LocalDateTime nextWeek = dt4.plusWeeks(1);
LocalDateTime nextMonth = dt4.plusMonths(1);
LocalDateTime nextYear = dt4.plusYears(1);
LocalDateTime plusHours = dt4.plusHours(2);
LocalDateTime minusMinutes = dt4.minusMinutes(30);
System.out.println("明天此刻: " + tomorrow);
System.out.println("加2小时: " + plusHours);
// 链式调用
LocalDateTime result = dt4.plusYears(1)
.plusMonths(2)
.minusDays(3)
.plusHours(4);
System.out.println("链式结果: " + result);
// 4. 使用TemporalAdjusters(更复杂的调整)
// 例如:获取当月的最后一天
LocalDateTime lastDayOfMonth = dt4.with(
java.time.temporal.TemporalAdjusters.lastDayOfMonth()
);
System.out.println("当月最后一天: " + lastDayOfMonth);
}
}
5.6 Instant------机器时间(时间戳)
Instant表示时间线上的一个瞬时点,从1970-01-01T00:00:00Z开始计算的秒数和纳秒数,是面向机器的表示。
java
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
public class InstantDemo {
public static void main(String[] args) {
// 1. 获取当前Instant(UTC时间)
Instant now = Instant.now();
System.out.println("当前Instant: " + now); // 2026-02-20T07:30:45.123Z(Z表示UTC)
// 2. 从时间戳创建
Instant fromEpochMilli = Instant.ofEpochMilli(1773084600000L); // 毫秒
Instant fromEpochSecond = Instant.ofEpochSecond(1773084600L); // 秒
Instant fromEpochSecondWithNano = Instant.ofEpochSecond(1773084600L, 123456789); // 秒+纳秒
System.out.println("毫秒创建: " + fromEpochMilli);
// 3. 获取时间戳
long epochSecond = now.getEpochSecond(); // 秒
long epochMilli = now.toEpochMilli(); // 毫秒
int nano = now.getNano(); // 纳秒
System.out.println("秒: " + epochSecond);
System.out.println("毫秒: " + epochMilli);
System.out.println("纳秒: " + nano);
// 4. 运算
Instant plusSeconds = now.plusSeconds(3600); // 加1小时
Instant minusMillis = now.minusMillis(5000); // 减5秒
// 5. 比较
Instant earlier = Instant.now().minusSeconds(10);
Instant later = Instant.now().plusSeconds(10);
System.out.println("earlier before later? " + earlier.isBefore(later));
// 6. 与Date转换
Date date = Date.from(now); // Instant -> Date
Instant instantFromDate = date.toInstant(); // Date -> Instant
System.out.println("Date转回Instant: " + instantFromDate);
// 7. 转换为带时区的日期时间
ZonedDateTime beijingTime = now.atZone(ZoneId.of("Asia/Shanghai"));
System.out.println("北京时间: " + beijingTime);
}
}
关键理解:
Instant总是UTC时间,不附带时区信息- 适合用于时间戳记录、计算时间差、跨时区传输等场景
- 与
Date可以互相转换,是连接新旧API的桥梁
5.7 ZonedDateTime------带时区的日期时间
ZonedDateTime包含日期、时间和时区信息,解决了跨时区应用的需求。
java
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class ZonedDateTimeDemo {
public static void main(String[] args) {
// 1. 获取当前时区的日期时间
ZonedDateTime now = ZonedDateTime.now();
System.out.println("当前时区时间: " + now); // 2026-02-20T15:30:45.123+08:00[Asia/Shanghai]
// 2. 指定时区
ZonedDateTime nyNow = ZonedDateTime.now(ZoneId.of("America/New_York"));
System.out.println("纽约时间: " + nyNow);
// 3. 从LocalDateTime加时区
LocalDateTime localDateTime = LocalDateTime.of(2026, 2, 20, 15, 30);
ZonedDateTime zoned1 = localDateTime.atZone(ZoneId.of("Asia/Shanghai"));
ZonedDateTime zoned2 = ZonedDateTime.of(localDateTime, ZoneId.of("Asia/Shanghai"));
// 4. 直接指定
ZonedDateTime zoned3 = ZonedDateTime.of(2026, 2, 20, 15, 30, 45, 0,
ZoneId.of("Asia/Shanghai"));
// 5. 时区转换
ZonedDateTime beijing = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDateTime newYork = beijing.withZoneSameInstant(ZoneId.of("America/New_York"));
ZonedDateTime london = beijing.withZoneSameInstant(ZoneId.of("Europe/London"));
System.out.println("北京时间: " + beijing);
System.out.println("纽约时间: " + newYork);
System.out.println("伦敦时间: " + london);
// 6. 获取时区信息
ZoneId zone = beijing.getZone();
String zoneId = zone.getId(); // "Asia/Shanghai"
System.out.println("时区ID: " + zoneId);
// 7. 偏移量
java.time.ZoneOffset offset = beijing.getOffset();
System.out.println("偏移量: " + offset); // +08:00
// 8. 格式化输出
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss zzzz");
System.out.println("格式化: " + beijing.format(formatter)); // 2026-02-20 15:30:45 中国标准时间
}
}
5.8 Period与Duration------时间间隔
Period用于日期之间的间隔(年、月、日),Duration用于时间之间的间隔(时、分、秒、纳秒)。
java
import java.time.*;
import java.time.temporal.ChronoUnit;
public class PeriodDurationDemo {
public static void main(String[] args) {
// ========== Period(日期间隔)==========
LocalDate startDate = LocalDate.of(2020, 1, 1);
LocalDate endDate = LocalDate.of(2026, 2, 20);
Period period = Period.between(startDate, endDate);
System.out.println("日期差: " + period); // P6Y1M19D(6年1个月19天)
System.out.println("年差: " + period.getYears());
System.out.println("月差: " + period.getMonths());
System.out.println("日差: " + period.getDays());
// 创建Period
Period ofYears = Period.ofYears(5); // 5年
Period ofMonths = Period.ofMonths(3); // 3个月
Period ofWeeks = Period.ofWeeks(2); // 2周(即14天)
Period ofDays = Period.ofDays(10); // 10天
Period custom = Period.of(2, 6, 15); // 2年6个月15天
// 应用Period
LocalDate newDate = startDate.plus(period);
System.out.println("加上间隔后: " + newDate); // 2026-02-20
// ========== Duration(时间间隔)==========
LocalTime startTime = LocalTime.of(10, 0, 0);
LocalTime endTime = LocalTime.of(15, 30, 45);
Duration duration = Duration.between(startTime, endTime);
System.out.println("时间差: " + duration); // PT5H30M45S
System.out.println("小时差: " + duration.toHours()); // 5
System.out.println("分钟差: " + duration.toMinutes()); // 330
System.out.println("秒差: " + duration.getSeconds()); // 19845
// 创建Duration
Duration ofHours = Duration.ofHours(3); // 3小时
Duration ofMinutes = Duration.ofMinutes(45); // 45分钟
Duration ofSeconds = Duration.ofSeconds(120); // 120秒
Duration ofMillis = Duration.ofMillis(5000); // 5秒
Duration ofNanos = Duration.ofNanos(1000000); // 1毫秒
Duration of = Duration.of(2, ChronoUnit.HOURS); // 2小时
// 应用Duration
LocalTime newTime = startTime.plus(duration);
System.out.println("加上间隔后: " + newTime); // 15:30:45
// 更精确的计算(考虑纳秒)
Instant startInstant = Instant.now();
// 模拟操作
Instant endInstant = Instant.now().plusSeconds(3);
Duration elapsed = Duration.between(startInstant, endInstant);
System.out.println("耗时(秒): " + elapsed.getSeconds());
System.out.println("耗时(毫秒): " + elapsed.toMillis());
}
}
5.9 DateTimeFormatter------线程安全的格式化器
DateTimeFormatter是Java 8提供的格式化类,取代了SimpleDateFormat,并且是线程安全的。
java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;
public class DateTimeFormatterDemo {
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
// ========== 1. 预定义格式化器 ==========
DateTimeFormatter isoDate = DateTimeFormatter.ISO_DATE;
DateTimeFormatter isoDateTime = DateTimeFormatter.ISO_DATE_TIME;
System.out.println("ISO日期: " + now.format(isoDate)); // 2026-02-20
System.out.println("ISO日期时间: " + now.format(isoDateTime)); // 2026-02-20T15:30:45.123
// ========== 2. 本地化格式 ==========
DateTimeFormatter fullDate = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL);
DateTimeFormatter longDateTime = DateTimeFormatter.ofLocalizedDateTime(
FormatStyle.LONG, FormatStyle.MEDIUM);
System.out.println("本地化FULL: " + now.format(fullDate)); // 2026年2月20日 星期五
System.out.println("本地化LONG/MEDIUM: " + now.format(longDateTime)); // 2026年2月20日 15:30:45
// 指定Locale
DateTimeFormatter usFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
.withLocale(Locale.US);
System.out.println("美国格式: " + now.format(usFormatter)); // Friday, February 20, 2026
// ========== 3. 自定义模式(最常用)==========
DateTimeFormatter pattern1 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
DateTimeFormatter pattern2 = DateTimeFormatter.ofPattern("yyyy年MM月dd日 EEEE HH时mm分ss秒");
DateTimeFormatter pattern3 = DateTimeFormatter.ofPattern("yyyy/MM/dd hh:mm:ss a");
System.out.println("模式1: " + now.format(pattern1));
System.out.println("模式2: " + now.format(pattern2));
System.out.println("模式3: " + now.format(pattern3));
// ========== 4. 解析字符串 ==========
String dateStr = "2026-02-20 15:30:45";
DateTimeFormatter parser = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime parsed = LocalDateTime.parse(dateStr, parser);
System.out.println("解析结果: " + parsed);
// ========== 5. 线程安全演示 ==========
// 可以在多个线程中共享同一个formatter实例
DateTimeFormatter sharedFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 创建多个线程使用同一个formatter
for (int i = 0; i < 10; i++) {
new Thread(() -> {
String result = sharedFormatter.format(LocalDateTime.now());
System.out.println(Thread.currentThread().getName() + ": " + result);
}).start();
}
// 不会出现线程安全问题
}
}
5.10 时区处理------ZoneId与ZoneOffset
java
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Set;
public class ZoneDemo {
public static void main(String[] args) {
// ========== ZoneId(时区ID)==========
// 1. 系统默认时区
ZoneId defaultZone = ZoneId.systemDefault();
System.out.println("默认时区: " + defaultZone); // Asia/Shanghai
// 2. 通过ID获取
ZoneId shanghai = ZoneId.of("Asia/Shanghai");
ZoneId newYork = ZoneId.of("America/New_York");
ZoneId utc = ZoneId.of("UTC");
// 3. 所有可用时区
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
System.out.println("总时区数: " + zoneIds.size()); // 约600个
// 打印前10个
zoneIds.stream().limit(10).forEach(System.out::println);
// ========== ZoneOffset(偏移量)==========
ZoneOffset offset1 = ZoneOffset.of("+08:00");
ZoneOffset offset2 = ZoneOffset.ofHours(8);
ZoneOffset offset3 = ZoneOffset.ofHoursMinutes(8, 30);
ZoneOffset utcOffset = ZoneOffset.UTC; // 零偏移
System.out.println("偏移量+8: " + offset1); // +08:00
// 使用偏移量创建ZonedDateTime
ZonedDateTime zonedWithOffset = ZonedDateTime.now(offset1);
System.out.println("偏移量时间: " + zonedWithOffset);
// 时区转换示例:东京时间转纽约时间
ZonedDateTime tokyoTime = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
ZonedDateTime nyTime = tokyoTime.withZoneSameInstant(ZoneId.of("America/New_York"));
System.out.println("东京时间: " + tokyoTime);
System.out.println("对应纽约时间: " + nyTime);
}
}
5.11 日期时间调整------TemporalAdjusters
TemporalAdjusters提供了许多常用的日期调整工具。
java
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
public class TemporalAdjustersDemo {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2026, 2, 20); // 星期五
// 下一个/上一个/本月第一个/最后一个
LocalDate nextMonday = date.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
LocalDate previousSunday = date.with(TemporalAdjusters.previous(DayOfWeek.SUNDAY));
LocalDate firstDayOfMonth = date.with(TemporalAdjusters.firstDayOfMonth());
LocalDate lastDayOfMonth = date.with(TemporalAdjusters.lastDayOfMonth());
LocalDate firstDayOfNextMonth = date.with(TemporalAdjusters.firstDayOfNextMonth());
LocalDate firstDayOfYear = date.with(TemporalAdjusters.firstDayOfYear());
LocalDate lastDayOfYear = date.with(TemporalAdjusters.lastDayOfYear());
System.out.println("当前日期: " + date);
System.out.println("下周一: " + nextMonday);
System.out.println("上周日: " + previousSunday);
System.out.println("本月第一天: " + firstDayOfMonth);
System.out.println("本月最后一天: " + lastDayOfMonth);
// 本月第几个星期几
LocalDate thirdFriday = date.with(TemporalAdjusters.dayOfWeekInMonth(3, DayOfWeek.FRIDAY));
System.out.println("本月第三个星期五: " + thirdFriday);
// 下个月的今天(如果存在)
LocalDate nextMonthSameDay = date.plusMonths(1);
System.out.println("下个月今天: " + nextMonthSameDay);
}
}
第六章:新旧API对比与转换指南
6.1 三代API对比总结
| 维度 | 第一代 (Date/DateFormat) | 第二代 (Calendar) | 第三代 (java.time) |
|---|---|---|---|
| 核心类 | Date, SimpleDateFormat |
Calendar, GregorianCalendar |
LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant |
| 不可变性 | 可变 | 可变 | 不可变 |
| 线程安全 | 不安全 | 不安全 | 安全 |
| 月份偏移 | 0-11 | 0-11 | 1-12 |
| 年份偏移 | year-1900 | 正常 | 正常 |
| API设计 | 混乱 | 繁琐 | 清晰流畅 |
| 时区支持 | 弱 | 有 | 强大 |
| 性能 | 一般 | 较差(重量级) | 优秀 |
| 可读性 | 差 | 差 | 好 |
6.2 何时使用旧API,何时使用新API
使用旧API的场景(仅限于维护遗留代码):
- 正在维护使用JDK 7及以下版本的项目
- 与某些旧框架集成(如Hibernate早期版本)
- 需要与遗留数据库交互(部分JDBC驱动仍使用
java.sql.Date/Timestamp)
强烈推荐使用新API的场景(所有新项目):
- JDK 8及以上版本的新开发
- 需要复杂的日期计算
- 涉及时区转换
- 多线程环境下的日期处理
- 希望代码更清晰、更易维护
6.3 新旧API转换工具类
在实际开发中,经常需要在新旧API之间转换(例如,数据库操作可能返回java.sql.Date)。下面是一个完整的转换工具类:
java
import java.time.*;
import java.util.Date;
import java.util.Calendar;
import java.util.GregorianCalendar;
/**
* 新旧日期时间API转换工具类
*/
public class DateTimeConversionUtil {
// ========== java.util.Date <-> java.time ==========
/**
* Date -> Instant
*/
public static Instant toInstant(Date date) {
return date == null ? null : date.toInstant();
}
/**
* Instant -> Date
*/
public static Date toDate(Instant instant) {
return instant == null ? null : Date.from(instant);
}
/**
* Date -> LocalDate(系统默认时区)
*/
public static LocalDate toLocalDate(Date date) {
if (date == null) return null;
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
}
/**
* LocalDate -> Date(系统默认时区)
*/
public static Date toDate(LocalDate localDate) {
if (localDate == null) return null;
return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
}
/**
* Date -> LocalDateTime(系统默认时区)
*/
public static LocalDateTime toLocalDateTime(Date date) {
if (date == null) return null;
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
}
/**
* LocalDateTime -> Date(系统默认时区)
*/
public static Date toDate(LocalDateTime localDateTime) {
if (localDateTime == null) return null;
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
}
/**
* Date -> ZonedDateTime(系统默认时区)
*/
public static ZonedDateTime toZonedDateTime(Date date) {
if (date == null) return null;
return date.toInstant().atZone(ZoneId.systemDefault());
}
/**
* ZonedDateTime -> Date
*/
public static Date toDate(ZonedDateTime zonedDateTime) {
if (zonedDateTime == null) return null;
return Date.from(zonedDateTime.toInstant());
}
// ========== java.util.Calendar <-> java.time ==========
/**
* Calendar -> Instant
*/
public static Instant toInstant(Calendar calendar) {
if (calendar == null) return null;
return calendar.toInstant();
}
/**
* Instant -> GregorianCalendar
*/
public static GregorianCalendar toCalendar(Instant instant) {
if (instant == null) return null;
return GregorianCalendar.from(instant.atZone(ZoneId.systemDefault()));
}
/**
* Calendar -> ZonedDateTime
*/
public static ZonedDateTime toZonedDateTime(Calendar calendar) {
if (calendar == null) return null;
return ZonedDateTime.ofInstant(calendar.toInstant(), calendar.getTimeZone().toZoneId());
}
/**
* ZonedDateTime -> GregorianCalendar
*/
public static GregorianCalendar toCalendar(ZonedDateTime zonedDateTime) {
if (zonedDateTime == null) return null;
return GregorianCalendar.from(zonedDateTime);
}
/**
* Calendar -> LocalDateTime
*/
public static LocalDateTime toLocalDateTime(Calendar calendar) {
if (calendar == null) return null;
return LocalDateTime.ofInstant(calendar.toInstant(), calendar.getTimeZone().toZoneId());
}
/**
* LocalDateTime -> GregorianCalendar(指定时区)
*/
public static GregorianCalendar toCalendar(LocalDateTime localDateTime, ZoneId zoneId) {
if (localDateTime == null || zoneId == null) return null;
return GregorianCalendar.from(localDateTime.atZone(zoneId));
}
// ========== java.sql 相关 ==========
/**
* java.sql.Date -> LocalDate
*/
public static LocalDate toLocalDate(java.sql.Date sqlDate) {
return sqlDate == null ? null : sqlDate.toLocalDate();
}
/**
* LocalDate -> java.sql.Date
*/
public static java.sql.Date toSqlDate(LocalDate localDate) {
return localDate == null ? null : java.sql.Date.valueOf(localDate);
}
/**
* java.sql.Timestamp -> LocalDateTime
*/
public static LocalDateTime toLocalDateTime(java.sql.Timestamp timestamp) {
return timestamp == null ? null : timestamp.toLocalDateTime();
}
/**
* LocalDateTime -> java.sql.Timestamp
*/
public static java.sql.Timestamp toTimestamp(LocalDateTime localDateTime) {
return localDateTime == null ? null : java.sql.Timestamp.valueOf(localDateTime);
}
/**
* java.sql.Time -> LocalTime
*/
public static LocalTime toLocalTime(java.sql.Time sqlTime) {
return sqlTime == null ? null : sqlTime.toLocalTime();
}
/**
* LocalTime -> java.sql.Time
*/
public static java.sql.Time toSqlTime(LocalTime localTime) {
return localTime == null ? null : java.sql.Time.valueOf(localTime);
}
// ========== 使用示例 ==========
public static void main(String[] args) {
// Date <-> LocalDateTime
Date now = new Date();
LocalDateTime ldt = toLocalDateTime(now);
Date backToDate = toDate(ldt);
System.out.println("原始Date: " + now);
System.out.println("转LocalDateTime: " + ldt);
System.out.println("转回Date: " + backToDate);
System.out.println("是否相等: " + now.equals(backToDate));
// Calendar <-> ZonedDateTime
Calendar calendar = Calendar.getInstance();
ZonedDateTime zdt = toZonedDateTime(calendar);
GregorianCalendar backToCal = toCalendar(zdt);
System.out.println("原始Calendar: " + calendar.getTime());
System.out.println("转ZonedDateTime: " + zdt);
System.out.println("转回Calendar: " + backToCal.getTime());
}
}
第七章:最佳实践与常见陷阱
7.1 开发中应该选择哪个API?
决策树:
- 项目JDK版本 ≥ 8? → 使用
java.time包 - 需要与遗留代码/库交互? → 在边界处转换,核心逻辑仍用
java.time - 需要数据库操作? → 使用
java.time类型与JPA 2.2+(支持LocalDate等) - 需要高性能、线程安全的格式化? → 使用
DateTimeFormatter - 维护JDK 7及以下项目? → 只能使用
Calendar+SimpleDateFormat(注意线程安全)
7.2 常见陷阱与解决方案
陷阱1:月份从0开始(Calendar/Date)
java
// 错误
Calendar cal = Calendar.getInstance();
cal.set(2026, 2, 20); // 以为是2月20日,实际是3月20日
// 正确
cal.set(2026, Calendar.FEBRUARY, 20); // 使用Calendar常量
// 或者
cal.set(2026, 1, 20); // 月份0=1月,1=2月
// 最佳:使用Java 8
LocalDate date = LocalDate.of(2026, 2, 20); // 直接使用2
陷阱2:SimpleDateFormat线程不安全
java
// 错误
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 多个线程共用,导致数据错乱
// 正确
private static final ThreadLocal<SimpleDateFormat> sdfHolder =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
// 最佳
private static final DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd");
陷阱3:时区混淆
java
// 错误:认为LocalDateTime有时区
LocalDateTime now = LocalDateTime.now(); // 实际上只是系统默认时区的本地时间,不包含时区信息
// 需要时区时应使用ZonedDateTime
ZonedDateTime zonedNow = ZonedDateTime.now();
// 跨时区转换正确做法
ZonedDateTime beijing = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDateTime newYork = beijing.withZoneSameInstant(ZoneId.of("America/New_York"));
陷阱4:时间精度丢失
java
// Date只能精确到毫秒
Date date = new Date();
long millis = date.getTime(); // 毫秒
// Instant可以精确到纳秒
Instant instant = Instant.now();
long seconds = instant.getEpochSecond();
int nanos = instant.getNano(); // 纳秒部分
// 互相转换时可能丢失精度
Instant nanoInstant = Instant.now();
Date dateFromInstant = Date.from(nanoInstant); // 纳秒部分被截断为毫秒
陷阱5:Period与Duration混淆
java
// Period用于日期(年、月、日)
LocalDate start = LocalDate.of(2026, 1, 1);
LocalDate end = LocalDate.of(2026, 2, 20);
Period period = Period.between(start, end);
System.out.println(period.getDays()); // 19?不对,是19天,但月份部分也是1个月
// Duration用于时间(时、分、秒)
LocalTime startTime = LocalTime.of(10, 0);
LocalTime endTime = LocalTime.of(15, 30);
Duration duration = Duration.between(startTime, endTime);
System.out.println(duration.toMinutes()); // 330分钟
// 不要用Period计算时间差,用Duration
7.3 性能考虑
java.time性能优于Calendar:Calendar内部有复杂的字段计算和同步开销DateTimeFormatter重用 :由于线程安全,可以定义为static final常量重用- 避免频繁创建
Instant/LocalDateTime:除非必要,否则使用now()获取当前时间即可 - 大量日期计算时考虑使用
java.time:API设计更高效
7.4 代码示例:业务场景实战
场景1:计算年龄
java
import java.time.LocalDate;
import java.time.Period;
public class AgeCalculator {
public static int calculateAge(LocalDate birthDate) {
LocalDate today = LocalDate.now();
return Period.between(birthDate, today).getYears();
}
public static void main(String[] args) {
LocalDate birth = LocalDate.of(1990, 5, 15);
int age = calculateAge(birth);
System.out.println("年龄: " + age); // 根据当前日期计算
}
}
场景2:订单超时判断(30分钟未支付取消)
java
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
public class OrderTimeout {
public static boolean isTimeout(LocalDateTime orderTime, int timeoutMinutes) {
LocalDateTime now = LocalDateTime.now();
long minutesElapsed = ChronoUnit.MINUTES.between(orderTime, now);
return minutesElapsed >= timeoutMinutes;
}
public static void main(String[] args) {
LocalDateTime orderTime = LocalDateTime.now().minusMinutes(25);
boolean timeout = isTimeout(orderTime, 30);
System.out.println("订单是否超时: " + timeout); // false
orderTime = LocalDateTime.now().minusMinutes(35);
timeout = isTimeout(orderTime, 30);
System.out.println("订单是否超时: " + timeout); // true
}
}
场景3:获取某月的所有周末
java
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.YearMonth;
import java.util.ArrayList;
import java.util.List;
public class WeekendsInMonth {
public static List<LocalDate> getWeekends(int year, int month) {
List<LocalDate> weekends = new ArrayList<>();
YearMonth yearMonth = YearMonth.of(year, month);
LocalDate firstOfMonth = yearMonth.atDay(1);
LocalDate lastOfMonth = yearMonth.atEndOfMonth();
LocalDate date = firstOfMonth;
while (!date.isAfter(lastOfMonth)) {
DayOfWeek dow = date.getDayOfWeek();
if (dow == DayOfWeek.SATURDAY || dow == DayOfWeek.SUNDAY) {
weekends.add(date);
}
date = date.plusDays(1);
}
return weekends;
}
public static void main(String[] args) {
List<LocalDate> weekends = getWeekends(2026, 2);
System.out.println("2026年2月周末:");
weekends.forEach(System.out::println);
}
}
场景4:国际化日期显示
java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;
public class I18nDateDemo {
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
// 中文显示
DateTimeFormatter chineseFormatter = DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.FULL, FormatStyle.MEDIUM)
.withLocale(Locale.CHINA);
System.out.println("中文: " + now.format(chineseFormatter));
// 英文显示
DateTimeFormatter usFormatter = DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.FULL, FormatStyle.MEDIUM)
.withLocale(Locale.US);
System.out.println("英文: " + now.format(usFormatter));
// 日文显示
DateTimeFormatter japanFormatter = DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.FULL, FormatStyle.MEDIUM)
.withLocale(Locale.JAPAN);
System.out.println("日文: " + now.format(japanFormatter));
}
}
第八章:总结与展望
8.1 三代API的演进之路回顾
-
第一代(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等)
附录:常用代码片段速查
获取当前时间
java
// 旧
Date now = new Date();
Calendar cal = Calendar.getInstance();
// 新
LocalDate today = LocalDate.now();
LocalTime nowTime = LocalTime.now();
LocalDateTime nowDateTime = LocalDateTime.now();
Instant instant = Instant.now();
创建指定日期
java
// 旧
Calendar cal = Calendar.getInstance();
cal.set(2026, Calendar.FEBRUARY, 20); // 注意月份常量
// 新
LocalDate date = LocalDate.of(2026, 2, 20);
LocalDateTime dt = LocalDateTime.of(2026, 2, 20, 15, 30, 45);
日期加减
java
// 旧
cal.add(Calendar.DAY_OF_MONTH, 5);
cal.add(Calendar.MONTH, -2);
// 新
LocalDate newDate = date.plusDays(5).minusMonths(2);
格式化
java
// 旧(注意线程安全问题)
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String formatted = sdf.format(new Date());
// 新
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted = LocalDateTime.now().format(dtf);
解析字符串
java
// 旧
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = sdf.parse("2026-02-20");
// 新
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate date = LocalDate.parse("2026-02-20", dtf);
计算两个日期的天数差
java
// 旧(繁琐)
long days = (date2.getTime() - date1.getTime()) / (24*60*60*1000);
// 新
long days = ChronoUnit.DAYS.between(date1, date2);
时区转换
java
// 旧(麻烦)
TimeZone tz = TimeZone.getTimeZone("America/New_York");
Calendar cal = Calendar.getInstance(tz);
// 新
ZonedDateTime nyTime = ZonedDateTime.now(ZoneId.of("America/New_York"));
ZonedDateTime shanghaiTime = nyTime.withZoneSameInstant(ZoneId.of("Asia/Shanghai"));