为什么时间是个麻烦(以及Kotlin如何解决它)
\n\n如果你曾经编写过服务不同时区用户的应用程序,你一定知道这种痛苦。请求带着时间戳到来,数据库中存储的是UTC,纽约的用户抱怨通知在凌晨3点到达,而数据库中显示的是早上8点。熟悉吗?
\n\n时区处理是编程中一个看似简单却容易引发数十个bug的领域。但Kotlin继承了Java的最佳实践,为我们提供了强大的工具来应对混乱。在本文中,我们将探讨如何正确处理Kotlin中的时区,避免常见错误,并编写在夏令时切换时不会崩溃的代码。
\n\n简要概述:什么是时区?
\n\n在深入代码之前,让我们先统一术语。时区(time zone)不仅仅是相对于UTC的偏移量。它还包括夏令时/冬令时(DST)的切换规则。例如,Europe/Moscow不仅仅是+3小时,它还包含历史变化(俄罗斯自2014年起停止调整时钟)。
在Java/Kotlin世界中,有两个关键概念负责处理时区:
\n\n- \n
- ZoneId — 时区标识符(例如,"America/New_York")。包含所有DST历史记录。 \n
- ZoneOffset — 简单的偏移量(例如,+05:00)。不包含切换规则。 \n
区别至关重要:如果你使用ZoneOffset而不是ZoneId,你的代码将在时钟调整的那一天崩溃。请记住这一点。
Kotlin中的java.time基础类
\n\nKotlin完全支持java.time包(Java 8+)。如果你在Android上开发,别忘了添加desugaring或使用ThreeTenABP。对于服务器端Kotlin,没有问题。
Instant — 时间点
\n\nInstant是时间轴上的绝对时刻。它不依赖于时区。这正是你应该存储在数据库中的内容。
import java.time.Instant\n\nfun main() {\n val now = Instant.now()\n println(now) // 2025-03-15T10:30:00.123Z\n}\n\n注意末尾的字母Z — 这是祖鲁时间,即UTC。将所有内容存储在UTC中 — 这是黄金法则。
ZonedDateTime — 带时区的时间
\n\n当你需要向用户显示时间时,请使用ZonedDateTime。它存储日期、时间、偏移量和时区标识符。
import java.time.ZonedDateTime\nimport java.time.ZoneId\n\nfun main() {\n val moscowTime = ZonedDateTime.now(ZoneId.of("Europe/Moscow"))\n println(moscowTime) // 2025-03-15T13:30:00.123+03:00[Europe/Moscow]\n}\n\n看到方括号了吗?这正是ZoneId,它使系统能够正确处理DST。
OffsetDateTime — 带偏移量的时间
\n\n如果你不需要时区信息,只需要偏移量,请使用OffsetDateTime。它更轻量、更快。
import java.time.OffsetDateTime\nimport java.time.ZoneOffset\n\nfun main() {\n val offsetTime = OffsetDateTime.now(ZoneOffset.ofHours(5))\n println(offsetTime) // 2025-03-15T15:30:00.123+05:00\n}\n\n但请记住:如果你为纽约保存了OffsetDateTime,然后美国切换到夏令时,你的偏移量就会过时。
Kotlin中的时区转换
\n\n最常见的任务是从数据库(UTC)获取时间,并将其显示在用户的时区中。让我们看看如何正确操作。
\n\n从UTC到本地时区
\n\nimport java.time.Instant\nimport java.time.ZoneId\nimport java.time.ZonedDateTime\n\nfun convertUtcToLocal(utcInstant: Instant, targetZone: String): ZonedDateTime {\n return utcInstant.atZone(ZoneId.of(targetZone))\n}\n\nfun main() {\n val utcNow =