ZonedDateTime — nó là gì, làm việc với múi giờ và thời gian

Tác giả: IT Sectr Đã đăng: 2026-07-13 Thời gian đọc: 10 phút

ZonedDateTime là một lớp bất biến (immutable) từ gói java.time lưu trữ ngày và giờ cùng với thông tin múi giờ (ZoneId). Khác với LocalDateTime, ZonedDateTime xác định một cách duy nhất một thời điểm trên dòng thời gian. Theo đặc tả của Oracle Java 17 (2024), lớp này xử lý chính xác các chuyển đổi thời gian mùa hè (DST) thông qua các quy tắc múi giờ từ Cơ sở dữ liệu múi giờ IANA Time Zone Database.

Các điểm chính

  • ZonedDateTime là một lớp bất biến kết hợp ngày, giờ và múi giờ (ZoneId) trong một đối tượng duy nhất.
  • Khác với LocalDateTime, ZonedDateTime xác định duy nhất một thời điểm trên dòng thời gian và phù hợp cho các hệ thống toàn cầu.
  • Lớp này tự động xử lý các chuyển đổi thời gian mùa hè (DST) theo các quy tắc của Cơ sở dữ liệu IANA Time Zone Database.
  • Để chuyển đổi giữa các múi giờ, hãy sử dụng phương thức withZoneSameInstant(ZoneId).
  • Việc lưu trữ ZonedDateTime trong cơ sở dữ liệu được khuyến nghị thông qua OffsetDateTime hoặc TIMESTAMP WITH TIME ZONE.

ZonedDateTime là gì?

ZonedDateTime là một trong những lớp chính của gói java.time, biểu diễn ngày và giờ với thông tin múi giờ đầy đủ. Nó kết hợp ba thành phần: LocalDateTime (ngày và giờ), ZoneId (định danh múi giờ) và ZoneOffset (độ lệch so với UTC).

Khác với LocalDateTime, chỉ lưu trữ thời gian treo tường (wall-clock time) mà không có ràng buộc múi giờ, ZonedDateTime xác định duy nhất một thời điểm. Hai thể hiện LocalDateTime giống hệt nhau ở các múi giờ khác nhau đại diện cho các thời điểm khác nhau. Hai thể hiện ZonedDateTime giống hệt nhau — cùng một thời điểm.

Lớp này hoàn toàn bất biến và an toàn luồng (thread-safe). Tất cả các phép toán số học đều trả về một đối tượng mới. ZonedDateTime triển khai giao diện ChronoZonedDateTime và có thể được sử dụng ở bất cứ nơi nào cần xử lý thời gian theo múi giờ trong Java.

Theo đặc tả Oracle Java 17, ZonedDateTime hỗ trợ làm việc với bất kỳ múi giờ nào từ Cơ sở dữ liệu IANA Time Zone Database, bao gồm hơn 600 múi giờ.

ZonedDateTime vs LocalDateTime: sự khác biệt là gì?

Sự khác biệt chính — ZonedDateTime chứa múi giờ, trong khi LocalDateTime thì không. Sự khác biệt cơ bản này xác định phạm vi áp dụng của mỗi lớp.

LocalDateTime được sử dụng cho các sự kiện địa phương: giờ hòa nhạc, lịch học, ngày sinh. Nếu một sự kiện xảy ra ở Moscow lúc 15:00, LocalDateTime sẽ ghi lại 15:00 mà không có ràng buộc nào. Nếu bạn di chuyển máy chủ đến New York, thời gian vẫn là 15:00 — nhưng đó sẽ là một thời điểm vật lý khác.

ZonedDateTime được sử dụng cho dữ liệu toàn cầu: nhật ký máy chủ, dấu thời gian API, cuộc họp quốc tế. Nếu một cuộc họp được lên lịch lúc 15:00 MSK, ZonedDateTime sẽ giữ nguyên cả thời gian và múi giờ. Ở New York, nó sẽ hiển thị chính xác là 8:00 EST. Theo Baeldung (2024), việc chọn giữa LocalDateTime và ZonedDateTime là quyết định kiến trúc phổ biến nhất khi làm việc với ngày tháng.

Quy tắc thực tế: nếu dữ liệu được lưu trữ cho một khu vực — hãy sử dụng LocalDateTime. Nếu dữ liệu vượt qua ranh giới múi giờ — hãy sử dụng ZonedDateTime. Nếu bạn cần biểu diễn một thời điểm tuyệt đối — hãy sử dụng Instant.

Múi giờ trong java.time hoạt động như thế nào?

