LocalDate、LocalTime 和 LocalDateTime:它是什么,日期处理

作者: IT Sectr 发布日期: 2026-07-13 阅读时间: 12 分钟

LocalDate、LocalTime 和 LocalDateTime — java.time 包的主要类,提供不绑定时区的日期和时间处理。根据 Oracle 文档(Java 17, 2024),这些类型被设计为 immutable 和 thread-safe,这使它们对于多线程应用程序是安全的。它们在 Android 上通过 desugaring 从 API 26 开始可用,而对于更早版本则通过 ThreeTenABP 库。

主要内容

  • LocalDate — 用于表示日期(年、月、日)的 immutable 类,不包含时间和时区。
  • LocalTime — 用于表示时间(小时、分钟、秒、纳秒)的 immutable 类,不包含日期和时区。
  • LocalDateTime — LocalDate 和 LocalTime 的组合,存储日期和时间但不绑定时区。
  • 所有三个类都支持 算术运算 — 通过 plus 和 minus 方法添加和减少天、月、小时。
  • 在 Android 上,这些类型可通过 desugaring (API 26+) 或 ThreeTenABP 库 (API < 26) 获取。

什么是 LocalDate、LocalTime 和 LocalDateTime?

LocalDate — 以年-月-日格式表示日期的类,不包含时间和时区信息。用于存储生日、事件日期或过期日期等数据。

LocalDate 存储的年份范围从 -999999999 到 +999999999,月份从 1 到 12,并考虑 闰年。该类完全不可变(immutable)— 每个操作都返回一个新对象。

LocalTime 表示一天中的时间:小时、分钟、秒和纳秒。最大精度 — 达到纳秒。LocalTime 不包含日期和时区信息,这使它适合存储商店开门时间或进程持续时间。

LocalDateTime 将 LocalDate 和 LocalTime 组合到一个对象中。这是最常用的类型,当需要存储既有日期又有时间、但不需要绑定时区时。例如,以本地格式存储音乐会的日期和时间。

根据 Oracle Java Documentation (2024),所有三个类都是基于 Joda-Time 库的思想设计的,但具有更改进的架构和与标准库的完全集成。

java.time 包是如何结构的?

java.time 包在 Java 8 中出现,取代了过时的 Date、Calendar 和 SimpleDateFormat 类。它的架构基于不可变对象和 fluent 接口的原则。

关键特征 — 所有主要类都是 value-based。这意味着它们的实例按值而非引用进行比较,且不能被继承。比较两个对象时使用 equals 方法,而非 == 运算符。

该包分为几个类别。无时区类型 — LocalDate、LocalTime、LocalDateTime — 用于本地日期和时间。带时区类型 — ZonedDateTime、OffsetDateTime、OffsetTime — 添加偏移或时区信息。即时类型 — Instant — 表示 UTC 中时间线上的点。

这种划分解决了旧 API 的典型问题:程序员从来不知道 Date 对象是否包含时区信息。在 java.time 中,每个类型都明确声明其语义。

LocalDate:日期处理

LocalDate 类提供了许多方法用于创建、读取和修改日期。当前日期可通过静态方法 now() 获取。特定日期 — 通过方法 of(int year, int month, int dayOfMonth)

读取日期组件使用 getter:getYear()、getMonthValue()、getDayOfMonth()、getDayOfWeek()、getDayOfYear()。 getMonth() 方法返回 Month 枚举,而 getDayOfWeek() 返回 DayOfWeek 枚举。

LocalDate 支持日期检查。isBefore()、isAfter() 和 isEqual() 方法可以比较日期。isLeapYear() 方法检查年份是否为闰年。lengthOfMonth() 方法返回月份的天数,lengthOfYear() 返回年份的天数。

修改时使用 withYear()、withMonth()、withDayOfMonth() 方法,它们返回带有修改组件的新对象。plusDays()、minusMonths() 等方法执行日期算术。

LocalTime:时间处理

LocalTime 表示一天中的时间,精度达到纳秒。标准格式 — ISO-8601 (HH:mm:ss.nnnnnnnnn)。最小值 — 00:00,最大值 — 23:59:59.999999999。

