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

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

在 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、SimpleDateFormatCalendar、GregorianCalendar、TimeZoneLocalDate、LocalTime、LocalDateTime、ZonedDateTime、Instant、DateTimeFormatter 等我们将从源码层面剖析其设计原理,探讨线程安全问题,提供最佳实践,并给出新旧 API 的转换指南。
在深入 Java API 之前,我们需要理解计算机系统中时间的基本表示方法。
几乎所有计算机系统都采用一个共同的时间原点——1970 年 1 月 1 日 00:00:00 UTC(协调世界时)。这个时间点被称为 Unix 纪元(Unix Epoch)。
为什么选择这个时间?这主要源于 Unix 操作系统的历史原因。1970 年左右,Unix 系统诞生,设计者选择了一个相对"干净"的时间起点。此后,几乎所有类 Unix 系统(包括 Linux、macOS)以及 Java 等编程语言都沿用了这一约定。
计算机系统中有两种表示时间的模型:
1. 面向人类的模型
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");
}
}
时区(TimeZone):由于地球的自转,不同经度的地区时间不同。时区将地球划分为 24 个区域,每个区域相差 1 小时。UTC(协调世界时)是时间基准,北京时间为 UTC+8。
历法(Calendar):不同文化使用不同的历法系统,如公历(GregorianCalendar)、农历、伊斯兰历等。Java 主要支持公历(ISO-8601 标准),但 Calendar 设计时考虑了扩展性,可以支持其他历法。
java.util.Date 是 Java 中最早出现的日期时间类,自 JDK 1.0 起就存在。
查看 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 {
getMillisOf() < getMillisOf();
}
{
getMillisOf() > getMillisOf();
}
String {
toString(ZoneId.systemDefault());
}
}
关键点分析:
fastTime:这是一个 long 类型的变量,存储从 1970-01-01 UTC 开始的毫秒数。整个 Date 对象本质上就是这个数字的封装。@Deprecated 标记,不再推荐使用。getTime() 和 setTime() 用于获取和设置毫秒数;before()、after()、compareTo() 用于日期比较;toString() 用于输出。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);
}
}
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
}
}
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");
}
}
Date 类的大部分方法在 JDK 1.1 之后就被标记为 @Deprecated,主要原因如下:
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 年。这完全是反直觉的设计。
@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 月)完全不符,极易导致月份错误。
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 实例且进行修改,会产生竞态条件。
// 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() 方法格式固定,不考虑不同国家和语言的日期表示习惯。
java.util.Date 实际上既包含日期也包含时间,但类名却只叫"Date",容易让人误以为它只处理日期部分。
鉴于上述缺陷,官方建议:
new Date() 和 new Date(long) 构造、getTime()、setTime()、before()、after()、compareTo()@Deprecated 标记的方法// 推荐的使用方式:仅将 Date 作为时间戳对象
Date now = new Date();
// 当前时刻
long timestamp = now.getTime();
// 获取毫秒数
// 如果需要操作年月日,使用 Calendar
Calendar cal = Calendar.getInstance();
cal.setTime(now);
int year = cal.get(Calendar.YEAR);
// 正确获取年份
Date 类本身无法控制输出的格式,因此 Java 提供了专门的格式化类 DateFormat 及其子类 SimpleDateFormat,用于在 Date 对象和字符串之间进行转换。
java.text.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" |
// 获取日期格式化器
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);
// 指定风格
// Date -> String 格式化
public final String format(Date date)
// String -> Date 解析
public Date parse(String source) throws ParseException
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.getDateTimeInstance(DateFormat.LONG, DateFormat.MEDIUM, Locale.CHINA);
System.out.println( + dtf.format(now));
{
;
DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.CHINA);
parser.parse(dateStr);
System.out.println( + parsed);
} (Exception e) {
e.printStackTrace();
}
}
}
java.text.SimpleDateFormat 是 DateFormat 的具体子类,允许通过模式字符串自定义日期时间格式。
// 使用默认模式
SimpleDateFormat()
// 使用指定模式
SimpleDateFormat(String pattern)
// 使用指定模式和区域
SimpleDateFormat(String pattern, Locale locale)
| 字母 | 日期/时间元素 | 示例 |
|---|---|---|
| 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 |
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( + sdf4.format(now));
();
System.out.println( + sdf5.format(now));
{
;
();
parser.parse(dateStr);
System.out.println( + parsed);
} (Exception e) {
e.printStackTrace();
}
}
}
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().format(now));
System.out.println( + ().format(now));
System.out.println( + ().format(now));
System.out.println( + ().format(now));
System.out.println( + ().format(now));
System.out.println( + ().format(now));
System.out.println( + ().format(now));
System.out.println( + ().format(now));
}
}
核心问题:SimpleDateFormat 是线程不安全的。
查看 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 状态已被改变,产生错误结果甚至程序崩溃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 + + e);
}
});
}
executor.shutdown();
}
}
运行上述代码,可能出现:
NumberFormatException 等异常方案 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。
为了解决 Date 类的缺陷,JDK 1.1 引入了 java.util.Calendar 类,它是一个抽象类,提供了更强大的日期字段操作能力。
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 ;
;
;
;
;
;
;
;
;
;
;
;
;
;
;
Calendar {
createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));
}
;
;
{
complete();
internalGet(field);
}
{
isTimeSet = ;
fields[field] = value;
isSet[field] = ;
}
;
;
Date {
(getTimeInMillis());
}
{
setTimeInMillis(date.getTime());
}
}
关键设计解析:
Calendar 内部同时维护两种表示——time(毫秒数)和 fields[](字段数组)。当修改字段时,isTimeSet 标记设为 false,表示 time 需要重新计算;当设置时间时,fields[] 会被重新计算。get(int field) 方法调用 complete(),如果 isTimeSet 为 false,则调用 computeTime() 从 fields[] 计算 time;如果 fields[] 不完整,则调用 computeFields() 从 time 计算 fields[]。这种延迟计算提高了性能。fields[] 数组的索引,每个常量代表一个日历字段。理解这些常量是使用 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(公历)实例。
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);
cal.get(Calendar.WEEK_OF_MONTH);
cal.get(Calendar.AM_PM);
System.out.printf(, year);
System.out.printf(, month, month + );
System.out.printf(, day);
System.out.printf(, hour24);
System.out.printf(, hour12, ampm == ? : );
System.out.printf(, minute);
System.out.printf(, second);
System.out.printf(, millis);
System.out.printf(, dayOfWeek);
System.out.printf(, dayOfYear);
System.out.printf(, weekOfYear);
}
}
重要提醒:
Calendar.MONTH 从 0 开始(0=一月,11=十二月)Calendar.DAY_OF_WEEK:1=周日,2=周一,…,7=周六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(, , );
System.out.println( + wrong.getTime());
}
}
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, );
System.out.println( + sdf.format(cal.getTime()));
cal.set(, , );
cal.add(Calendar.MONTH, );
System.out.println( + sdf.format(cal.getTime()));
cal.set(, , );
cal.add(Calendar.DAY_OF_MONTH, -);
System.out.println( + sdf.format(cal.getTime()));
}
}
典型应用:计算 30 天后、3 个月前、5 年后等。
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, );
System.out.println( + sdf.format(cal.getTime()));
}
}
适用场景:当你只想在字段范围内循环(如调整日期但不想改变月份)时使用。
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 日(具体规则依赖于实现)。
严格模式:字段值非法时抛出异常,适合需要严格校验的场景。
add() 和 roll() 方法支持灵活的日期运算roll)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));
}
}
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(, , );
Calendar.getInstance();
cal2.set(, , );
daysBetween(cal1.getTime(), cal2.getTime());
System.out.println( + (days + ) + );
}
}
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());
}
}
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(世纪年规则)
}
}
鉴于第一代 Date 和第二代 Calendar 的种种缺陷,Java 8 引入了全新的日期时间 API——java.time 包(JSR 310),它深受 Joda-Time 库的启发,提供了更优雅、更安全、更强大的日期时间处理能力。
Java 8 日期时间 API 的设计遵循以下核心原则:
final 的,且内部状态不可变,任何修改操作都返回新对象,天然线程安全| 类名 | 用途 | 示例 |
|---|---|---|
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 |
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.ofYearDay(, );
System.out.println( + fromYearDay);
LocalDate.ofEpochDay();
System.out.println( + fromEpoch);
}
}
重要:LocalDate 的月份从 1 开始,符合人类直觉,再也不需要 month+1 了!
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());
System.out.println( + date.lengthOfYear());
}
}
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(, ChronoUnit.WEEKS);
System.out.println( + plus);
date.minusDays();
System.out.println( + minusDays);
date.plusYears().plusMonths().minusDays();
System.out.println( + result);
}
}
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));
}
}
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());
System.out.println( + time2.getSecond());
System.out.println( + time2.getNano());
time2.plusHours();
time2.plusMinutes();
time2.plus(, ChronoUnit.SECONDS);
time2.minusHours();
System.out.println( + plusHours);
System.out.println( + plusMinutes);
System.out.println( + minus);
LocalTime.of(, );
LocalTime.of(, );
System.out.println( + timeA.isBefore(timeB));
}
}
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, , );
LocalTime.of(, , );
LocalDateTime.of(date, time);
LocalDateTime.parse();
DateTimeFormatter.ofPattern();
LocalDateTime.parse(, formatter);
System.out.println( + parsedCustom);
dt4.toLocalDate();
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());
dt4.plusDays();
dt4.plusWeeks();
dt4.plusMonths();
dt4.plusYears();
dt4.plusHours();
dt4.minusMinutes();
System.out.println( + tomorrow);
System.out.println( + plusHours);
dt4.plusYears().plusMonths().minusDays().plusHours();
System.out.println( + result);
dt4.with(java.time.temporal.TemporalAdjusters.lastDayOfMonth());
System.out.println( + lastDayOfMonth);
}
}
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);
now.plusSeconds();
now.minusMillis();
Instant.now().minusSeconds();
Instant.now().plusSeconds();
System.out.println( + earlier.isBefore(later));
Date.from(now);
date.toInstant();
System.out.println( + instantFromDate);
now.atZone(ZoneId.of());
System.out.println( + beijingTime);
}
}
关键理解:
Instant 总是 UTC 时间,不附带时区信息Date 可以互相转换,是连接新旧 API 的桥梁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, , , , , ZoneId.of());
ZonedDateTime.now(ZoneId.of());
beijing.withZoneSameInstant(ZoneId.of());
beijing.withZoneSameInstant(ZoneId.of());
System.out.println( + beijing);
System.out.println( + newYork);
System.out.println( + london);
beijing.getZone();
zone.getId();
System.out.println( + zoneId);
java.time. beijing.getOffset();
System.out.println( + offset);
DateTimeFormatter.ofPattern();
System.out.println( + beijing.format(formatter));
}
}
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();
Period.of(, , );
startDate.plus(period);
System.out.println( + newDate);
LocalTime.of(, , );
LocalTime.of(, , );
Duration.between(startTime, endTime);
System.out.println( + duration);
System.out.println( + duration.toHours());
System.out.println( + duration.toMinutes());
System.out.println( + duration.getSeconds());
Duration.ofHours();
Duration.ofMinutes();
Duration.ofSeconds();
Duration.ofMillis();
Duration.ofNanos();
Duration.of(, ChronoUnit.HOURS);
startTime.plus(duration);
System.out.println( + newTime);
Instant.now();
Instant.now().plusSeconds();
Duration.between(startInstant, endInstant);
System.out.println( + elapsed.getSeconds());
System.out.println( + elapsed.toMillis());
}
}
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));
DateTimeFormatter.ofPattern();
DateTimeFormatter.ofPattern();
DateTimeFormatter.ofPattern();
System.out.println( + now.format(pattern1));
System.out.println( + now.format(pattern2));
System.out.println( + now.format(pattern3));
;
DateTimeFormatter.ofPattern();
LocalDateTime.parse(dateStr, parser);
System.out.println( + parsed);
DateTimeFormatter.ofPattern();
( ; i < ; i++) {
(() -> {
sharedFormatter.format(LocalDateTime.now());
System.out.println(Thread.currentThread().getName() + + result);
}).start();
}
}
}
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 ZoneOffset.ofHoursMinutes(, );
ZoneOffset.UTC;
System.out.println( + offset1);
ZonedDateTime.now(offset1);
System.out.println( + zonedWithOffset);
ZonedDateTime.now(ZoneId.of());
tokyoTime.withZoneSameInstant(ZoneId.of());
System.out.println( + tokyoTime);
System.out.println( + nyTime);
}
}
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);
date.with(TemporalAdjusters.dayOfWeekInMonth(, DayOfWeek.FRIDAY));
System.out.println( + thirdFriday);
date.plusMonths();
System.out.println( + nextMonthSameDay);
}
}
| 维度 | 第一代 (Date/DateFormat) | 第二代 (Calendar) | 第三代 (java.time) |
|---|---|---|---|
| 核心类 | Date, SimpleDateFormat | Calendar, GregorianCalendar | LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant |
| 不可变性 | 可变 | 可变 | 不可变 |
| 线程安全 | 不安全 | 不安全 | 安全 |
| 月份偏移 | 0-11 | 0-11 | 1-12 |
| 年份偏移 | year-1900 | 正常 | 正常 |
| API 设计 | 混乱 | 繁琐 | 清晰流畅 |
| 时区支持 | 弱 | 有 | 强大 |
| 性能 | 一般 | 较差(重量级) | 优秀 |
| 可读性 | 差 | 差 | 好 |
使用旧 API 的场景(仅限于维护遗留代码):
java.sql.Date/Timestamp)强烈推荐使用新 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(系统默认时区)
*/
LocalDateTime {
(date == ) ;
date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
}
Date {
(localDateTime == ) ;
Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
}
ZonedDateTime {
(date == ) ;
date.toInstant().atZone(ZoneId.systemDefault());
}
Date {
(zonedDateTime == ) ;
Date.from(zonedDateTime.toInstant());
}
Instant {
(calendar == ) ;
calendar.toInstant();
}
GregorianCalendar {
(instant == ) ;
GregorianCalendar.from(instant.atZone(ZoneId.systemDefault()));
}
ZonedDateTime {
(calendar == ) ;
ZonedDateTime.ofInstant(calendar.toInstant(), calendar.getTimeZone().toZoneId());
}
GregorianCalendar {
(zonedDateTime == ) ;
GregorianCalendar.from(zonedDateTime);
}
LocalDateTime {
(calendar == ) ;
LocalDateTime.ofInstant(calendar.toInstant(), calendar.getTimeZone().toZoneId());
}
GregorianCalendar {
(localDateTime == || zoneId == ) ;
GregorianCalendar.from(localDateTime.atZone(zoneId));
}
LocalDate {
sqlDate == ? : sqlDate.toLocalDate();
}
java.sql.Date {
localDate == ? : java.sql.Date.valueOf(localDate);
}
LocalDateTime {
timestamp == ? : timestamp.toLocalDateTime();
}
java.sql.Timestamp {
localDateTime == ? : java.sql.Timestamp.valueOf(localDateTime);
}
LocalTime {
sqlTime == ? : sqlTime.toLocalTime();
}
java.sql.Time {
localTime == ? : java.sql.Time.valueOf(localTime);
}
{
();
toLocalDateTime(now);
toDate(ldt);
System.out.println( + now);
System.out.println( + ldt);
System.out.println( + backToDate);
System.out.println( + now.equals(backToDate));
Calendar.getInstance();
toZonedDateTime(calendar);
toCalendar(zdt);
System.out.println( + calendar.getTime());
System.out.println( + zdt);
System.out.println( + backToCal.getTime());
}
}
决策树:
java.time 包java.timejava.time 类型与 JPA 2.2+(支持 LocalDate 等)DateTimeFormatterCalendar + SimpleDateFormat(注意线程安全)// 错误
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
// 错误
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");
// 错误:认为 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"));
// 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); // 纳秒部分被截断为毫秒
// 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
java.time 性能优于 Calendar:Calendar 内部有复杂的字段计算和同步开销DateTimeFormatter 重用:由于线程安全,可以定义为 static final 常量重用Instant/LocalDateTime:除非必要,否则使用 now() 获取当前时间即可java.time:API 设计更高效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); // 根据当前日期计算
}
}
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
}
}
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);
}
}
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));
}
}
| 核心类 | 用途 | 关键特性 |
|---|---|---|
Date | 表示时间戳(已过时) | 可变、线程不安全、月份 0-11 |
Calendar | 日历字段操作(已过时) | 可变、线程不安全、月份 0-11、API 繁琐 |
LocalDate | 日期(无时间) | 不可变、线程安全、月份 1-12 |
LocalTime | 时间(无日期) | 不可变、线程安全 |
LocalDateTime | 日期 + 时间(无时区) | 不可变、线程安全 |
ZonedDateTime | 带时区的日期时间 | 不可变、线程安全、时区转换 |
Instant | 时间戳(机器时间) | 不可变、线程安全、UTC |
DateTimeFormatter | 格式化/解析 | 线程安全、取代 SimpleDateFormat |
Period/Duration | 时间间隔 | 不可变、线程安全 |
随着 Java 的持续发展,日期时间 API 也在不断完善:
java.time 包进行了细微优化和 bug 修复Record、Pattern Matching 等新特性更好地集成java.time 包,告别旧 API 的烦恼java.timeSimpleDateFormat 在多线程环境下必须采取保护措施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"));

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online