Múi giờ trong java.time được biểu diễn bởi lớp ZoneId. ZoneId là một định danh múi giờ theo định dạng “lục địa/khu vực”, ví dụ “Europe/Moscow”, “America/New_York”, “Asia/Tokyo”. ZoneId được lấy thông qua phương thức tĩnh of(String zoneId) hoặc thông qua múi giờ mặc định của hệ thống.

ZoneId được chia thành hai loại: fixed offset (độ lệch cố định, vd. “+03:00”) và region-based (múi giờ khu vực, vd. “Europe/London”). Các múi giờ khu vực chứa các quy tắc chuyển đổi thời gian mùa hè và các thay đổi lịch sử. Fixed offset chỉ đơn giản là một độ lệch cố định.

Để lấy độ lệch hiện tại của ZoneId tại một thời điểm cụ thể, hãy sử dụng phương thức getRules(), trả về ZoneRules. ZoneRules chứa tất cả các chuyển đổi và độ lệch cho một múi giờ nhất định. Đây là cơ chế chính để xử lý DST chính xác.

Tất cả các múi giờ đều đi kèm với JDK thông qua các tệp tzdata (IANA Time Zone Database) và được cập nhật thường xuyên. Trên Android, phiên bản tzdata phụ thuộc vào các bản cập nhật hệ thống thông qua Google Play Services.

Tạo ZonedDateTime

ZonedDateTime có thể được tạo theo nhiều cách. Đơn giản nhất là now(), trả về thời gian hiện tại trong múi giờ của hệ thống. Biến thể now(ZoneId) cho phép lấy thời gian hiện tại trong một múi giờ được chỉ định.

Phương thức of(LocalDateTime, ZoneId) tạo ZonedDateTime từ thời gian địa phương và múi giờ. Biến thể of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond, ZoneId zone) tạo từ các thành phần.

LocalDateTime có thể được chuyển đổi thành ZonedDateTime thông qua phương thức atZone(ZoneId). Instant — thông qua Instant.atZone(ZoneId). Date — thông qua Date.toInstant().atZone(ZoneId).

kotlin
val moscowZone = ZoneId.of("Europe/Moscow")

val nowInMoscow = ZonedDateTime.now(moscowZone)

val fromComponents = ZonedDateTime.of(
    2026, 7, 21, 15, 30, 0, 0, moscowZone
)

val fromLocal = LocalDateTime.now().atZone(moscowZone)

val fromInstant = Instant.now().atZone(moscowZone)

Chuyển đổi giữa các múi giờ

Phương thức chuyển đổi chính là withZoneSameInstant(ZoneId). Nó chuyển đổi ZonedDateTime sang múi giờ khác trong khi giữ nguyên cùng một thời điểm. Ví dụ, 15:00 MSK → 8:00 EST. Phương thức withZoneSameLocal(ZoneId) thay đổi múi giờ trong khi giữ nguyên thời gian địa phương — điều này tạo ra một thời điểm khác.

Để lấy độ lệch so với UTC, hãy sử dụng phương thức getOffset(), trả về ZoneOffset. ZoneOffset là một lớp con của ZoneId biểu diễn một độ lệch cố định theo định dạng “+HH:mm” hoặc “-HH:mm”.

Việc chuyển đổi sang Instant được thực hiện thông qua phương thức toInstant(). Instant là một thời điểm tuyệt đối trong thời gian, không phụ thuộc vào múi giờ. Chuyển đổi ngược lại là Instant.atZone(ZoneId).

kotlin
val moscow = ZonedDateTime.of(
    2026, 7, 21, 15, 0, 0, 0,
    ZoneId.of("Europe/Moscow")
)

val newYork = moscow.withZoneSameInstant(
    ZoneId.of("America/New_York")
)

val utcInstant = moscow.toInstant()
val backToMoscow = utcInstant.atZone(ZoneId.of("Europe/Moscow"))

Làm việc với thời gian mùa hè (DST)

Các chuyển đổi thời gian mùa hè tạo ra hai vấn đề: khoảng trống (gaps)sự chồng chéo (overlaps). Khoảng trống xảy ra vào mùa xuân khi đồng hồ được chỉnh nhanh hơn — một thời gian nhất định không tồn tại. Sự chồng chéo xảy ra vào mùa thu khi đồng hồ được chỉnh chậm lại — cùng một thời gian xảy ra hai lần.

ZonedDateTime xử lý các tình huống này thông qua chiến lược giải quyết (resolve). Khi tạo một đối tượng trong khoảng trống, java.time tự động dịch chuyển thời gian theo lượng độ lệch. Khi tạo trong thời gian chồng chéo, tùy chọn đầu tiên (trước khi chuyển đổi) được chọn. Hành vi này có thể được thay đổi thông qua withZoneSameInstant.