可通过 now() 获取当前时间,或通过 of(int hour, int minute)、of(int hour, int minute, int second) 或 of(int hour, int minute, int second, int nanoOfSecond) 创建 LocalTime 对象。parse(CharSequence text) 方法解析 ISO-8601 格式的字符串。

Getter 包括 getHour()、getMinute()、getSecond()、getNano()。toSecondOfDay() 方法返回从天开始的秒数,toNanoOfDay() 返回纳秒数。这对于计算一天内的持续时间很方便。

LocalTime 支持与 LocalDate 相同的比较和修改操作:plusHours()、minusMinutes()、withHour()、withMinute()。isBefore() 和 isAfter() 方法考虑了时间在一天内的循环性。

LocalDateTime:日期和时间的组合

LocalDateTime 将 LocalDate 和 LocalTime 的功能组合到一个类中。存储日期和时间,但不包含时区。这是最灵活的本地类型,但在分布式系统中使用时需要谨慎。

可通过静态方法 now()、of(LocalDate date, LocalTime time)、of(int year, Month month, int dayOfMonth, int hour, int minute) 及其重载创建 LocalDateTime。也可通过 atTime() 方法组合 LocalDate 和 LocalTime。

LocalDateTime 通过相应的 getter 提供对所有日期和时间字段的访问:toLocalDate() 和 toLocalTime() 返回单独的组件。truncatedTo(TemporalUnit unit) 方法可以将时间四舍五入到指定精度 — 例如,到分钟。

转换到时区使用 atZone(ZoneId zone) 方法,该方法返回 ZonedDateTime。这是向 LocalDateTime 添加时区的唯一方法。

如何创建日期和时间对象?

所有三个类都使用通过静态工厂方法的统一创建模式。类的构造器声明为 private — 无法通过 new 直接创建对象。

主要创建方式:

  • now() — 从系统时钟获取当前日期/时间
  • of(...) — 从组件(年、月、日等)创建
  • parse(String) — 从 ISO-8601 格式的字符串解析
  • from(TemporalAccessor) — 从其他时间对象转换

of 方法有多个重载。对于 LocalDate,需要年、月和日。对于 LocalTime — 小时和分钟(可选秒和纳秒)。对于 LocalDateTime — 年、月、日、小时、分钟。月份可以传递为 int (1-12) 或 Month 枚举。

kotlin
val today = LocalDate.now()
val specificDate = LocalDate.of(2026, Month.JULY, 21)
val parsedDate = LocalDate.parse("2026-07-21")

val currentTime = LocalTime.now()
val lunchTime = LocalTime.of(13, 30, 0)
val parsedTime = LocalTime.parse("13:30:00")

val now = LocalDateTime.now()
val meeting = LocalDateTime.of(2026, 7, 21, 15, 0)

类型之间的转换

java.time 类被设计为可互相方便转换。LocalDate 可通过 atTime(LocalTime) 或 atStartOfDay() 方法转换为 LocalDateTime。LocalTime — 通过 atDate(LocalDate)。

LocalDateTime 可通过 toLocalDate() 转换回 LocalDate,通过 toLocalTime() 转换回 LocalTime。转换为 ZonedDateTime 使用 atZone(ZoneId) 方法。

转换为 java.util.Date(与旧代码兼容)需要通过 Instant 和时区的中间步骤。根据 Baeldung (2024),该操作通过 Date.from(instant) 执行。

kotlin
val date = LocalDate.of(2026, 7, 21)
val dateTime = date.atTime(LocalTime.of(10, 30))

val time = LocalTime.of(14, 0)
val dateTimeFromTime = time.atDate(date)

val extractedDate = dateTime.toLocalDate()
val extractedTime = dateTime.toLocalTime()

val zoned = dateTime.atZone(ZoneId.of("Europe/Moscow"))

格式化和解析

格式化和解析使用 DateTimeFormatter 类。它通过常量(ISO_LOCAL_DATE、ISO_LOCAL_TIME、ISO_LOCAL_DATE_TIME)提供预定义格式,并可通过模式创建自定义格式。

格式化模式使用符号:yyyy — 年,MM — 月(两位),dd — 日,HH — 小时 (0-23),mm — 分钟,ss — 秒。format() 方法在日期时间对象上或通过 DateTimeFormatter 调用。

