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

✅ 真正的“无时间”:这个对象压根没有 hourminute 字段,从根本上杜绝了误操作。

我们也可以写个测试验证其边界行为:

@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));
}

✅ 说明:LocalDateatStartOfDay() 开始计算时间,行为一致且语义清晰。

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


原始标题:Get Date Without Time in Java | Baeldung