小维的学习笔记和工具

Java时间API:从Date到java.time,我花了三年才搞明白

年前公司的电商项目上线一个优惠券活动,代码里有一段简单的逻辑:判断当前时间是否在活动有效期内。

我用的是Date,代码大概是这样的:

java

Date now = new Date();
Date start = activity.getStartDate();
Date end = activity.getEndDate();

if (now.after(start) && now.before(end)) {
    // 活动进行中
}

测试环境跑得好好的,上线第二天,运营反馈:活动提前一小时结束了

我查了半天,最后发现是时区问题。服务器在东八区,代码里某个地方把时间转成了UTC,比较的时候出了偏差。那天晚上我躺在床上翻来覆去睡不着,凌晨三点爬起来去公司改代码。

从那以后,我开始认真研究Java的时间API。从DateCalendar,再到java.time,踩了无数坑,也终于搞明白了它们到底该怎么用。

这篇笔记是我这些年的总结,希望能帮同样被时间搞晕的人省点时间。

一、老古董们:Date和Calendar的坑

1.1 Date:Java 1.0的遗产

java.util.Date是Java出生时就有的类,活了二十多年,现在还在用。

基本用法

java

// 创建一个Date对象(当前时间)
Date now = new Date();
System.out.println(now);  // Fri Feb 27 14:30:25 CST 2026

// 从时间戳创建
Date date = new Date(1740642625000L);  // 2026-02-27 14:30:25

// 比较
boolean after = date1.after(date2);
boolean before = date1.before(date2);
int compare = date1.compareTo(date2);

它有什么问题?

我这些年遇到的坑,随便列几个:

问题1:月份从0开始

java

Date date = new Date(2026, 2, 27);  // 你以为的是2026-02-27?
// 实际是2026-03-27,因为月份0=1月,2=3月

问题2:可变性

java

Date date = new Date();
date.setTime(123456L);  // 直接修改了内部状态
// 如果这个date被多个地方共享,容易出并发问题

问题3:时区混乱

java

Date date = new Date();
// 打印出来带CST,但Date本身不存储时区信息
// 全靠JVM默认时区撑着,换台服务器结果可能不一样

问题4:大部分方法已废弃

java

date.getYear();    // 已废弃
date.getMonth();   // 已废弃
date.getDate();    // 已废弃
date.getHours();   // 已废弃

我刚入行时,看到IDE里划掉的getMonth()方法,整个人都是懵的——废弃了那我用什么?

1.2 Calendar:补丁没打好

Java 1.1推出了Calendar,试图修复Date的问题。

java

Calendar cal = Calendar.getInstance();
cal.set(2026, Calendar.FEBRUARY, 27);  // 月份用常量,比0-11好点
cal.set(Calendar.HOUR_OF_DAY, 14);
cal.set(Calendar.MINUTE, 30);
cal.set(Calendar.SECOND, 25);

Date date = cal.getTime();  // Calendar转Date

但它也有自己的坑

坑1:还是可变对象

java

Calendar cal = Calendar.getInstance();
cal.set(Calendar.MONTH, 1);  // 直接改状态
// 方法参数传递时容易出问题

坑2:API设计臃肿

java

// 想加一天
cal.add(Calendar.DAY_OF_MONTH, 1);  // 还算直观

// 想设置到当天的开始
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);  // 四行代码

坑3:线程不安全
Calendar不是线程安全的,多个线程操作同一个实例,数据可能错乱。

坑4:返回类型暧昧

java

int month = cal.get(Calendar.MONTH);  // 返回int,不知道是几月
// 需要你自己记住月份从0开始

我用Calendar写过一段代码,加了两个月,结果因为闰年问题算错了日期。从那以后我对这套API彻底失去了信心。

二、救星来了:java.time(Java 8+)

Java 8推出的java.time包,基于Joda-Time,终于把时间API做对了。

2.1 核心理念:不可变、线程安全、链式调用

java

// 所有修改都返回新对象,原对象不变
LocalDate date = LocalDate.of(2026, 2, 27);
LocalDate nextWeek = date.plusWeeks(1);  // date还是2026-02-27

这是我最喜欢的设计——再也不怕传参被改了。

2.2 三大核心:日期、时间、时间戳

先上对比表:

含义精度示例
LocalDate日期(年月日)2026-02-27
LocalTime时间(时分秒纳秒)纳秒14:30:25.123
LocalDateTime日期+时间纳秒2026-02-27T14:30:25
Instant时间戳(从1970年开始)纳秒2026-02-27T06:30:25Z

三、LocalDate:只关心年月日

3.1 创建

java

// 当前日期
LocalDate today = LocalDate.now();  // 2026-02-27

