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.

KonsepLocalDateTimeInstantZonedDateTime
Mengandung zona?TidakSelalu UTCYa
Cocok untuk penyimpananTidakYaYa (tapi boros)
Cocok untuk tampilan penggunaHanya bila zona implisitTidakYa
Dapat dibandingkan lintas zonaTidak amanYaYa (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:

kotlin
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:

text
=== 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:

  1. Baris 8 (Instant.parse("2026-01-15T08:30:00Z")): akhiran Z menandai UTC (Zulu time). Tanpa akhiran Z atau offset, parser akan menolak string tersebut karena Instant wajib mengandung zona.
  2. Baris 17-19 (ZonedDateTime.ofInstant(instan, zona)): mengonversi Instant murni ke ZonedDateTime. Offset muncul otomatis berdasarkan aturan zona. Pada 15 Januari 2026, Jakarta +07:00, Tokyo +09:00, London Z (UTC karena bukan masa DST).
  3. Baris 27 (diJakarta.withZoneSameInstant(zonaTokyo)): method ini mengubah zona tanpa mengubah momen. Hasilnya sama dengan membuat ZonedDateTime langsung dari instan dan zona tujuan, seperti yang dibuktikan oleh perbandingan toInstant() di baris 29.
  4. Baris 34 (ZonedDateTime.of(LocalDateTime, zona)): arah berlawanan — kita punya LocalDateTime lokal (08:30 di Jakarta) dan ingin tahu Instant UTC-nya. Hasilnya 2026-01-15T01:30:00Z karena 08:30 Jakarta = 01:30 UTC.
  5. Baris 41-43 (toLocalDate(), toLocalTime()): mengekstrak komponen lokal dari ZonedDateTime. Perlu diingat bahwa tanggal lokal bisa berbeda antar zona untuk Instant yang sama (mis. 08:30 UTC = 03:30 New York pada tanggal yang sama, tetapi 19:30 Sydney sudah masuk hari berikutnya di kasus tertentu).
  6. Baris 50-58 (iterasi daftar zona): membandingkan lima zona populer. Perhatikan offset Z untuk London (UTC musim dingin) berbeda dari +07:00 Jakarta — 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.

kotlin Pola: konversi zona untuk tampilan pengguna
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 #

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.