20. 日期与时间:Java 8 Time API在Kotlin中的使用
说实话,日期时间处理是每个项目都绕不开的坎。我见过太多因为 SimpleDateFormat 线程安全问题导致的线上bug,也踩过 Date 和 Calendar 那些反人类设计的坑。Java 8 推出的 Time API 算是彻底拯救了我们这些搬砖的。今天咱们就聊聊,在 Kotlin 里怎么用好 LocalDate、LocalDateTime、Duration 这些家伙。
为什么我抛弃了旧的 Date 和 Calendar?
你想想看,java.util.Date 明明叫“日期”,却能同时表示日期和时间,月份从0开始,年份从1900算起……这设计简直反直觉。我在一个金融项目里就遇到过,因为月份少算了1,导致利息计算差了整整一个月。从那以后,我坚决只用 Java 8 Time API。
Kotlin 对这套 API 的支持非常友好,没有额外的学习成本。说白了,就是 Java 那套东西直接拿来用,但 Kotlin 的语法糖能让代码更简洁。
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。
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 存储,前端展示时再转本地时间。记住:数据库里存 Instant 或 Timestamp 更安全。
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_FORMATTER、DATETIME_FORMATTER,避免到处重复创建。另外,DateTimeFormatter 是线程安全的,可以放心地定义为 companion object 里的静态变量。
fun LocalDateTime.toDisplayString(): String = this.format(DISPLAY_FORMATTER),这样调用起来就像原生方法一样自然。
知识体系一览
下面这张图帮你理清 Java 8 Time API 在 Kotlin 中的核心脉络。我画的时候特意把常用类和它们的关系标出来了,你可以保存下来当速查表。
实战建议:Kotlin 中的最佳实践
说了这么多,最后分享几个我在项目中沉淀下来的习惯:
- 尽量用
LocalDate和LocalDateTime,除非你明确需要时区。它们简单、不易出错。 - 数据库交互时,用
Instant或Timestamp,避免时区转换的麻烦。我一般用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 管格式化。剩下的就是多写多练,慢慢就形成肌肉记忆了。