ZonedDateTime — kelas immutable dari paket java.time yang menyimpan tanggal dan waktu bersama dengan informasi zona waktu (ZoneId). Berbeda dengan LocalDateTime, ZonedDateTime secara unik mengidentifikasi momen pada garis waktu. Menurut spesifikasi Oracle Java 17 (2024), kelas ini menangani transisi ke waktu musim panas (DST) dengan benar melalui aturan zona dari basis data IANA Time Zone Database.
Poin Utama
ZonedDateTime — salah satu kelas kunci dari paket java.time, yang mewakili tanggal dan waktu dengan informasi lengkap tentang zona waktu. Ini menggabungkan tiga komponen: LocalDateTime (tanggal dan waktu), ZoneId (identifikasi zona), dan ZoneOffset (offset relatif terhadap UTC).
Berbeda dengan LocalDateTime, yang hanya menyimpan waktu dinding (wall-clock time) tanpa kaitan dengan zona, ZonedDateTime secara unik mengidentifikasi momen. Dua LocalDateTime yang sama di zona waktu yang berbeda mewakili momen waktu yang berbeda. Dua ZonedDateTime yang sama — momen yang sama.
Kelas ini sepenuhnya immutable dan thread-safe. Semua operasi aritmatika mengembalikan objek baru. ZonedDateTime mengimplementasikan antarmuka ChronoZonedDateTime dan dapat digunakan di mana pun diperlukan bekerja dengan waktu zona di Java.
Menurut spesifikasi Oracle Java 17, ZonedDateTime mendukung bekerja dengan zona apa pun dari IANA Time Zone Database, yang mencakup lebih dari 600 zona waktu.
Perbedaan utama — ZonedDateTime berisi zona waktu, sedangkan LocalDateTime tidak. Perbedaan mendasar ini menentukan ruang lingkup penerapan setiap kelas.
LocalDateTime digunakan untuk acara lokal: waktu konser, jadwal pelajaran, tanggal lahir. Jika acara berlangsung di Moskow pukul 15:00, LocalDateTime akan mencatat 15:00 tanpa kaitan. Jika Anda memindahkan server ke New York, waktu tetap 15:00 — tetapi ini sudah merupakan momen fisik yang berbeda.
ZonedDateTime diterapkan untuk data global: log server, stempel waktu di API, pertemuan internasional. Jika pertemuan dijadwalkan pada pukul 15:00 MSK, ZonedDateTime akan menyimpan waktu dan zona. Di New York akan ditampilkan dengan benar sebagai 8:00 EST. Menurut Baeldung (2024), pilihan antara LocalDateTime dan ZonedDateTime adalah keputusan arsitektur paling umum saat bekerja dengan tanggal.
Aturan praktis: jika data disimpan untuk satu wilayah — gunakan LocalDateTime. Jika data melintasi batas zona waktu — gunakan ZonedDateTime. Jika Anda perlu mengirimkan momen absolut — gunakan Instant.
Zona waktu di java.time diwakili oleh kelas ZoneId. ZoneId adalah pengidentifikasi zona dalam format “continent/region”, misalnya “Europe/Moscow”, “America/New_York”, “Asia/Tokyo”. ZoneId diperoleh melalui metode statis of(String zoneId) atau melalui zona waktu sistem default.
ZoneId dibagi menjadi dua tipe: fixed offset (offset tetap, misalnya “+03:00”) dan region-based (zona regional, misalnya “Europe/London”). Zona regional berisi aturan transisi ke waktu musim panas dan perubahan historis. Fixed offset — hanya offset tetap.
Untuk mendapatkan offset saat ini dari ZoneId pada momen tertentu, digunakan metode getRules(), yang mengembalikan ZoneRules. ZoneRules berisi semua transisi dan offset untuk zona tersebut. Ini adalah mekanisme kunci untuk penanganan DST yang benar.
Semua zona waktu disediakan dengan JDK melalui file tzdata (IANA Time Zone Database) dan diperbarui secara teratur. Di Android, versi tzdata tergantung pada pembaruan sistem melalui Google Play Services.
Ada beberapa cara untuk membuat ZonedDateTime. Yang paling sederhana — now(), yang mengembalikan waktu saat ini di zona waktu sistem. Varian now(ZoneId) memungkinkan mendapatkan waktu saat ini di zona yang ditentukan.
Metode of(LocalDateTime, ZoneId) membuat ZonedDateTime dari waktu lokal dan zona. Varian of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond, ZoneId zone) — dari komponen.
LocalDateTime dapat dikonversi ke ZonedDateTime melalui metode atZone(ZoneId). Instant — melalui Instant.atZone(ZoneId). Date — melalui Date.toInstant().atZone(ZoneId).
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)
Metode konversi utama — withZoneSameInstant(ZoneId). Mengonversi ZonedDateTime ke zona waktu lain, mempertahankan momen waktu yang sama. Misalnya, 15:00 MSK → 8:00 EST. Metode withZoneSameLocal(ZoneId) mengubah zona, mempertahankan waktu lokal — ini memberikan momen yang berbeda.
Untuk mendapatkan offset relatif terhadap UTC, digunakan metode getOffset(), yang mengembalikan ZoneOffset. ZoneOffset adalah turunan dari ZoneId yang mewakili offset tetap dalam format “+HH:mm” atau “-HH:mm”.
Konversi ke Instant dilakukan melalui metode toInstant(). Instant adalah momen waktu absolut, tidak tergantung pada zona waktu. Konversi terbalik — Instant.atZone(ZoneId).
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"))
Transisi ke waktu musim panas menciptakan dua masalah: celah (gap) dan tumpang tindih (overlap). Celah terjadi di musim semi ketika jam dimajukan — waktu tertentu tidak ada. Tumpang tindih — di musim gugur ketika waktu dimundurkan — waktu yang sama ada dua kali.
ZonedDateTime menangani situasi ini melalui strategi resolve. Saat membuat objek selama celah, java.time secara otomatis memajukan waktu sebesar nilai offset. Saat membuat selama tumpang tindih, varian pertama (sebelum perubahan) dipilih. Perilaku dapat diubah melalui withZoneSameInstant.
Untuk memeriksa apakah waktu berada di zona DST, gunakan zone.getRules().isDaylightSavings(instant). Metode getOffset() menunjukkan offset saat ini untuk momen tersebut, dan getRules().getDaylightSavings(instant) — besarnya koreksi DST dalam milidetik.
fun checkDST(zdt: ZonedDateTime) {
val rules = zdt.getZone().getRules()
val instant = zdt.toInstant()
if (rules.isDaylightSavings(instant)) {
val dstAmount = rules.getDaylightSavings(instant)
Log.d("DST", "Offset DST: $dstAmount")
}
}
Untuk memformat ZonedDateTime digunakan DateTimeFormatter. Format ISO standar mencakup tanggal, waktu, dan offset: “2026-07-21T15:30:00+03:00[Europe/Moscow]”. Format yang telah ditentukan: ISO_ZONED_DATE_TIME, ISO_OFFSET_DATE_TIME, ISO_INSTANT.
Gunakan DateTimeFormatter.ofLocalizedDateTime(FormatStyle) untuk pemformatan yang dilokalkan. FormatStyle dapat berupa SHORT, MEDIUM, LONG, FULL. LONG menyertakan nama zona (“MSK”), FULL — nama lengkap (“Moscow Standard Time”).
Penting: saat mengurai string dengan ZonedDateTime, format harus berisi informasi tentang zona atau offset. Jika zona tidak ditentukan, gunakan LocalDateTime.parse() dan kemudian atZone().
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
)
Contoh pertama — menampilkan waktu pertemuan untuk pengguna di zona waktunya sendiri. Server mengembalikan ZonedDateTime dalam UTC, klien mengonversi ke zona waktu lokal perangkat.
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)
}
Contoh kedua — menghitung waktu hingga acara berikutnya dengan mempertimbangkan zona waktu. Kami menggunakan ZonedDateTime untuk waktu server dan Duration.between() untuk menghitung perbedaannya.
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"
}
Contoh ketiga — bekerja dengan API Retrofit. Server mengembalikan string dalam ISO-8601 dengan zona. Kami menggunakan deserializer kustom untuk konversi ke ZonedDateTime.
data class EventResponse(
@JsonAdapter(ZonedDateTimeAdapter::class)
val eventTime: ZonedDateTime
)
class ZonedDateTimeAdapter : JsonAdapter<ZonedDateTime>() {
override fun fromJson(reader: JsonReader): ZonedDateTime? {
return ZonedDateTime.parse(
reader.nextString()
)
}
}
Kesalahan pertama — penggunaan ZoneId.systemDefault() dalam kode server. Zona waktu server mungkin berbeda dari klien, dan penggunaan zona sistem di server menyebabkan perhitungan yang salah. Selalu tentukan zona secara eksplisit atau gunakan UTC sebagai referensi.
Kesalahan kedua — mengabaikan DST saat menghitung durasi. Duration.between() menangani transisi dengan benar, tetapi jika Anda mengurangi stempel waktu secara manual, transisi ke waktu musim panas dapat memberikan kesalahan 1 jam. Gunakan metode ChronoUnit.HOURS.between() sebagai ganti perhitungan manual.
Kesalahan ketiga — kebingungan antara withZoneSameInstant dan withZoneSameLocal. Yang pertama mengubah zona, mempertahankan momen — waktu bergeser. Yang kedua mengubah zona, mempertahankan waktu lokal — momen berubah. Pemilihan metode yang salah adalah salah satu kesalahan paling umum menurut SonarSource (2024).
Kesalahan keempat — asumsi bahwa zona waktu perangkat selalu sama dengan zona waktu pengguna. Pengguna dapat bepergian dan mengharapkan aplikasi menampilkan waktu di zona “rumah” mereka, bukan zona saat ini. Dalam hal ini, Anda perlu menyediakan pilihan zona melalui antarmuka.
Pertanyaan yang Sering Diajukan
ZonedDateTime berisi pengidentifikasi zona regional (misalnya “Europe/Moscow”) dan menangani DST. OffsetDateTime hanya menyimpan offset tetap (+03:00) tanpa aturan regional. Untuk penyimpanan di basis data, OffsetDateTime direkomendasikan.
Gunakan ZonedDateTime.now(ZoneOffset.UTC) atau Instant.now().atZone(ZoneOffset.UTC). Kedua varian mengembalikan momen saat ini dengan offset nol. Untuk stempel waktu sederhana, gunakan Instant.now() tanpa kaitan ke zona.
Ya, tetapi diperlukan adapter kustom. Gson tidak mendukung ZonedDateTime secara default. Moshi — mendukung melalui adapter Rfc3339DateJsonAdapter. Untuk Jackson, disarankan menggunakan Kotlinx Serialization atau pustaka JavaTimeModule.
java.time secara otomatis memajukan waktu sebesar nilai offset. Misalnya, jika waktu 02:30 tidak ada saat transisi ke 03:00, ZonedDateTime akan membuat objek pada 03:30. Keberadaan celah dapat diperiksa melalui ZoneRules.getTransition(instant).
JDBC 4.2 mendukung OffsetDateTime, tetapi tidak ZonedDateTime secara langsung. ZonedDateTime berisi zona regional yang tidak memiliki padanan di SQL. Disarankan untuk menyimpan OffsetDateTime atau Instant, dan zona disimpan di kolom terpisah.
Kesimpulan
Kami akan mengembangkan aplikasi seluler turnkey
IT Sectr membuat aplikasi iOS dan Android untuk startup dan bisnis sejak 2017. Kami akan memberi saran dan mengusulkan solusi terbaik.
Baca juga