DateTimeFormatter 还通过静态方法 ofLocalizedDate(FormatStyle)、ofLocalizedTime(FormatStyle) 和 ofLocalizedDateTime(FormatStyle) 支持本地化。可用的样式包括 SHORT、MEDIUM、LONG 和 FULL。

kotlin
val formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
val formatted = LocalDateTime.now().format(formatter)

val parsed = LocalDate.parse(
    "21.07.2026",
    DateTimeFormatter.ofPattern("dd.MM.yyyy")
)

日期时间对象的比较

所有三个类都实现了 Comparable 接口,这使得可以进行自然比较。compareTo() 方法根据顺序返回负数、零或正数。isBefore()、isAfter() 和 isEqual() 方法返回 boolean。

对于 LocalDate,比较按时序进行 — 更早的日期更小。对于 LocalTime — 按照一天中的时间。对于 LocalDateTime — 先按日期,再按时间。所有比较都正确考虑闰年和每月的天数。

与旧 API 的重要区别:equals() 对于 LocalDate、LocalTime 和 LocalDateTime 比较的是值而非引用。这意味着具有相同字段的两个对象将相等,即便它们是不同的实例。

kotlin
val d1 = LocalDate.of(2026, 7, 21)
val d2 = LocalDate.of(2026, 12, 25)

if (d1.isBefore(d2)) {
    Log.d("日期", "d1 在 d2 之前")
}

val sortedDates = listOf(d2, d1).sorted()

日期和时间的算术

所有三个类都通过 plusminus 方法支持算术操作。LocalDate 提供 plusDays()、plusWeeks()、plusMonths()、plusYears() 及相应的 minus 方法。LocalTime 支持 plusHours()、plusMinutes()、plusSeconds()、plusNanos()。

LocalDateTime 继承了两种类型的所有算术操作。LocalDate 的特点:添加月份时,结果会正确处理不同月份的长度。例如,1月 31 日 + 1 个月 = 2月 28 日(闰年为 29 日)。

对于更复杂的操作,存在 Period(用于日期)和 Duration(用于时间)类。plus(TemporalAmount) 和 minus(TemporalAmount) 方法接受这些对象。

kotlin
val today = LocalDate.now()
val nextWeek = today.plusDays(7)
val nextMonth = today.plusMonths(1)
val lastYear = today.minusYears(1)

val now = LocalTime.now()
val inTwoHours = now.plusHours(2)
val halfHourAgo = now.minusMinutes(30)

Kotlin 代码示例

考虑一个实际示例:用于记录工作班次的应用程序。需要计算班次的持续时间并确定它是否落在夜间时间。我们使用 LocalTime 处理开始和结束时间,LocalDate 处理日期,LocalDateTime 处理超过午夜的班次计算。

kotlin
data class Shift(
    val startTime: LocalTime,
    val endTime: LocalTime,
    val date: LocalDate
) {
    fun isOvernight(): Boolean = endTime.isBefore(startTime)

    fun durationInMinutes(): Long {
        val start = LocalDateTime.of(date, startTime)
        val end = LocalDateTime.of(
            if (isOvernight()) date.plusDays(1) else date,
            endTime
        )
        return Duration.between(start, end).toMinutes()
    }
}

第二个示例 — 计算用户年龄。我们使用 LocalDate 处理生日,并与当前日期进行比较,考虑出生的月和日。

kotlin
fun calculateAge(birthDate: LocalDate): Int {
    val today = LocalDate.now()
    val period = Period.between(birthDate, today)
    return period.years
}

第三个示例 — 处理通知。LocalDateTime 用于计划提醒。我们检查计划的时间是否已到。

kotlin
data class Reminder(
    val id: Long,
    val scheduledAt: LocalDateTime
) {
    fun isDue(): Boolean =
        LocalDateTime.now().isAfter(scheduledAt)
}

Android 中的支持:API 级别和 desugaring

对于 java.time 的内置支持在 Android 上从 API 26 (Android 8.0 Oreo) 开始出现。对于运行更早版本 Android 的设备,需要使用 desugaring — 一种在早期版本中添加新 Java API 支持的机制。