// 指定日期
LocalDate date = LocalDate.of(2026, 2, 27);
LocalDate date2 = LocalDate.of(2026, Month.FEBRUARY, 27);  // 用枚举更清晰

// 从字符串解析
LocalDate parsed = LocalDate.parse("2026-02-27");  // 必须是yyyy-MM-dd格式

3.2 常用操作

java

LocalDate date = LocalDate.of(2026, 2, 27);

// 获取信息
int year = date.getYear();              // 2026
int month = date.getMonthValue();       // 2
Month monthEnum = date.getMonth();      // FEBRUARY
int day = date.getDayOfMonth();         // 27
int dayOfYear = date.getDayOfYear();    // 58
DayOfWeek dow = date.getDayOfWeek();    // FRIDAY

// 日期计算
LocalDate tomorrow = date.plusDays(1);      // 2026-02-28
LocalDate nextMonth = date.plusMonths(1);   // 2026-03-27
LocalDate lastWeek = date.minusWeeks(1);    // 2026-02-20

// 比较
boolean isBefore = date.isBefore(LocalDate.of(2026, 3, 1));
boolean isAfter = date.isAfter(LocalDate.of(2026, 2, 1));
boolean isEqual = date.isEqual(LocalDate.of(2026, 2, 27));

// 特殊日期
LocalDate firstDayOfMonth = date.withDayOfMonth(1);  // 2026-02-01
LocalDate lastDayOfMonth = date.withDayOfMonth(date.lengthOfMonth());  // 2026-02-28
LocalDate nextFriday = date.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));

3.3 我踩过的坑

:用LocalDate存生日没问题,但存“2026-02-29”这种非闰年日期会抛异常。

java

LocalDate.of(2025, 2, 29);  // 抛异常:Invalid date 'February 29' as '2025' is not a leap year

解决:业务逻辑里要先校验闰年,或者用MonthDay存生日。

四、LocalTime:只关心时分秒

4.1 创建

java

// 当前时间
LocalTime now = LocalTime.now();  // 14:30:25.123456789

// 指定时间
LocalTime time = LocalTime.of(14, 30);                // 14:30
LocalTime time2 = LocalTime.of(14, 30, 25);           // 14:30:25
LocalTime time3 = LocalTime.of(14, 30, 25, 123456789); // 带纳秒

// 解析
LocalTime parsed = LocalTime.parse("14:30:25");       // 默认格式 HH:mm:ss
LocalTime parsed2 = LocalTime.parse("14:30");          // HH:mm 也行

4.2 常用操作

java

LocalTime time = LocalTime.of(14, 30, 25);

// 获取
int hour = time.getHour();        // 14
int minute = time.getMinute();    // 30
int second = time.getSecond();    // 25
int nano = time.getNano();        // 0

// 计算
LocalTime later = time.plusHours(2);        // 16:30:25
LocalTime earlier = time.minusMinutes(15);  // 14:15:25

// 比较
boolean isBefore = time.isBefore(LocalTime.of(15, 0));  // true
boolean isAfter = time.isAfter(LocalTime.of(13, 0));    // true

// 时间范围
LocalTime start = LocalTime.MIN;  // 00:00
LocalTime end = LocalTime.MAX;    // 23:59:59.999999999
LocalTime midnight = LocalTime.MIDNIGHT;  // 00:00
LocalTime noon = LocalTime.NOON;          // 12:00

4.3 我踩过的坑

:用LocalTime存“24:00”会抛异常,因为它是23:59:59.999…的下一瞬间。

java

LocalTime.of(24, 0);  // 抛异常

解决:业务上如果有“次日凌晨”的概念,用LocalDateTime或者加一个日期字段。

五、LocalDateTime:日期+时间

5.1 创建

java

// 当前日期时间
LocalDateTime now = LocalDateTime.now();  // 2026-02-27T14:30:25.123

// 从日期和时间组合
LocalDate date = LocalDate.of(2026, 2, 27);
LocalTime time = LocalTime.of(14, 30, 25);
LocalDateTime dt = LocalDateTime.of(date, time);  // 2026-02-27T14:30:25

// 直接指定
LocalDateTime dt2 = LocalDateTime.of(2026, 2, 27, 14, 30, 25);

// 解析
LocalDateTime parsed = LocalDateTime.parse("2026-02-27T14:30:25");

5.2 常用操作

java

LocalDateTime dt = LocalDateTime.of(2026, 2, 27, 14, 30, 25);

// 拆分成日期和时间
LocalDate date = dt.toLocalDate();  // 2026-02-27
LocalTime time = dt.toLocalTime();  // 14:30:25

// 计算
LocalDateTime tomorrow = dt.plusDays(1);           // 2026-02-28T14:30:25
LocalDateTime nextHour = dt.plusHours(1);          // 2026-02-27T15:30:25

