LocalDate、LocalTime 和 LocalDateTime — java.time 包的主要类,提供不绑定时区的日期和时间处理。根据 Oracle 文档(Java 17, 2024),这些类型被设计为 immutable 和 thread-safe,这使它们对于多线程应用程序是安全的。它们在 Android 上通过 desugaring 从 API 26 开始可用,而对于更早版本则通过 ThreeTenABP 库。
主要内容
LocalDate — 以年-月-日格式表示日期的类,不包含时间和时区信息。用于存储生日、事件日期或过期日期等数据。
LocalDate 存储的年份范围从 -999999999 到 +999999999,月份从 1 到 12,并考虑 闰年。该类完全不可变(immutable)— 每个操作都返回一个新对象。
LocalTime 表示一天中的时间:小时、分钟、秒和纳秒。最大精度 — 达到纳秒。LocalTime 不包含日期和时区信息,这使它适合存储商店开门时间或进程持续时间。
LocalDateTime 将 LocalDate 和 LocalTime 组合到一个对象中。这是最常用的类型,当需要存储既有日期又有时间、但不需要绑定时区时。例如,以本地格式存储音乐会的日期和时间。
根据 Oracle Java Documentation (2024),所有三个类都是基于 Joda-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 类提供了许多方法用于创建、读取和修改日期。当前日期可通过静态方法 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 表示一天中的时间,精度达到纳秒。标准格式 — 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 将 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 直接创建对象。
主要创建方式:
of 方法有多个重载。对于 LocalDate,需要年、月和日。对于 LocalTime — 小时和分钟(可选秒和纳秒)。对于 LocalDateTime — 年、月、日、小时、分钟。月份可以传递为 int (1-12) 或 Month 枚举。
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) 执行。
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。
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 比较的是值而非引用。这意味着具有相同字段的两个对象将相等,即便它们是不同的实例。
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()
所有三个类都通过 plus 和 minus 方法支持算术操作。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) 方法接受这些对象。
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)
考虑一个实际示例:用于记录工作班次的应用程序。需要计算班次的持续时间并确定它是否落在夜间时间。我们使用 LocalTime 处理开始和结束时间,LocalDate 处理日期,LocalDateTime 处理超过午夜的班次计算。
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 处理生日,并与当前日期进行比较,考虑出生的月和日。
fun calculateAge(birthDate: LocalDate): Int {
val today = LocalDate.now()
val period = Period.between(birthDate, today)
return period.years
}
第三个示例 — 处理通知。LocalDateTime 用于计划提醒。我们检查计划的时间是否已到。
data class Reminder(
val id: Long,
val scheduledAt: LocalDateTime
) {
fun isDue(): Boolean =
LocalDateTime.now().isAfter(scheduledAt)
}
对于 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 包中。
@Suppress("UnstableApiUsage")
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}
dependencies {
"coreLibraryDesugaring"("com.android.tools:desugar_jdk_libs:2.1.4")
}
第一个常见错误 — 在分布式系统中使用 LocalDateTime 时未考虑时区。如果服务器在 Europe/Moscow,客户端在 Asia/Tokyo,LocalDateTime 将被不同解释。解决方案:对于全局数据使用 Instant 或 ZonedDateTime。
第二个错误 — 字符串解析错误。默认情况下,LocalDate.parse() 期望 ISO-8601 (yyyy-MM-dd) 格式。如果字符串是其他格式,需要显式传递 DateTimeFormatter。还应处理 DateTimeParseException,以避免应用程序因无效输入而崩溃。
第三个错误 — 忽略空指针安全。LocalDate、LocalTime 和 LocalDateTime 是可以为 null 的对象。在 Kotlin 中,建议使用带明确检查或 Elvis 运算符的 nullable 类型。在 Java 中 — 在调用方法前检查是否为 null。
第四个错误 — 混淆 LocalDateTime 和 ZonedDateTime。LocalDateTime 不包含任何时区信息。如果需要传递绝对时间瞬间 — 使用 时区类型。如果本地时间就够了 — 使用本地类型。
常见问题
Date 存储从 1970-01-01 UTC 开始的毫秒数,而 LocalDate 存储年、月和日,不绑定时区。Date 是可变的且不是线程安全的,LocalDate 是不可变的且线程安全的。自 Java 8 以来 Date 已过时。
可以,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)。这可以防止序列化时的四舍五入问题。
now() 方法使用设备的系统时钟和默认时区。如果设备所处时区不同,日期可能不同。要获取统一的时间戳,请使用 Instant.now(),它始终返回 UTC 时间。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。