Android Gradle Plugin 中的 Desugaring 通过 build.gradle 中的 compileOptions 配置。只需设置 isCoreLibraryDesugaringEnabled = true 并添加 desugar_jdk_libs 库。之后,java.time 将可用于从 14 开始的所有 API 级别。

对于无法使用 desugaring 的项目(例如,使用低于 4.0 的 AGP 的迁移项目),存在 ThreeTenABP 库 — java.time 的向后移植。它提供相同的类(LocalDate、LocalTime、LocalDateTime),但位于 org.threeten.bp 包中。

groovy
@Suppress("UnstableApiUsage")
android {
    compileOptions {
        isCoreLibraryDesugaringEnabled = true
    }
}

dependencies {
    "coreLibraryDesugaring"("com.android.tools:desugar_jdk_libs:2.1.4")
}

常见错误及如何避免

第一个常见错误 — 在分布式系统中使用 LocalDateTime 时未考虑时区。如果服务器在 Europe/Moscow,客户端在 Asia/Tokyo,LocalDateTime 将被不同解释。解决方案:对于全局数据使用 InstantZonedDateTime

第二个错误 — 字符串解析错误。默认情况下,LocalDate.parse() 期望 ISO-8601 (yyyy-MM-dd) 格式。如果字符串是其他格式,需要显式传递 DateTimeFormatter。还应处理 DateTimeParseException,以避免应用程序因无效输入而崩溃。

第三个错误 — 忽略空指针安全。LocalDate、LocalTime 和 LocalDateTime 是可以为 null 的对象。在 Kotlin 中,建议使用带明确检查或 Elvis 运算符的 nullable 类型。在 Java 中 — 在调用方法前检查是否为 null。

第四个错误 — 混淆 LocalDateTime 和 ZonedDateTime。LocalDateTime 不包含任何时区信息。如果需要传递绝对时间瞬间 — 使用 时区类型。如果本地时间就够了 — 使用本地类型。

常见问题

Java 中 LocalDate 和 Date 之间有什么区别?

Date 存储从 1970-01-01 UTC 开始的毫秒数,而 LocalDate 存储年、月和日,不绑定时区。Date 是可变的且不是线程安全的,LocalDate 是不可变的且线程安全的。自 Java 8 以来 Date 已过时。

可以在数据库中使用 LocalDateTime 吗?

可以,LocalDateTime 能很好地映射到 SQL 类型 TIMESTAMP WITHOUT TIME ZONE。JPA 和 Room 通过 TypeConverter 支持它。对于 TIMESTAMP WITH TIME ZONE,使用 ZonedDateTime 或 OffsetDateTime。

如何获取两个日期之间的天数?

使用 ChronoUnit.DAYS.between(startDate, endDate)。该方法返回 long — 以天为单位的差值。更详细的计算使用 Period.between(),它返回带有年、月和日的 Period。

如果需要保持时间精度到毫秒,该怎么办?

LocalTime 支持精度到纳秒(9 位小数)。如果需要精度到毫秒,请在保存前使用 truncateTo(ChronoUnit.MILLIS)。这可以防止序列化时的四舍五入问题。

为什么 LocalDate.now() 在不同设备上返回不同的日期?

now() 方法使用设备的系统时钟和默认时区。如果设备所处时区不同,日期可能不同。要获取统一的时间戳,请使用 Instant.now(),它始终返回 UTC 时间。

总结

  • LocalDate — 用于日期(无时间和时区)的 immutable 类。用于存储生日、截止日期、事件日期。
  • LocalTime — 用于一天中时间(精确到纳秒)的 immutable 类。适合存储开门时间、进程持续时间。
  • LocalDateTime — 日期和时间的组合,不绑定时区。最灵活的本地类型,但不适合分布式系统。
  • 所有三个类都通过基于 DateTimeFormatter 的统一 API 支持算术、比较、格式化和解析。
  • 在 Android 上,java.time 可通过从 API 26 开始的内置支持或通过 desugaring 获取(旧版本)。
  • 对于全局时间戳和带时区的数据,使用 ZonedDateTimeInstant,而非本地类型。
  • 解析字符串时,始终为非标准格式提供 DateTimeFormatter,并处理 DateTimeParseException。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读