Bạn có thể kiểm tra xem một thời gian có trong DST hay không thông qua zone.getRules().isDaylightSavings(instant). Phương thức getOffset() hiển thị độ lệch thực tế cho một thời điểm nhất định, và getRules().getDaylightSavings(instant) hiển thị số lượng điều chỉnh DST tính bằng mili giây.

kotlin
fun checkDST(zdt: ZonedDateTime) {
    val rules = zdt.getZone().getRules()
    val instant = zdt.toInstant()

    if (rules.isDaylightSavings(instant)) {
        val dstAmount = rules.getDaylightSavings(instant)
        Log.d("Thời gian mùa hè", "Độ lệch thời gian mùa hè: $dstAmount")
    }
}

Định dạng ZonedDateTime

Để định dạng ZonedDateTime, hãy sử dụng DateTimeFormatter. Định dạng ISO tiêu chuẩn bao gồm ngày, giờ và độ lệch: “2026-07-21T15:30:00+03:00[Europe/Moscow]”. Các định dạng được xác định trước: ISO_ZONED_DATE_TIME, ISO_OFFSET_DATE_TIME, ISO_INSTANT.

Để đầu ra được bản địa hóa, hãy sử dụng DateTimeFormatter.ofLocalizedDateTime(FormatStyle). FormatStyle có thể là SHORT, MEDIUM, LONG, FULL. LONG bao gồm tên múi giờ (“MSK”), FULL bao gồm tên đầy đủ (“Moscow Standard Time”).

Quan trọng: khi phân tích cú pháp một chuỗi với ZonedDateTime, định dạng phải chứa thông tin múi giờ hoặc độ lệch. Nếu múi giờ không được chỉ định, hãy sử dụng LocalDateTime.parse() và sau đó atZone().

kotlin
val zdt = ZonedDateTime.now(ZoneId.of("Europe/Moscow"))

val iso = zdt.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)

val custom = DateTimeFormatter
    .ofPattern("dd.MM.yyyy HH:mm z")
val formatted = zdt.format(custom)

val parsed = ZonedDateTime.parse(
    "2026-07-21T15:30:00+03:00",
    DateTimeFormatter.ISO_OFFSET_DATE_TIME
)

ZonedDateTime trong Android: ví dụ thực tế

Ví dụ đầu tiên — hiển thị thời gian cuộc họp cho người dùng trong múi giờ địa phương của họ. Máy chủ gửi ZonedDateTime theo UTC, máy khách chuyển đổi sang múi giờ địa phương của thiết bị.

kotlin
fun displayMeetingTime(
    serverUtc: ZonedDateTime
): String {
    val deviceZone = ZoneId.systemDefault()
    val localTime = serverUtc.withZoneSameInstant(deviceZone)
    val formatter = DateTimeFormatter
        .ofPattern("dd.MM.yyyy HH:mm z")
    return localTime.format(formatter)
}

Ví dụ thứ hai — tính thời gian cho đến sự kiện tiếp theo có tính đến múi giờ. Chúng tôi sử dụng ZonedDateTime cho thời gian máy chủ và Duration.between() để tính toán sự khác biệt.

kotlin
fun timeUntilEvent(eventTime: ZonedDateTime): String {
    val now = ZonedDateTime.now()
    val duration = Duration.between(now, eventTime)

    val hours = duration.toHours()
    val minutes = duration.toMinutes() % 60
    return "Remaining $hours h $minutes min"
}

Ví dụ thứ ba — làm việc với Retrofit API. Máy chủ trả về một chuỗi ISO-8601 kèm múi giờ. Chúng tôi sử dụng trình giải mã tùy chỉnh để chuyển đổi thành ZonedDateTime.

kotlin
data class EventResponse(
    @JsonAdapter(ZonedDateTimeAdapter::class)
    val eventTime: ZonedDateTime
)

class ZonedDateTimeAdapter : JsonAdapter<ZonedDateTime>() {
    override fun fromJson(reader: JsonReader): ZonedDateTime? {
        return ZonedDateTime.parse(
            reader.nextString()
        )
    }
}

Các lỗi thường gặp khi làm việc với múi giờ

Lỗi đầu tiên — sử dụng ZoneId.systemDefault() trong mã máy chủ. Múi giờ của máy chủ có thể khác với máy khách và việc sử dụng múi giờ hệ thống trên máy chủ dẫn đến tính toán không chính xác. Luôn chỉ định rõ múi giờ hoặc sử dụng UTC làm tham chiếu.

