年前公司的电商项目上线一个优惠券活动,代码里有一段简单的逻辑:判断当前时间是否在活动有效期内。
我用的是Date,代码大概是这样的:
java
Date now = new Date();
Date start = activity.getStartDate();
Date end = activity.getEndDate();
if (now.after(start) && now.before(end)) {
// 活动进行中
}
测试环境跑得好好的,上线第二天,运营反馈:活动提前一小时结束了。
我查了半天,最后发现是时区问题。服务器在东八区,代码里某个地方把时间转成了UTC,比较的时候出了偏差。那天晚上我躺在床上翻来覆去睡不着,凌晨三点爬起来去公司改代码。
从那以后,我开始认真研究Java的时间API。从Date到Calendar,再到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。
解决:跨时区场景用ZonedDateTime或Instant。
六、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 区别总结
| 维度 | Duration | Period |
|---|---|---|
| 单位 | 秒、纳秒 | 年、月、日 |
| 适用对象 | Instant、LocalTime、LocalDateTime | LocalDate |
| 一天的概念 | 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 常用模式字母
| 字母 | 含义 | 示例 |
|---|---|---|
| y | 年 | yy: 26, yyyy: 2026 |
| M | 月 | M: 2, MM: 02, MMM: 2月, MMMM: 二月 |
| d | 日 | d: 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 |
| s | 秒 | s: 5, ss: 05 |
| a | AM/PM | 上午/下午 |
8.6 我踩过的坑
坑1:yyyy和YYYY不一样
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() |
| 格式化 | SimpleDateFormat | DateTimeFormatter |
| 加一天 | cal.add(Calendar.DATE, 1) | date.plusDays(1) |
| 时间差 | (d2.getTime() - d1.getTime())/1000 | Duration.between(t1, t2) |
| 线程安全 | ❌ 不安全 | ✅ 安全 |
| 可变性 | ❌ 可变 | ✅ 不可变 |
十、我的建议
用了这么多年Java时间API,最后总结几条原则:
- 新代码永远用java.time,别再碰Date和Calendar了
- LocalDateTime只用于本地时间,跨时区用Instant或ZonedDateTime
- 数据库交互用时间戳或字符串,不同ORM对java.time支持程度不同
- 对外API用ISO8601格式(
yyyy-MM-dd'T'HH:mm:ss'Z'),这是国际标准 - 格式化器定义为static final共享,它是线程安全的
最后说句实在话:时间处理是程序里最容易出bug的地方之一。别觉得自己聪明,老老实实按规范来,测试覆盖所有边界情况——闰年、跨月、跨年、夏令时、时区转换,每一个都可能让你凌晨爬起来改代码。
我经历过,不想你也经历。