LocalDateTime tidak mengetahui zona sehingga tidak cukup untuk operasi yang melibatkan jam di negara berbeda. Untuk kebutuhan ini java.time menyediakan Instant — titik waktu absolut di UTC — dan ZonedDateTime — proyeksi instan ke zona waktu tertentu. Bab ini membahas cara membuat Instant dari string ISO-8601, mengonversinya menjadi ZonedDateTime di berbagai zona, dan membandingkan momen yang sama dari sudut pandang zona berbeda.
Konsep: Instant Adalah Titik Mutlak UTC #
Instant adalah momen tunggal di garis waktu UTC. Nilainya direpresentasikan sebagai jumlah detik (dan nanodetik) sejak Unix epoch 1970-01-01T00:00:00Z. Karena selalu di UTC, Instant tidak ambigu: dua Instant yang sama selalu menunjuk momen yang persis sama di seluruh dunia, tidak peduli server berada di mana.
| Konsep | LocalDateTime | Instant | ZonedDateTime |
|---|---|---|---|
| Mengandung zona? | Tidak | Selalu UTC | Ya |
| Cocok untuk penyimpanan | Tidak | Ya | Ya (tapi boros) |
| Cocok untuk tampilan pengguna | Hanya bila zona implisit | Tidak | Ya |
| Dapat dibandingkan lintas zona | Tidak aman | Ya | Ya (lewat toInstant()) |
Aturan praktis: simpan Instant di database dan di log, tampilkan ZonedDateTime ke pengguna. Konversi terjadi di lapisan presentasi.
Membuat dan Mengonversi Instant #
Kode berikut membuat sebuah Instant dari string ISO-8601 tetap, lalu memproyeksikannya ke lima zona berbeda dan menunjukkan konversi dua arah antar zona:
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.ZonedDateTime
fun main() {
// 1. Instant: titik waktu absolut di UTC (bebas zona)
val instanKunci: Instant = Instant.parse("2026-01-15T08:30:00Z")
println("=== Instant (UTC) ===")
println("instanKunci : $instanKunci")
println("epochSecond : ${instanKunci.epochSecond}")
// 2. ZonedDateTime.ofInstant(instant, zoneId)
val zonaJakarta: ZoneId = ZoneId.of("Asia/Jakarta")
val zonaTokyo: ZoneId = ZoneId.of("Asia/Tokyo")
val zonaLondon: ZoneId = ZoneId.of("Europe/London")
val diJakarta: ZonedDateTime = ZonedDateTime.ofInstant(instanKunci, zonaJakarta)
val diTokyo: ZonedDateTime = ZonedDateTime.ofInstant(instanKunci, zonaTokyo)
val diLondon: ZonedDateTime = ZonedDateTime.ofInstant(instanKunci, zonaLondon)
println()
println("=== ZonedDateTime.ofInstant(instant, zone) ===")
println("diJakarta : $diJakarta")
println("diTokyo : $diTokyo")
println("diLondon : $diLondon")
// 3. Konversi antar zona via withZoneSameInstant
val dariJakartaKeTokyo: ZonedDateTime = diJakarta.withZoneSameInstant(zonaTokyo)
println()
println("=== withZoneSameInstant ===")
println("dariJakartaKeTokyo: $dariJakartaKeTokyo")
println("sama instan? : ${dariJakartaKeTokyo.toInstant() == diTokyo.toInstant()}")
// 4. Membuat ZonedDateTime dari LocalDateTime + zona
val jadwalLokalJakarta: ZonedDateTime =
ZonedDateTime.of(LocalDate.of(2026, 1, 15).atTime(8, 30), zonaJakarta)
println()
println("=== Dari LocalDateTime + zona ===")
println("jadwalLokalJakarta: $jadwalLokalJakarta")
println("instan di UTC : ${jadwalLokalJakarta.toInstant()}")
// 5. Mengambil LocalDate/LocalTime dari ZonedDateTime
val tanggalLokal: LocalDate = diJakarta.toLocalDate()
val jamLokal = diJakarta.toLocalTime()
println()
println("=== Ekstraksi komponen ===")
println("tanggalLokal : $tanggalLokal")
println("jamLokal : $jamLokal")
println("offset : ${diJakarta.offset}")
// 6. Daftar zona umum dan offset mereka pada saat instan tersebut
val daftarZona: List<Pair<String, ZoneId>> = listOf(
"Jakarta" to zonaJakarta,
"Tokyo" to zonaTokyo,
"London" to zonaLondon,
"New York" to ZoneId.of("America/New_York"),
"Sydney" to ZoneId.of("Australia/Sydney"),
)
println()
println("=== Offset per zona pada $instanKunci ===")
for ((nama, zona) in daftarZona) {
val zdt = instanKunci.atZone(zona)
println("${nama.padEnd(10)} -> ${zdt.offset} | $zdt")
}
}Output yang Dihasilkan:
=== Instant (UTC) ===
instanKunci : 2026-01-15T08:30:00Z
epochSecond : 1768465800
=== ZonedDateTime.ofInstant(instant, zone) ===
diJakarta : 2026-01-15T15:30+07:00[Asia/Jakarta]
diTokyo : 2026-01-15T17:30+09:00[Asia/Tokyo]
diLondon : 2026-01-15T08:30Z[Europe/London]
=== withZoneSameInstant ===
dariJakartaKeTokyo: 2026-01-15T17:30+09:00[Asia/Tokyo]
sama instan? : true
=== Dari LocalDateTime + zona ===
jadwalLokalJakarta: 2026-01-15T08:30+07:00[Asia/Jakarta]
instan di UTC : 2026-01-15T01:30:00Z
=== Ekstraksi komponen ===
tanggalLokal : 2026-01-15
jamLokal : 15:30
offset : +07:00
=== Offset per zona pada 2026-01-15T08:30:00Z ===
Jakarta -> +07:00 | 2026-01-15T15:30+07:00[Asia/Jakarta]
Tokyo -> +09:00 | 2026-01-15T17:30+09:00[Asia/Tokyo]
London -> Z | 2026-01-15T08:30Z[Europe/London]
New York -> -05:00 | 2026-01-15T03:30-05:00[America/New_York]
Sydney -> +11:00 | 2026-01-15T19:30+11:00[Australia/Sydney]Mari kita telaah baris-baris kode di atas:
- Baris 8 (
Instant.parse("2026-01-15T08:30:00Z")): akhiranZmenandai UTC (Zulu time). Tanpa akhiranZatau offset, parser akan menolak string tersebut karenaInstantwajib mengandung zona. - Baris 17-19 (
ZonedDateTime.ofInstant(instan, zona)): mengonversiInstantmurni keZonedDateTime. Offset muncul otomatis berdasarkan aturan zona. Pada 15 Januari 2026, Jakarta+07:00, Tokyo+09:00, LondonZ(UTC karena bukan masa DST). - Baris 27 (
diJakarta.withZoneSameInstant(zonaTokyo)): method ini mengubah zona tanpa mengubah momen. Hasilnya sama dengan membuatZonedDateTimelangsung dariinstandan zona tujuan, seperti yang dibuktikan oleh perbandingantoInstant()di baris 29. - Baris 34 (
ZonedDateTime.of(LocalDateTime, zona)): arah berlawanan — kita punyaLocalDateTimelokal (08:30 di Jakarta) dan ingin tahuInstantUTC-nya. Hasilnya2026-01-15T01:30:00Zkarena 08:30 Jakarta = 01:30 UTC. - Baris 41-43 (
toLocalDate(),toLocalTime()): mengekstrak komponen lokal dariZonedDateTime. Perlu diingat bahwa tanggal lokal bisa berbeda antar zona untukInstantyang sama (mis. 08:30 UTC = 03:30 New York pada tanggal yang sama, tetapi 19:30 Sydney sudah masuk hari berikutnya di kasus tertentu). - Baris 50-58 (iterasi daftar zona): membandingkan lima zona populer. Perhatikan offset
Zuntuk London (UTC musim dingin) berbeda dari+07:00Jakarta — momen tunggal direpresentasikan dengan lima pasangan tanggal-jam-offset berbeda.
Konversi Antar Zona #
Operasi withZoneSameInstant(...) adalah cara idiomatik untuk berpindah zona tanpa mengubah momen. Operasi ini memperhitungkan aturan zona (DST, perubahan kebijakan pemerintah) berdasarkan data IANA tzdb yang dibundel JDK.
fun tampilkanWaktuLokal(instanUTC: Instant, zonaPengguna: ZoneId): String {
val zdt = instanUTC.atZone(zonaPengguna)
return "${zdt.toLocalDate()} ${zdt.toLocalTime()} ${zdt.offset}"
}Fungsi di atas menerima Instant murni dari backend dan menampilkannya dalam zona pengguna. Backend tidak perlu tahu zona pengguna; konversi terjadi di lapisan presentasi.
Referensi Lanjutan #
- java.time package
— dokumentasi
java.timeuntukInstant,ZoneId, danZonedDateTime
Selanjutnya #
Setelah memahami cara membuat dan mengonversi instan, langkah berikutnya adalah memformat tanggal untuk tampilan pengguna. Bab Date-Time Formatting akan membahas DateTimeFormatter.ofPattern(...) beserta predefined formatters dan format locale-aware.