Java时间处理详解:从Date、Calendar到java.time新API
时间处理是Java开发中最基础、最高频的场景之一——用户注册时间记录、订单超时判断、定时任务调度、日志时间戳生成等,几乎所有Java应用都离不开时间操作。但Java的时间API经历了多轮迭代,从早期的Date、Calendar,到Java 8推出的java.time包新API,不同API的用法、特点差异较大,新手容易混淆,甚至踩坑。
本文将系统梳理Java中所有常用的时间相关类,从旧API的缺陷、新API的优势,到具体用法、实战案例、避坑技巧,层层递进,帮你彻底掌握Java时间处理,无论是维护遗留系统(用Date/Calendar),还是开发新项目(用java.time),都能从容应对。
一、Java时间API的发展历程:从混乱到规范
Java的时间API发展主要分为三个阶段,理解这个历程,能更好地明白不同API的设计初衷和适用场景,避免盲目使用:
-
JDK 1.0 阶段:推出
java.util.Date类,作为最早的时间处理类,仅能表示一个时间点(精确到毫秒),API设计简陋,存在诸多缺陷; -
JDK 1.1 阶段:推出
java.util.Calendar和java.text.SimpleDateFormat,解决Date类操作繁琐的问题,支持时间字段(年、月、日)的获取和计算,但仍有设计缺陷和线程安全问题; -
Java 8(JDK 1.8)阶段:推出
java.time包(JSR 310规范),包含LocalDate、LocalTime、LocalDateTime等核心类,彻底解决旧API的缺陷,实现了不可变性、线程安全,API设计更直观、规范,是目前推荐的首选方案。
核心结论:新项目开发优先使用Java 8新API;维护遗留系统需掌握Date、Calendar,同时建议逐步迁移到新API。
二、旧时代API:Date与Calendar(遗留系统必备)
虽然Java 8新API已成为主流,但很多遗留系统仍在使用Date和Calendar,掌握它们的用法和缺陷,是维护旧系统的关键。
2.1 java.util.Date类:最早的时间类
Date类的核心作用是「表示一个具体的时间点」,内部存储的是自1970年1月1日00:00:00 GMT(格林尼治标准时间)以来的毫秒数(即时间戳),本身不具备时间字段(年、月、日)的操作能力,需配合SimpleDateFormat格式化,或Calendar进行字段操作。
2.1.1 Date类核心用法
import java.util.Date;
public class DateDemo {
public static void main(String[] args) {
// 1. 创建当前时间的Date对象(默认系统时区)
Date now = new Date();
System.out.println("当前时间:" + now); // 输出格式:Sat Apr 13 09:58:00 CST 2026
// 2. 根据时间戳创建Date对象(毫秒级,1000L=1秒)
long timestamp = 1712995080000L; // 对应2026-04-13 09:58:00
Date specificDate = new Date(timestamp);
System.out.println("指定时间戳的时间:" + specificDate);
// 3. 核心方法:获取时间戳(毫秒)
long currentTimestamp = now.getTime();
System.out.println("当前时间戳(毫秒):" + currentTimestamp);
// 4. 时间比较(before、after、equals)
Date pastDate = new Date(timestamp - 86400000L); // 前一天
System.out.println("now在pastDate之后:" + now.after(pastDate)); // true
System.out.println("now在pastDate之前:" + now.before(pastDate)); // false
System.out.println("两个时间相等:" + now.equals(specificDate)); // 取决于时间戳是否一致
}
}
2.1.2 Date类的致命缺陷(必记)
Date类的设计存在诸多不合理之处,这也是它被逐渐淘汰的核心原因:
-
可变性:Date对象是可变的,调用setTime()等方法可直接修改其表示的时间,多线程环境下存在线程安全问题;
-
API混乱:很多方法已被废弃(如getYear()、getMonth()),且返回值设计不合理(如getYear()返回的是“年份-1900”,getMonth()返回0-11代表1-12月);
-
无明确时区概念:Date本身不存储时区信息,默认依赖系统时区,容易导致时区混淆;
-
功能单一:仅能表示时间点,无法直接处理日期、时间的字段(如获取当月天数、加减月份),需依赖其他类。
2.2 java.util.Calendar类:Date的增强版
Calendar是抽象类,不能直接实例化,需通过Calendar.getInstance()获取子类(默认是GregorianCalendar,公历)实例。它的核心作用是「处理时间字段」,解决了Date类无法操作年、月、日等字段的问题,支持时间计算、字段设置等操作。
2.2.1 Calendar类核心用法
import java.util.Calendar;
public class CalendarDemo {
public static void main(String[] args) {
// 1. 获取Calendar实例(默认系统时区、当前时间)
Calendar cal = Calendar.getInstance();
// 2. 获取时间字段(核心用法,需注意字段的取值范围)
int year = cal.get(Calendar.YEAR); // 年份(4位,如2026)
int month = cal.get(Calendar.MONTH) + 1; // 月份:0-11,需+1才是实际月份
int day = cal.get(Calendar.DAY_OF_MONTH); // 当月日期(1-31)
int hour = cal.get(Calendar.HOUR_OF_DAY); // 24小时制小时(0-23)
int minute = cal.get(Calendar.MINUTE); // 分钟(0-59)
int second = cal.get(Calendar.SECOND); // 秒(0-59)
int week = cal.get(Calendar.DAY_OF_WEEK); // 星期:1-7(1=周日,2=周一...7=周六)
System.out.printf("当前时间:%d年%d月%d日 %02d:%02d:%02d,星期%s%n",
year, month, day, hour, minute, second,
week == 1 ? "日" : (week - 1) + "");
// 3. 设置时间字段(修改Calendar对象)
cal.set(Calendar.YEAR, 2025); // 设置年份为2025
cal.set(Calendar.MONTH, 11); // 设置月份为12月(0-11)
cal.set(Calendar.DAY_OF_MONTH, 31); // 设置日期为31日
System.out.println("设置后的时间:" + cal.getTime()); // 转换为Date对象输出
// 4. 时间计算(加减操作)
cal.add(Calendar.DAY_OF_MONTH, 1); // 日期加1天(跨年/跨月会自动调整)
cal.add(Calendar.MONTH, -1); // 月份减1个月
System.out.println("计算后的时间:" + cal.getTime());
// 5. 转换为Date对象
java.util.Date date = cal.getTime();
System.out.println("Calendar转Date:" + date);
}
}
2.2.2 Calendar类的缺陷(仍需注意)
Calendar虽然解决了Date类的部分问题,但依然存在不足,无法满足现代开发的需求:
-
线程不安全:与Date类一样,Calendar对象是可变的,多线程共享实例会导致并发问题;
-
API设计繁琐:字段操作需通过常量(如Calendar.YEAR),且月份、星期的取值范围不直观,容易踩坑;
-
时区处理复杂:时区相关操作繁琐,容易出现时区偏移错误;
-
不可链式调用:时间计算、字段设置需多次调用方法,代码可读性差。
2.3 java.text.SimpleDateFormat:时间格式化工具
Date和Calendar本身不支持自定义格式的字符串转换,需配合SimpleDateFormat类,实现「Date对象 ↔ 字符串」的双向转换。但SimpleDateFormat存在严重的线程安全问题,是开发中常见的坑点。
2.3.1 核心用法:格式化与解析
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SimpleDateFormatDemo {
public static void main(String[] args) throws ParseException {
// 1. 定义格式化模板(区分大小写,不可写错)
// yyyy:4位年份,MM:2位月份,dd:2位日期,HH:24小时制,hh:12小时制,mm:分钟,ss:秒
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 2. Date对象 → 字符串(格式化)
Date now = new Date();
String timeStr = sdf.format(now);
System.out.println("格式化后的时间:" + timeStr); // 输出:2026-04-13 09:58:00
// 3. 字符串 → Date对象(解析)
String dateStr = "2025-12-31 23:59:59";
Date date = sdf.parse(dateStr);
System.out.println("解析后的Date对象:" + date);
// 4. 其他常用模板
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒");
System.out.println("中文格式:" + sdf2.format(now));
}
}
2.3.2 致命坑点:线程不安全
SimpleDateFormat是非线程安全的,其内部存在共享的Calendar实例,多线程并发调用format()或parse()方法时,会导致格式错乱、解析异常甚至数据污染。
避坑方案(二选一):
-
每次使用时新建SimpleDateFormat实例(轻量,适合低并发场景);
-
使用ThreadLocal缓存SimpleDateFormat实例(推荐,适合高并发场景),避免多线程共享。
// 高并发场景推荐写法:ThreadLocal缓存SimpleDateFormat
import java.text.SimpleDateFormat;
import java.util.concurrent.ThreadLocalRandom;
public class SafeSimpleDateFormat {
// ThreadLocal保证每个线程有独立的SimpleDateFormat实例
private static final ThreadLocal<SimpleDateFormat> SDF_THREAD_LOCAL = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
);
public static String format(Date date) {
return SDF_THREAD_LOCAL.get().format(date);
}
public static Date parse(String dateStr) throws ParseException {
return SDF_THREAD_LOCAL.get().parse(dateStr);
}
}
三、新时代API:java.time包(Java 8+首选)
Java 8推出的java.time包(也称为JSR 310),彻底解决了旧API的所有缺陷,遵循「不可变性、线程安全、API直观」的设计原则,涵盖了日期、时间、时区、持续时间等所有时间处理场景,是目前Java时间处理的首选方案。
核心类关系:java.time包的核心类都基于Temporal接口,主要分为三类——日期(LocalDate)、时间(LocalTime)、日期时间(LocalDateTime),此外还有Instant(时间戳)、Duration(持续时间)、Period(周期)等辅助类。
3.1 核心类1:LocalDate(仅处理日期,无时间)
LocalDate专门表示「无时间的日期」(如2026-04-13),仅包含年、月、日三个字段,适合只关注日期的场景(如生日、订单日期)。它是不可变对象,线程安全,所有修改操作都会返回新的LocalDate实例,不会修改原对象。
3.1.1 核心用法(创建+字段操作+计算)
import java.time.LocalDate;
import java.time.Month;
import java.time.temporal.TemporalAdjusters;
public class LocalDateDemo {
public static void main(String[] args) {
// 1. 创建LocalDate对象(3种常用方式)
LocalDate today = LocalDate.now(); // 当前日期(系统时区)
LocalDate birthday = LocalDate.of(1990, Month.JUNE, 15); // 指定日期(Month枚举,避免踩坑)
LocalDate customDate = LocalDate.of(1990, 6, 15); // 指定日期(数字月份,1-12,直观)
LocalDate parseDate = LocalDate.parse("2026-04-13"); // 从ISO格式字符串解析(yyyy-MM-dd)
System.out.println("当前日期:" + today); // 输出:2026-04-13
System.out.println("指定日期:" + birthday); // 输出:1990-06-15
// 2. 获取日期字段(直观,无偏移)
int year = today.getYear(); // 2026
Month month = today.getMonth(); // APRIL(枚举)
int monthValue = today.getMonthValue(); // 4(1-12,无需修正)
int day = today.getDayOfMonth(); // 13
int dayOfWeek = today.getDayOfWeek().getValue(); // 6(1=周一,7=周日,符合日常习惯)
int daysInMonth = today.lengthOfMonth(); // 30(当月天数)
boolean isLeapYear = today.isLeapYear(); // false(是否闰年)
System.out.printf("年份:%d,月份:%d,日期:%d,星期:%d,当月天数:%d,是否闰年:%b%n",
year, monthValue, day, dayOfWeek, daysInMonth, isLeapYear);
// 3. 日期计算(返回新对象,原对象不变)
LocalDate tomorrow = today.plusDays(1); // 加1天
LocalDate nextMonth = today.plusMonths(1); // 加1个月
LocalDate lastYear = today.minusYears(1); // 减1年
LocalDate firstDayOfMonth = today.with(TemporalAdjusters.firstDayOfMonth()); // 当月第一天
LocalDate lastDayOfYear = today.with(TemporalAdjusters.lastDayOfYear()); // 当年最后一天
System.out.println("明天:" + tomorrow);
System.out.println("下月今天:" + nextMonth);
System.out.println("去年今天:" + lastYear);
// 4. 日期比较
boolean isBefore = today.isBefore(birthday); // false
boolean isAfter = today.isAfter(birthday); // true
boolean isEqual = today.isEqual(parseDate); // true
// 5. 计算两个日期的间隔(Period:年、月、日)
java.time.Period period = java.time.Period.between(birthday, today);
System.out.printf("两个日期间隔:%d年%d月%d天%n",
period.getYears(), period.getMonths(), period.getDays());
}
}
3.2 核心类2:LocalTime(仅处理时间,无日期)
LocalTime专门表示「无日期的时间」(如09:58:00),仅包含时、分、秒、纳秒四个字段,适合只关注时间的场景(如上班时间、闹钟时间),同样是不可变对象,线程安全。
3.2.1 核心用法
import java.time.LocalTime;
public class LocalTimeDemo {
public static void main(String[] args) {
// 1. 创建LocalTime对象(3种常用方式)
LocalTime now = LocalTime.now(); // 当前时间(系统时区)
LocalTime lunchTime = LocalTime.of(12, 30); // 指定时间(时:分)
LocalTime preciseTime = LocalTime.of(15, 45, 30); // 指定时间(时:分:秒)
LocalTime parseTime = LocalTime.parse("09:58:00"); // 从ISO格式字符串解析(HH:mm:ss)
System.out.println("当前时间:" + now); // 输出:09:58:00.123456789(包含纳秒)
System.out.println("指定时间:" + lunchTime); // 输出:12:30
// 2. 获取时间字段
int hour = now.getHour(); // 9(24小时制)
int minute = now.getMinute(); // 58
int second = now.getSecond(); // 0
int nano = now.getNano(); // 123456789(纳秒)
System.out.printf("时:%d,分:%d,秒:%d,纳秒:%d%n", hour, minute, second, nano);
// 3. 时间计算(返回新对象)
LocalTime nextHour = now.plusHours(1); // 加1小时
LocalTime minusMinute = now.minusMinutes(10); // 减10分钟
LocalTime truncatedTime = now.truncatedTo(java.time.temporal.ChronoUnit.MINUTES); // 截断到分钟(去掉秒和纳秒)
System.out.println("1小时后:" + nextHour);
System.out.println("10分钟前:" + minusMinute);
System.out.println("截断到分钟:" + truncatedTime);
// 4. 时间比较
boolean isBefore = now.isBefore(lunchTime); // true
System.out.println("当前时间在午餐时间之前:" + isBefore);
}
}
3.3 核心类3:LocalDateTime(日期+时间,最常用)
LocalDateTime是LocalDate和LocalTime的结合体,包含年、月、日、时、分、秒、纳秒,是最常用的时间类,适合需要同时处理日期和时间的场景(如用户注册时间、日志时间戳),不可变、线程安全。
3.3.1 核心用法(综合实战)
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对象(4种常用方式)
LocalDateTime now = LocalDateTime.now(); // 当前日期时间
LocalDateTime specificTime = LocalDateTime.of(2026, 4, 13, 9, 58, 0); // 指定日期时间
LocalDateTime fromDateAndTime = LocalDateTime.of(LocalDate.now(), LocalTime.now()); // 结合LocalDate和LocalTime
LocalDateTime parseTime = LocalDateTime.parse("2026-04-13T09:58:00"); // ISO格式解析(T分隔日期和时间)
System.out.println("当前日期时间:" + now); // 输出:2026-04-13T09:58:00.123456789
// 2. 字段操作(获取+修改)
int year = now.getYear();
int month = now.getMonthValue();
int day = now.getDayOfMonth();
int hour = now.getHour();
LocalDateTime modifiedTime = now.withYear(2025).withMonth(12).withDayOfMonth(31); // 修改字段(返回新对象)
System.out.printf("当前:%d年%d月%d日 %d时%n", year, month, day, hour);
System.out.println("修改后的时间:" + modifiedTime);
// 3. 时间计算(常用场景)
LocalDateTime nextWeek = now.plusWeeks(1); // 加1周
LocalDateTime lastMonth = now.minusMonths(1); // 减1个月
long daysBetween = ChronoUnit.DAYS.between(modifiedTime, now); // 计算两个时间的间隔(天数)
long hoursBetween = ChronoUnit.HOURS.between(parseTime, now); // 计算间隔(小时)
System.out.println("1周后:" + nextWeek);
System.out.println("1个月前:" + lastMonth);
System.out.printf("两个时间间隔:%d天,%d小时%n", daysBetween, hoursBetween);
// 4. 格式化(DateTimeFormatter,线程安全,替代SimpleDateFormat)
// 定义格式化模板
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String timeStr = now.format(formatter); // LocalDateTime → 字符串
System.out.println("格式化后的时间:" + timeStr); // 输出:2026-04-13 09:58:00
// 解析(字符串 → LocalDateTime)
String dateTimeStr = "2025-12-31 23:59:59";
LocalDateTime parsedDateTime = LocalDateTime.parse(dateTimeStr, formatter);
System.out.println("解析后的时间:" + parsedDateTime);
}
}
3.4 其他常用辅助类
除了上述三个核心类,java.time包还有几个常用的辅助类,用于处理时间戳、持续时间等场景,补充完善时间处理能力。
3.4.1 Instant:时间戳(对应Date)
Instant表示「UTC时间戳」(格林尼治标准时间),内部存储的是自1970-01-01T00:00:00Z以来的秒数+纳秒数,与Date类的核心作用一致,但精度更高(纳秒级),不可变、线程安全。
import java.time.Instant;
import java.util.Date;
public class InstantDemo {
public static void main(String[] args) {
// 1. 创建Instant对象
Instant now = Instant.now(); // 当前UTC时间戳
Instant specificInstant = Instant.ofEpochMilli(1712995080000L); // 根据毫秒时间戳创建
System.out.println("当前UTC时间戳:" + now); // 输出:2026-04-13T01:58:00.123456789Z(Z表示UTC时区)
// 2. 转换为Date对象(兼容旧API)
Date date = Date.from(now);
Instant instantFromDate = date.toInstant();
System.out.println("Instant转Date:" + date);
System.out.println("Date转Instant:" + instantFromDate);
// 3. 时间计算
Instant later = now.plusSeconds(60); // 加60秒
Instant earlier = now.minusMillis(1000); // 减1000毫秒
System.out.println("60秒后:" + later);
}
}
3.4.2 Duration与Period:时间间隔
Duration和Period都用于表示「时间间隔」,核心区别在于:
-
Duration:表示「时间间隔」(如2小时30分钟),基于秒和纳秒,适合计算两个时间(LocalTime、Instant)之间的间隔;
-
Period:表示「日期间隔」(如1年2个月3天),基于年、月、日,适合计算两个日期(LocalDate)之间的间隔。
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Period;
public class DurationPeriodDemo {
public static void main(String[] args) {
// 1. Duration:计算时间间隔
LocalTime time1 = LocalTime.of(9, 0, 0);
LocalTime time2 = LocalTime.of(11, 30, 0);
Duration duration = Duration.between(time1, time2);
System.out.println("时间间隔:" + duration.toHours() + "小时" + duration.toMinutes()%60 + "分钟"); // 2小时30分钟
// 2. Period:计算日期间隔
LocalDate date1 = LocalDate.of(2025, 1, 1);
LocalDate date2 = LocalDate.of(2026, 4, 13);
Period period = Period.between(date1, date2);
System.out.println("日期间隔:" + period.getYears() + "年" + period.getMonths() + "月" + period.getDays() + "天"); // 1年3个月12天
}
}
四、新旧API转换(实战必备)
开发中经常需要在新旧API之间转换(如遗留系统用Date,新项目用LocalDateTime),以下是常用的转换方法,直接复制可用。
4.1 Date ↔ LocalDateTime(最常用)
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
public class DateLocalDateTimeConvert {
public static void main(String[] args) {
// 1. Date → LocalDateTime(需指定时区,默认系统时区)
Date date = new Date();
LocalDateTime localDateTime = date.toInstant()
.atZone(ZoneId.systemDefault()) // 转换为系统时区的ZonedDateTime
.toLocalDateTime(); // 转换为LocalDateTime
System.out.println("Date转LocalDateTime:" + localDateTime);
// 2. LocalDateTime → Date(需指定时区)
LocalDateTime now = LocalDateTime.now();
Date convertDate = Date.from(now.atZone(ZoneId.systemDefault()).toInstant());
System.out.println("LocalDateTime转Date:" + convertDate);
}
}
4.2 Calendar ↔ LocalDateTime
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Calendar;
public class CalendarLocalDateTimeConvert {
public static void main(String[] args) {
// 1. Calendar → LocalDateTime
Calendar cal = Calendar.getInstance();
LocalDateTime localDateTime = cal.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
System.out.println("Calendar转LocalDateTime:" + localDateTime);
// 2. LocalDateTime → Calendar
LocalDateTime now = LocalDateTime.now();
Calendar convertCal = Calendar.getInstance();
convertCal.setTime(Date.from(now.atZone(ZoneId.systemDefault()).toInstant()));
System.out.println("LocalDateTime转Calendar:" + convertCal.getTime());
}
}
五、Java时间处理实战场景(开发高频)
结合实际开发场景,整理以下高频需求的实现代码,直接复用,覆盖格式化、时间计算、校验等核心场景。
5.1 场景1:时间格式化(统一工具类)
封装java.time的DateTimeFormatter,实现统一的时间格式化和解析,替代线程不安全的SimpleDateFormat。
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
/**
* 时间格式化工具类(Java 8+,线程安全)
*/
public class TimeFormatterUtil {
// 常用格式化模板(可根据需求扩展)
public static final DateTimeFormatter DATETIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
public static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss");
// 私有化构造方法,禁止实例化
private TimeFormatterUtil() {}
// 1. LocalDateTime格式化
public static String formatLocalDateTime(LocalDateTime localDateTime) {
if (localDateTime == null) {
return null;
}
return localDateTime.format(DATETIME_FORMAT);
}
// 2. 字符串解析为LocalDateTime
public static LocalDateTime parseLocalDateTime(String timeStr) {
if (timeStr == null || timeStr.isEmpty()) {
return null;
}
try {
return LocalDateTime.parse(timeStr, DATETIME_FORMAT);
} catch (DateTimeParseException e) {
e.printStackTrace();
return null; // 解析失败返回null,也可抛出异常
}
}
// 3. LocalDate格式化
public static String formatLocalDate(LocalDate localDate) {
if (localDate == null) {
return null;
}
return localDate.format(DATE_FORMAT);
}
// 4. LocalTime格式化
public static String formatLocalTime(LocalTime localTime) {
if (localTime == null) {
return null;
}
return localTime.format(TIME_FORMAT);
}
// 测试
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
System.out.println("格式化:" + formatLocalDateTime(now));
System.out.println("解析:" + parseLocalDateTime("2026-04-13 09:58:00"));
}
}
5.2 场景2:计算两个时间的间隔(如计算年龄、有效期)
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
import java.time.temporal.ChronoUnit;
/**
* 时间计算工具类
*/
public class TimeCalculateUtil {
// 1. 根据生日计算年龄
public static int calculateAge(LocalDate birthDate) {
if (birthDate == null) {
throw new IllegalArgumentException("生日不能为空");
}
LocalDate today = LocalDate.now();
// 若今年生日还没到,年龄减1
if (birthDate.getMonthValue() > today.getMonthValue()
|| (birthDate.getMonthValue() == today.getMonthValue()
&& birthDate.getDayOfMonth() > today.getDayOfMonth())) {
return Period.between(birthDate, today).getYears() - 1;
}
return Period.between(birthDate, today).getYears();
}
// 2. 计算两个时间的间隔(天数)
public static long calculateDaysBetween(LocalDateTime start, LocalDateTime end) {
if (start == null || end == null) {
throw new IllegalArgumentException("时间不能为空");
}
return ChronoUnit.DAYS.between(start, end);
}
// 3. 判断时间是否在有效期内(如订单是否超时)
public static boolean isWithinValidity(LocalDateTime startTime, LocalDateTime endTime, LocalDateTime currentTime) {
if (startTime == null || endTime == null || currentTime == null) {
return false;
}
return currentTime.isAfter(startTime) && currentTime.isBefore(endTime);
}
// 测试
public static void main(String[] args) {
LocalDate birthDate = LocalDate.of(1990, 6, 15);
System.out.println("年龄:" + calculateAge(birthDate));
LocalDateTime start = LocalDateTime.of(2026, 4, 1, 0, 0, 0);
LocalDateTime end = LocalDateTime.of(2026, 4, 15, 23, 59, 59);
System.out.println("间隔天数:" + calculateDaysBetween(start, end));
System.out.println("当前时间是否在有效期内:" + isWithinValidity(start, end, LocalDateTime.now()));
}
}
5.3 场景3:获取指定时间(如当月第一天、下周同一时间)
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.TemporalAdjusters;
import java.time.DayOfWeek;
public class SpecificTimeUtil {
// 1. 获取当月第一天(LocalDate)
public static LocalDate getFirstDayOfMonth() {
return LocalDate.now().with(TemporalAdjusters.firstDayOfMonth());
}
// 2. 获取当月最后一天(LocalDate)
public static LocalDate getLastDayOfMonth() {
return LocalDate.now().with(TemporalAdjusters.lastDayOfMonth());
}
// 3. 获取下周同一时间(LocalDateTime)
public static LocalDateTime getNextWeekSameTime() {
return LocalDateTime.now().plusWeeks(1);
}
// 4. 获取当月第一个周一
public static LocalDate getFirstMondayOfMonth() {
return LocalDate.now().with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY));
}
// 测试
public static void main(String[] args) {
System.out.println("当月第一天:" + getFirstDayOfMonth());
System.out.println("当月最后一天:" + getLastDayOfMonth());
System.out.println("下周同一时间:" + getNextWeekSameTime());
System.out.println("当月第一个周一:" + getFirstMondayOfMonth());
}
}
六、Java时间处理避坑指南(开发+面试重点)
时间处理看似简单,但容易出现各种坑点,尤其是旧API的使用和时区问题,以下是高频坑点及解决方案,也是面试中常考的知识点。
6.1 坑点1:旧API的线程安全问题(Date/Calendar/SimpleDateFormat)
Date、Calendar是可变对象,多线程共享会导致线程安全问题;SimpleDateFormat非线程安全,并发调用会出现格式错乱。
解决方案:
-
新项目直接使用java.time包新API(不可变、线程安全);
-
遗留系统:SimpleDateFormat使用ThreadLocal缓存,Date/Calendar避免多线程共享,每次使用时新建实例。
6.2 坑点2:月份取值范围混淆(旧API)
Calendar的月份是0-11(0代表1月,11代表12月),Date类的getMonth()方法(已废弃)也是0-11,新手容易忘记+1,导致月份错误。
解决方案:
-
使用Calendar时,获取月份后必须+1(如cal.get(Calendar.MONTH) + 1);
-
优先使用java.time包的LocalDate,月份是1-12,直观无偏移。
6.3 坑点3:时区混淆(最隐蔽)
旧API(Date/Calendar)默认依赖系统时区,Instant表示UTC时区,若不注意时区转换,会导致时间偏差(如UTC时间比北京时间晚8小时)。
解决方案:
-
转换API时(如Date ↔ LocalDateTime),必须指定时区(推荐使用ZoneId.systemDefault()获取系统时区,或明确指定时区如ZoneId.of("Asia/Shanghai"));
-
跨系统传输时间时,建议使用UTC时间戳(Instant),避免时区差异导致的错误。
6.4 坑点4:格式化模板错误(大小写混淆)
格式化模板中,大小写代表不同含义,写错会导致格式化/解析失败,常见错误:
-
yyyy(4位年份) vs YYYY(周基年,可能导致跨年错误);
-
MM(2位月份) vs mm(2位分钟);
-
HH(24小时制) vs hh(12小时制);
-
dd(2位日期) vs DD(一年中的第几天)。
解决方案:牢记常用模板的大小写,避免混用,推荐封装统一的格式化工具类。
6.5 坑点5:不可变对象的使用误区(新API)
java.time包的所有类都是不可变对象,调用plusXXX()、minusXXX()、withXXX()等方法时,不会修改原对象,而是返回新对象,新手容易误以为原对象被修改。
七、总结
Java时间处理的核心是「选择合适的API」:Java 8+新项目优先使用java.time包(LocalDate、LocalTime、LocalDateTime),其不可变性、线程安全、直观的API设计,能彻底解决旧API的痛点;维护遗留系统时,需掌握Date、Calendar、SimpleDateFormat的用法,同时注意规避线程安全、月份偏移等坑点。
本文涵盖了Java时间处理的所有核心知识点,从API演变、核心类用法,到实战工具类、避坑指南,层层递进,代码可直接复用。时间处理的关键在于「细节」——时区、格式化模板、不可变对象的使用,只要掌握这些细节,就能从容应对所有Java时间处理场景。
最后提醒:开发中建议统一时间处理规范,优先使用新API,封装通用工具类,避免重复编码,同时注意时区一致性,避免因时间偏差导致的业务问题。 Java时间处理详解:从Date、Calendar到java.time新API 时间处理是Java开发中最基础、最高频的场景之一——用户注册时间记录、订单超