// 比较
boolean isBefore = dt.isBefore(LocalDateTime.of(2026, 3, 1, 0, 0));

// 修改某个字段
LocalDateTime firstSecond = dt.withSecond(0);      // 2026-02-27T14:30:00
LocalDateTime firstDay = dt.withDayOfMonth(1);     // 2026-02-01T14:30:25

5.3 我踩过的坑

LocalDateTime不包含时区信息,存储或传输时容易出问题。

比如我存了一个2026-02-27T14:30:00到数据库,服务器在东八区,客户端在美国,读出来还是这个时间,用户看到的就是凌晨2:30。

解决:跨时区场景用ZonedDateTimeInstant

六、Instant:时间戳

6.1 创建

java

// 当前时刻(UTC时间)
Instant now = Instant.now();  // 2026-02-27T06:30:25.123Z

// 从时间戳
Instant instant = Instant.ofEpochSecond(1740642625);  // 秒级
Instant instant2 = Instant.ofEpochMilli(1740642625000L);  // 毫秒级

// 从字符串
Instant parsed = Instant.parse("2026-02-27T06:30:25Z");

6.2 常用操作

java

Instant instant = Instant.now();

// 转时间戳
long seconds = instant.getEpochSecond();      // 秒
long millis = instant.toEpochMilli();         // 毫秒

// 计算
Instant later = instant.plusSeconds(3600);    // 一小时后
Instant earlier = instant.minusMillis(1000);  // 一秒前

// 比较
boolean isAfter = instant.isAfter(Instant.parse("2026-02-27T00:00:00Z"));

// 和LocalDateTime互转
LocalDateTime dt = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
Instant fromDt = dt.atZone(ZoneId.systemDefault()).toInstant();

6.3 什么时候用

适用场景

  • 记录日志时间戳
  • 跨时区的时间传递
  • 时间计算(如计算耗时)

java

Instant start = Instant.now();
// 执行某操作
Instant end = Instant.now();
Duration duration = Duration.between(start, end);
System.out.println("耗时:" + duration.toMillis() + "ms");

七、Duration和Period:时间差

7.1 Duration:时分秒纳秒级差

java

// 创建
Duration d1 = Duration.ofSeconds(30);
Duration d2 = Duration.ofMinutes(5);
Duration d3 = Duration.ofHours(2);
Duration d4 = Duration.ofDays(1);  // 注意:24小时

// 计算两个时间点
LocalTime t1 = LocalTime.of(10, 0);
LocalTime t2 = LocalTime.of(14, 30);
Duration between = Duration.between(t1, t2);  // PT4H30M(4小时30分钟)

Instant i1 = Instant.now();
Thread.sleep(1000);
Instant i2 = Instant.now();
Duration elapsed = Duration.between(i1, i2);  // 大约PT1S

// 获取值
long seconds = elapsed.getSeconds();  // 总秒数
int nano = elapsed.getNano();         // 纳秒部分
long minutes = elapsed.toMinutes();   // 转分钟

// 比较和运算
Duration d = Duration.ofMinutes(10);
boolean isLonger = d.compareTo(Duration.ofMinutes(5)) > 0;
Duration sum = d.plus(Duration.ofMinutes(20));
Duration neg = d.negated();  // 负数

注意Duration.ofDays(1)是24小时,不是“一天”。涉及到“一天”的概念(可能有时区变更),要用Period

7.2 Period:年月周日历差

java

// 创建
Period p1 = Period.ofDays(7);
Period p2 = Period.ofMonths(3);
Period p3 = Period.of(1, 2, 15);  // 1年2个月15天

// 计算两个日期
LocalDate d1 = LocalDate.of(2026, 2, 27);
LocalDate d2 = LocalDate.of(2027, 5, 10);
Period between = Period.between(d1, d2);  // P1Y2M13D(1年2个月13天)

// 获取值
int years = between.getYears();
int months = between.getMonths();
int days = between.getDays();

// 运算
LocalDate newDate = d1.plus(between);  // 加上这段时间

// 规范化(把多余的月进位成年?不,Period不会自动进位)
Period normalized = between.normalized();  // 如果月>12,会进位成年

实战例子:计算年龄

java

LocalDate birth = LocalDate.of(1995, 5, 15);
LocalDate today = LocalDate.now();
Period age = Period.between(birth, today);
System.out.println("年龄:" + age.getYears() + "岁" + age.getMonths() + "个月");

7.3 区别总结

维度DurationPeriod
单位秒、纳秒年、月、日
适用对象Instant、LocalTime、LocalDateTimeLocalDate
一天的概念24小时日历日(可能是23或25小时,如果有夏令时)
常用场景耗时计算、超时判断年龄计算、账单周期

八、DateTimeFormatter:格式化与解析

