1. 简介
本文将介绍如何在 Java 中获取一个只包含日期部分、不包含时间信息的日期对象。
我们将分别讨论 Java 8 之前和之后的实现方式。因为从 Java 8 开始引入了全新的时间 API(java.time),处理日期的方式发生了显著变化,代码更清晰、语义更明确。
如果你还在用 java.util.Date
那一套,看完这篇文章可能会意识到:有些坑,其实是可以绕开的。
2. Java 8 之前的做法
在 Java 8 之前,标准库中并没有专门表示“只有日期”的类型。⚠️ java.util.Date
本质上是一个时间戳(毫秒值),它总是包含时间信息,哪怕你设为 0。
所以“获取不含时间的 Date”其实是个伪命题 —— 你只能把时间归零,但无法真正“剥离”它。
常见的 workaround 有以下两种。
2.1 使用 Calendar 归零时间
最常见的方式是借助 Calendar
类,手动将时、分、秒、毫秒全部设为 0。
✅ 示例代码:
public static Date getDateWithoutTimeUsingCalendar() {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar.getTime();
}
调用后返回的结果类似:
Sat Jun 23 00:00:00 CEST 2018
虽然时间显示为 00:00:00
,但这个 Date
对象依然携带了时间字段。你无法阻止别人调用 setTime(... + 3600000)
把它改成上午 1 点。
为了验证时间确实被“归零”,我们可以写个测试:
@Test
public void whenGettingDateWithoutTimeUsingCalendar_thenReturnDateWithoutTime() {
Date dateWithoutTime = DateWithoutTime.getDateWithoutTimeUsingCalendar();
Calendar calendar = Calendar.getInstance();
calendar.setTime(dateWithoutTime);
int day = calendar.get(Calendar.DAY_OF_MONTH);
calendar.setTimeInMillis(dateWithoutTime.getTime() + MILLISECONDS_PER_DAY - 1);
assertEquals(day, calendar.get(Calendar.DAY_OF_MONTH));
calendar.setTimeInMillis(dateWithoutTime.getTime() + MILLISECONDS_PER_DAY);
assertNotEquals(day, calendar.get(Calendar.DAY_OF_MONTH));
}
✅ 解读:
- 加上一整天减 1 毫秒,仍在同一天
- 加上整整一天,进入下一天
- 说明原始时间从 00:00:00 开始,验证成功
2.2 使用日期格式化“截断”时间
另一种思路是:先把 Date
格式化成不带时间的字符串(如 "23/06/2018"
),再解析回 Date
。
由于格式化时没有包含时间部分,解析出来的 Date
默认从当天 00:00:00 开始。
✅ 示例代码:
public static Date getDateWithoutTimeUsingFormat()
throws ParseException {
SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy");
return formatter.parse(formatter.format(new Date()));
}
结果同样是:
Sat Jun 23 00:00:00 CEST 2018
⚠️ 缺点:
- 多次格式化/解析,性能较差
- 依赖默认时区
- 本质仍是归零,不是真正“无时间”
3. Java 8 及之后的正确姿势
Java 8 引入了 java.time
包,带来了清晰的类型划分:
类型 | 含义 |
---|---|
LocalDate |
只有日期,无时间,无时区 ✅ |
LocalDateTime |
有日期+时间,无时区 |
ZonedDateTime |
有日期+时间+时区 |
我们要的“不含时间的日期”,直接用 LocalDate
就行了,简单粗暴。
3.1 使用 LocalDate
public static LocalDate getLocalDate() {
return LocalDate.now();
}
输出:
2018-06-23
✅ 真正的“无时间”:这个对象压根没有 hour
、minute
字段,从根本上杜绝了误操作。
我们也可以写个测试验证其边界行为:
@Test
public void whenGettingLocalDate_thenReturnDateWithoutTime() {
LocalDate localDate = DateWithoutTime.getLocalDate();
long millisLocalDate = localDate
.atStartOfDay()
.toInstant(OffsetDateTime.now().getOffset())
.toEpochMilli();
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(millisLocalDate + MILLISECONDS_PER_DAY - 1);
assertEquals(localDate.getDayOfMonth(), calendar.get(Calendar.DAY_OF_MONTH));
calendar.setTimeInMillis(millisLocalDate + MILLISECONDS_PER_DAY);
assertNotEquals(localDate.getDayOfMonth(), calendar.get(Calendar.DAY_OF_MONTH));
}
✅ 说明:LocalDate
从 atStartOfDay()
开始计算时间,行为一致且语义清晰。
3.2 与其他类型的互操作
实际开发中,可能需要与旧代码(Date
)交互。可以这样转换:
// LocalDate → Date
public static Date convertToDate(LocalDate date) {
return Date.from(date.atStartOfDay(ZoneId.systemDefault()).toInstant());
}
// Date → LocalDate
public static LocalDate convertToLocalDate(Date date) {
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
}
⚠️ 注意时区问题!转换时务必明确指定 ZoneId
,避免依赖默认时区导致线上踩坑。
4. 总结
方式 | 是否推荐 | 说明 |
---|---|---|
Calendar 归零 |
❌ 不推荐 | 老项目兼容可用,语义不清 |
SimpleDateFormat 格式化 |
❌ 不推荐 | 性能差,易出错 |
LocalDate (Java 8+) |
✅ 强烈推荐 | 类型安全,语义清晰,代码简洁 |
结论很明确:
如果你的项目已经使用 Java 8 或更高版本,不要再用 Date + Calendar
那套老方法处理“纯日期”场景。直接上 LocalDate
,从类型层面就杜绝了时间干扰,代码可读性和健壮性都提升一个档次。
所有示例代码已托管至 GitHub:https://github.com/baeldung/core-java-modules/tree/master/core-java-date-operations-1