20. 日期与时间:Java 8 Time API在Kotlin中的使用

说实话,日期时间处理是每个项目都绕不开的坎。我见过太多因为 SimpleDateFormat 线程安全问题导致的线上bug,也踩过 DateCalendar 那些反人类设计的坑。Java 8 推出的 Time API 算是彻底拯救了我们这些搬砖的。今天咱们就聊聊,在 Kotlin 里怎么用好 LocalDateLocalDateTimeDuration 这些家伙。

为什么我抛弃了旧的 Date 和 Calendar?

你想想看,java.util.Date 明明叫“日期”,却能同时表示日期和时间,月份从0开始,年份从1900算起……这设计简直反直觉。我在一个金融项目里就遇到过,因为月份少算了1,导致利息计算差了整整一个月。从那以后,我坚决只用 Java 8 Time API。

Kotlin 对这套 API 的支持非常友好,没有额外的学习成本。说白了,就是 Java 那套东西直接拿来用,但 Kotlin 的语法糖能让代码更简洁。

核心思想: 用不可变对象、线程安全、方法命名清晰。再也不用担心 SimpleDateFormat 的线程问题了。

LocalDate:只关心年月日

如果你只需要处理日期(年、月、日),比如生日、节假日、账单日,那就用 LocalDate。它没有时间,也没有时区,干净利落。

// 创建今天的日期
val today = LocalDate.now()
println(today) // 2025-03-21

// 指定日期
val myBirthday = LocalDate.of(1990, Month.JUNE, 15)
// 或者用数字月份(注意:1月是1,不是0)
val anotherDay = LocalDate.of(2025, 3, 21)

// 解析字符串
val parsed = LocalDate.parse("2025-12-25")
println(parsed) // 2025-12-25

我个人习惯用 Month 枚举,可读性更好。不过要注意,LocalDate.parse() 默认格式是 yyyy-MM-dd,如果遇到其他格式,需要自己定义 DateTimeFormatter

小技巧: 在 Kotlin 里,你可以用 LocalDate.now().plusDays(7) 直接算一周后的日期,链式调用非常舒服。

LocalDateTime:日期+时间,最常用的组合

大部分业务场景都需要同时记录日期和时间,比如订单创建时间、日志时间戳。这时候 LocalDateTime 就派上用场了。

// 当前日期时间
val now = LocalDateTime.now()
println(now) // 2025-03-21T14:30:15.123

// 指定日期时间
val meeting = LocalDateTime.of(2025, 3, 21, 14, 30, 0)

// 字符串解析
val fromStr = LocalDateTime.parse("2025-03-21T14:30:00")
// 注意:默认格式是 ISO_LOCAL_DATE_TIME,中间用 T 分隔

嗯,这里要注意:LocalDateTime 不包含时区信息。如果你需要处理跨时区的场景(比如国际航班、全球会议),那就得用 ZonedDateTime 了。不过大部分内部系统,用 LocalDateTime 就够了。

我曾经踩过的坑:LocalDateTime 直接存到数据库里,结果因为服务器时区不同,读出来的时间差了8小时。后来我统一用 UTC 存储,前端展示时再转本地时间。记住:数据库里存 InstantTimestamp 更安全。

Duration:计算时间差,精确到纳秒

两个时间点之间隔了多久?用 Duration 就对了。它适合处理秒、毫秒、纳秒级别的时间差。比如计算接口响应时间、任务执行时长。

val start = LocalDateTime.now()
// 模拟一些操作
Thread.sleep(1500) // 睡1.5秒
val end = LocalDateTime.now()

val duration = Duration.between(start, end)
println(duration.seconds)   // 1 秒(注意:只返回秒部分)
println(duration.toMillis()) // 1500 毫秒
println(duration.toMinutes()) // 0 分钟(因为不足1分钟)

你可能会问:那 Period 呢?它俩有啥区别?Duration 处理的是时间(时分秒纳秒),Period 处理的是日期(年月日)。比如计算两个日期之间隔了多少天,用 Period 更合适。

val startDate = LocalDate.of(2025, 1, 1)
val endDate = LocalDate.of(2025, 3, 21)
val period = Period.between(startDate, endDate)
println(period.days)   // 20 天(注意:只返回“日”部分,不是总天数)
println(period.months) // 2 个月
println(period.years)  // 0 年
避坑指南: Period.between() 返回的是“年月日”分别的差值,不是总天数。如果你想要两个日期之间总共隔了多少天,用 ChronoUnit.DAYS.between(startDate, endDate)

格式化与解析:DateTimeFormatter 的正确姿势

实际项目中,我们经常需要把日期时间转成特定格式的字符串,或者从字符串解析回来。这时候 DateTimeFormatter 是唯一正确的选择。

val formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss")
val now = LocalDateTime.now()
val formatted = now.format(formatter)
println(formatted) // 2025年03月21日 14:30:15

// 解析
val parsed = LocalDateTime.parse("2025年03月21日 14:30:15", formatter)
println(parsed) // 2025-03-21T14:30:15

我个人习惯把常用的格式定义成常量,比如 DATE_FORMATTERDATETIME_FORMATTER,避免到处重复创建。另外,DateTimeFormatter 是线程安全的,可以放心地定义为 companion object 里的静态变量。

Kotlin 扩展函数: 我通常会写几个扩展函数,让代码更 Kotlin 风格。比如 fun LocalDateTime.toDisplayString(): String = this.format(DISPLAY_FORMATTER),这样调用起来就像原生方法一样自然。

知识体系一览

下面这张图帮你理清 Java 8 Time API 在 Kotlin 中的核心脉络。我画的时候特意把常用类和它们的关系标出来了,你可以保存下来当速查表。

Java 8 Time API 在 Kotlin 中的核心类 java.time 包 LocalDate 年月日,无时间无时区 LocalDateTime 日期+时间,无时区 ZonedDateTime 日期+时间+时区 Duration 时分秒纳秒差 Period 年月日差 DateTimeFormatter 格式化/解析,线程安全 ChronoUnit 时间单位枚举,计算差值 Kotlin 中直接使用,无额外学习成本

实战建议:Kotlin 中的最佳实践

说了这么多,最后分享几个我在项目中沉淀下来的习惯:

  • 尽量用 LocalDateLocalDateTime,除非你明确需要时区。它们简单、不易出错。
  • 数据库交互时,用 InstantTimestamp,避免时区转换的麻烦。我一般用 Instant.now() 记录时间戳,存到数据库的 TIMESTAMP 字段。
  • 写扩展函数,比如 fun LocalDate.toChineseString(): String,让业务代码更干净。Kotlin 的扩展函数特性就是为这种场景准备的。
  • 不要用 SimpleDateFormat,哪怕是在遗留代码里。用 DateTimeFormatter 替换它,线程安全,性能也更好。
再次提醒: LocalDateTime.parse() 默认格式是 yyyy-MM-ddTHH:mm:ss,如果字符串格式不匹配,会直接抛异常。记得用 DateTimeFormatter.ofPattern() 指定格式,或者用 try-catch 兜底。

好了,关于 Java 8 Time API 在 Kotlin 中的使用,今天就聊到这儿。这套 API 设计得确实漂亮,用起来顺手,坑也少。你只要记住:LocalDate 管日期,LocalDateTime 管日期+时间,Duration 管时间差,DateTimeFormatter 管格式化。剩下的就是多写多练,慢慢就形成肌肉记忆了。

公众号:蓝海资料掘金营,微信deep3321