8.1 基本用法

java

// 预定义格式器
DateTimeFormatter isoDate = DateTimeFormatter.ISO_LOCAL_DATE;  // 2026-02-27
DateTimeFormatter isoDateTime = DateTimeFormatter.ISO_LOCAL_DATE_TIME;  // 2026-02-27T14:30:25

// 自定义格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy年MM月dd日 EEEE", Locale.CHINA);

8.2 格式化(对象转字符串)

java

LocalDateTime now = LocalDateTime.now();

// 方式1:用对象的format方法
String s1 = now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
String s2 = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));

// 方式2:用格式器的format方法
String s3 = DateTimeFormatter.ofPattern("yyyy/MM/dd").format(now);

// 中文格式
DateTimeFormatter chinese = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分");
String s4 = now.format(chinese);  // 2026年02月27日 14时30分

8.3 解析(字符串转对象)

java

// 解析日期
String dateStr = "2026-02-27";
LocalDate date = LocalDate.parse(dateStr);  // 默认格式 yyyy-MM-dd

// 解析时间
String timeStr = "14:30:25";
LocalTime time = LocalTime.parse(timeStr);  // 默认格式 HH:mm:ss

// 自定义格式
String dtStr = "2026/02/27 14:30";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm");
LocalDateTime dt = LocalDateTime.parse(dtStr, formatter);

// 解析时处理可选部分
DateTimeFormatter flexible = DateTimeFormatter.ofPattern("yyyy-MM-dd[ HH:mm:ss]");  // []表示可选
LocalDateTime dt1 = LocalDateTime.parse("2026-02-27", flexible);  // 没时间部分,默认为00:00
LocalDateTime dt2 = LocalDateTime.parse("2026-02-27 14:30:25", flexible);

8.4 线程安全

DateTimeFormatter是线程安全的,可以定义为static final共享使用,这是和SimpleDateFormat最大的区别。

java

public class DateUtils {
    // 这个可以安全地在多线程中使用
    public static final DateTimeFormatter YMD_HMS = 
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
}

8.5 常用模式字母

字母含义示例
yyy: 26, yyyy: 2026
MM: 2, MM: 02, MMM: 2月, MMMM: 二月
dd: 7, dd: 07
E星期E: 周五, EEEE: 星期五
H小时(0-23)H: 5, HH: 05
h小时(1-12)h: 5, hh: 05
m分钟m: 5, mm: 05
ss: 5, ss: 05
aAM/PM上午/下午

8.6 我踩过的坑

坑1yyyyYYYY不一样

java

// yyyy = 日历年,YYYY = 周年(用于星期计算的年)
// 跨年那一周容易出错,永远用yyyy

坑2:解析时格式必须严格匹配

java

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate.parse("2026-2-1", formatter);  // 抛异常,月份必须是两位02
// 解决:用DateTimeFormatterBuilder设置宽松解析

九、实战对比:什么时候用哪个?

9.1 选型决策树

text

需求:你要处理什么?
├── 只有日期(生日、排班) → LocalDate
├── 只有时间(闹钟、营业时间) → LocalTime
├── 日期+时间,且无时区问题(本地业务) → LocalDateTime
├── 跨时区(全球化系统) → ZonedDateTime 或 Instant
├── 计算两个时刻的差(耗时) → Duration
├── 计算两个日期的差(年龄、账单周期) → Period
└── 和用户交互(展示、输入) → DateTimeFormatter

9.2 代码对照

场景旧API新API
获取当前时间new Date()LocalDateTime.now()
格式化SimpleDateFormatDateTimeFormatter
加一天cal.add(Calendar.DATE, 1)date.plusDays(1)
时间差(d2.getTime() - d1.getTime())/1000Duration.between(t1, t2)
线程安全❌ 不安全✅ 安全
可变性❌ 可变✅ 不可变

十、我的建议

用了这么多年Java时间API,最后总结几条原则:

  1. 新代码永远用java.time,别再碰Date和Calendar了
  2. LocalDateTime只用于本地时间,跨时区用Instant或ZonedDateTime
  3. 数据库交互用时间戳或字符串,不同ORM对java.time支持程度不同
  4. 对外API用ISO8601格式yyyy-MM-dd'T'HH:mm:ss'Z'),这是国际标准
  5. 格式化器定义为static final共享,它是线程安全的

最后说句实在话:时间处理是程序里最容易出bug的地方之一。别觉得自己聪明,老老实实按规范来,测试覆盖所有边界情况——闰年、跨月、跨年、夏令时、时区转换,每一个都可能让你凌晨爬起来改代码。

我经历过,不想你也经历。

未经允许不得转载:小维的学习笔记和工具 » Java时间API:从Date到java.time,我花了三年才搞明白