Lỗi thứ hai — bỏ qua DST khi tính toán thời lượng. Duration.between() xử lý các chuyển đổi một cách chính xác, nhưng nếu bạn trừ dấu thời gian theo cách thủ công, thời gian mùa hè có thể gây ra lỗi 1 giờ. Hãy sử dụng ChronoUnit.HOURS.between() thay vì tính toán thủ công.

Lỗi thứ ba — nhầm lẫn giữa withZoneSameInstant và withZoneSameLocal. Cái đầu tiên thay đổi múi giờ trong khi giữ nguyên thời điểm — thời gian bị dịch chuyển. Cái thứ hai thay đổi múi giờ trong khi giữ nguyên thời gian địa phương — thời điểm thay đổi. Theo SonarSource (2024), việc chọn sai phương thức là một trong những lỗi phổ biến nhất.

Lỗi thứ tư — cho rằng múi giờ của thiết bị luôn giống với múi giờ của người dùng. Người dùng có thể đang di chuyển và mong đợi ứng dụng hiển thị thời gian ở múi giờ “nhà” của họ thay vì múi giờ hiện tại. Trong trường hợp này, hãy cung cấp tùy chọn chọn múi giờ thông qua giao diện.

Câu hỏi thường gặp

Sự khác biệt giữa ZonedDateTime và OffsetDateTime là gì?

ZonedDateTime chứa một định danh múi giờ khu vực (vd., “Europe/Moscow”) và xử lý DST. OffsetDateTime chỉ lưu trữ một độ lệch cố định (+03:00) mà không có quy tắc khu vực. Để lưu trữ cơ sở dữ liệu, OffsetDateTime được khuyến nghị.

Làm thế nào để lấy thời gian hiện tại theo UTC thông qua ZonedDateTime?

Sử dụng ZonedDateTime.now(ZoneOffset.UTC) hoặc Instant.now().atZone(ZoneOffset.UTC). Cả hai tùy chọn đều trả về thời điểm hiện tại với độ lệch bằng không. Để có một dấu thời gian đơn giản, hãy sử dụng Instant.now() mà không có ràng buộc múi giờ.

ZonedDateTime có thể được tuần tự hóa qua Gson hoặc Moshi không?

Có, nhưng cần một bộ chuyển đổi tùy chỉnh. Gson không hỗ trợ ZonedDateTime theo mặc định. Moshi hỗ trợ nó thông qua Rfc3339DateJsonAdapter. Nên sử dụng Kotlinx Serialization hoặc thư viện JavaTimeModule cho Jackson.

Làm thế nào để xử lý tình huống khi thời gian rơi vào khoảng trống DST?

java.time tự động dịch chuyển thời gian về phía trước theo lượng độ lệch. Ví dụ, nếu 02:30 không tồn tại khi đồng hồ được chỉnh nhanh đến 03:00, ZonedDateTime sẽ tạo một đối tượng lúc 03:30. Bạn có thể kiểm tra khoảng trống thông qua ZoneRules.getTransition(instant).

Tại sao ZonedDateTime không được khuyến nghị cho cơ sở dữ liệu SQL?

JDBC 4.2 hỗ trợ OffsetDateTime nhưng không hỗ trợ trực tiếp ZonedDateTime. ZonedDateTime chứa một múi giờ khu vực không có tương đương trong SQL. Nên lưu trữ OffsetDateTime hoặc Instant và lưu trữ múi giờ trong một cột riêng.

Tóm tắt

  • ZonedDateTime là một lớp bất biến cho ngày và giờ với múi giờ, xử lý chính xác DST thông qua Cơ sở dữ liệu IANA Time Zone Database.
  • Sự khác biệt chính so với LocalDateTime là sự hiện diện của múi giờ, làm cho ZonedDateTime trở thành một định danh duy nhất của một thời điểm.
  • Để chuyển đổi giữa các múi giờ, hãy sử dụng withZoneSameInstant(), giữ nguyên thời điểm, không phải withZoneSameLocal.
  • Trong quá trình chuyển đổi thời gian mùa hè, java.time tự động giải quyết các khoảng trống và sự chồng chéo thông qua các quy tắc múi giờ tích hợp.
  • Để lưu trữ cơ sở dữ liệu, hãy sử dụng OffsetDateTime hoặc lưu trữ Instant và ZoneId riêng biệt.
  • Trong Android, để chuyển đổi ZonedDateTime sang thời gian địa phương của thiết bị, hãy sử dụng ZoneId.systemDefault() kết hợp với withZoneSameInstant.
  • Để tuần tự hóa JSON, cần một bộ chuyển đổi tùy chỉnh — hãy sử dụng Kotlinx Serialization hoặc Jackson JavaTimeModule.

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm