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