Bekerja dengan tanggal dan jam adalah salah satu area yang paling sering memunculkan bug produksi. Kesalahan klasik seperti menyimpan waktu lokal padahal server berada di zona berbeda, atau menambahkan satu bulan ke tanggal 31 Januari, kerap terjadi bila kita memakai API lama java.util.Date. Kotlin di atas JVM mewarisi paket java.time yang memisahkan lima konsep inti: Instant, LocalDateTime, ZonedDateTime, Duration, dan Period. Bab ini memperkenalkan kelima konsep tersebut dan format ISO-8601 yang menjadi standar pertukaran antar sistem.
Lima Konsep Inti java.time #
Setiap tipe di java.time dirancang untuk satu pekerjaan spesifik. Mencampur-adukkan tipe adalah sumber bug paling umum. Tabel berikut memetakan tipe, peran, dan kapan harus dipakai:
| Tipe | Mengandung Zona? | Cocok Untuk |
|---|---|---|
Instant | Selalu UTC | Penyimpanan timestamp database, log audit |
LocalDate | Tidak | Tanggal lahir, tanggal faktur, hari libur |
LocalTime | Tidak | Jam alarm, jam buka toko |
LocalDateTime | Tidak | Penjadwalan abstrak tanpa konteks zona |
ZonedDateTime | Ya | Tampilan layar pengguna, perhitungan zona spesifik |
Duration | — | Panjang waktu berbasis jam/menit/detik |
Period | — | Panjang waktu berbasis tahun/bulan/hari |
Perbedaan paling penting ada pada Duration dan Period. Duration.ofDays(1) selalu 24 jam. Period.ofDays(1) berarti “tanggal besok” yang bisa jadi 23 atau 25 jam pada hari pergantian daylight saving time. Memilih salah satu secara keliru menjadi sumber bug tersekat (subtle bug) yang sulit dideteksi.
Contoh Lengkap: Lima Tipe Secara Bersamaan #
Kode berikut membangun sebuah instan acara tetap (15 Januari 2026, 08:30 UTC), lalu memproyeksikannya menjadi LocalDateTime, ZonedDateTime, ditambah Duration, dan Period:
import java.time.Duration
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.Period
import java.time.ZoneId
import java.time.ZonedDateTime
fun main() {
// 1. Instant: titik waktu absolut di UTC, bebas zona waktu
val instanSekarang: Instant = Instant.parse("2026-01-15T08:30:00Z")
println("=== Instant (UTC) ===")
println("instant : $instanSekarang")
println("epochSecond : ${instanSekarang.epochSecond}")
println("nano : ${instanSekarang.nano}")
// 2. LocalDateTime: tanggal + jam TANPA zona waktu
val jadwalRapat: LocalDateTime = LocalDateTime.of(2026, 1, 15, 8, 30)
println()
println("=== LocalDateTime (tanpa zona) ===")
println("jadwalRapat : $jadwalRapat")
// 3. ZonedDateTime: tanggal + jam + zona waktu
val zonaJakarta: ZoneId = ZoneId.of("Asia/Jakarta")
val jadwalJakarta: ZonedDateTime = ZonedDateTime.of(jadwalRapat, zonaJakarta)
println()
println("=== ZonedDateTime (dengan zona) ===")
println("jadwalJakarta : $jadwalJakarta")
println("zona : ${jadwalJakarta.zone}")
// 4. Duration: panjang waktu berbasis jam/menit/detik
val durasiRapat: Duration = Duration.ofMinutes(90)
val selesai: LocalDateTime = jadwalRapat.plus(durasiRapat)
println()
println("=== Duration ===")
println("durasiRapat : $durasiRapat (${durasiRapat.toMinutes()} menit)")
println("selesai : $selesai")
// 5. Period: panjang waktu berbasis tanggal (tahun/bulan/hari)
val periodeHalal: Period = Period.of(0, 1, 15)
val tanggalKedaluwarsa: LocalDate = LocalDate.of(2026, 1, 15).plus(periodeHalal)
println()
println("=== Period ===")
println("periodeHalal : $periodeHalal")
println("tanggalKedaluwarsa: $tanggalKedaluwarsa")
// 6. ISO-8601 format default
println()
println("=== ISO-8601 ===")
println("LocalDate ISO : ${LocalDate.of(2026, 1, 15)}")
println("LocalTime ISO : ${LocalTime.of(8, 30)}")
println("Instant ISO : ${Instant.parse("2026-01-15T08:30:00Z")}")
}Output yang Dihasilkan:
=== Instant (UTC) ===
instant : 2026-01-15T08:30:00Z
epochSecond : 1768465800
nano : 0
=== LocalDateTime (tanpa zona) ===
jadwalRapat : 2026-01-15T08:30
=== ZonedDateTime (dengan zona) ===
jadwalJakarta : 2026-01-15T08:30+07:00[Asia/Jakarta]
zona : Asia/Jakarta
=== Duration ===
durasiRapat : PT1H30M (90 menit)
selesai : 2026-01-15T10:00
=== Period ===
periodeHalal : P1M15D
tanggalKedaluwarsa: 2026-03-02
=== ISO-8601 ===
LocalDate ISO : 2026-01-15
LocalTime ISO : 08:30
Instant ISO : 2026-01-15T08:30:00ZMari kita telaah baris-baris kode di atas:
- Baris 11 (
Instant.parse("2026-01-15T08:30:00Z")): mengurai string ISO-8601 dengan akhiranZyang berarti Zulu/UTC. Hasilnya adalahInstantmurni yang dapat disimpan di kolomTIMESTAMPdatabase tanpa ambiguitas zona. - Baris 19 (
LocalDateTime.of(2026, 1, 15, 8, 30)): membentuk tanggal+jam tanpa zona. Cocok untuk input pengguna yang belum diikat ke zona tertentu, misalnya formulir “jam berapa rapat dimulai” sebelum pengguna memilih zona. - Baris 26 (
ZonedDateTime.of(jadwalRapat, zonaJakarta)): menggabungkanLocalDateTimedenganZoneIdmenjadi momen yang sepenuhnya terdefinisi. Offset+07:00muncul otomatis dari data zona. - Baris 33 (
Duration.ofMinutes(90)): menghasilkanPT1H30M. Kita menambahkannya keLocalDateTimelewatplus(...). Operasi ini bekerja untuk jam karenaDurationberbasis detik. - Baris 41 (
Period.of(0, 1, 15)): menambahkan 1 bulan 15 hari ke tanggal 15 Januari 2026 menghasilkan 2 Maret 2026 (bukan 30 Februari, karena Februari 2026 hanya 28 hari). Inilah kekuatanPeriodyang menghormati panjang kalender. - Baris 50-52 (cetak ISO): setiap tipe
java.timememakaitoString()yang menghasilkan format ISO-8601 bawaan. Format ini dapat di-parse kembali tanpa formatter khusus.
Format ISO-8601: Bahasa Antar Sistem #
ISO-8601 adalah standar internasional representasi tanggal dan jam. Setiap tipe java.time memakai format ISO secara default di toString(). Pola dasarnya ringkas dan tidak ambigu:
Date : yyyy-MM-dd -> 2026-01-15
Time : HH:mm:ss -> 08:30:00
DateTime : yyyy-MM-ddTHH:mm -> 2026-01-15T08:30
Instant (UTC) : yyyy-MM-ddTHH:mm:ssZ -> 2026-01-15T08:30:00Z
Period : PnYnMnD -> P1M15D
Duration : PTnHnMnS -> PT1H30MKarakter T memisahkan tanggal dan jam. Karakter Z menandai UTC. Karakter P memulai period dan PT memulai duration. Format ini dapat dipertukarkan antara backend, frontend, API, dan log tanpa perlu formatter tambahan.
Referensi Lanjutan #
- java.time package
— ringkasan paket
java.timeJava SE 21 untuk tipe tanggal dan jam - Kotlin stdlib java.time
— halaman Kotlin stdlib untuk alias
java.timeyang idiomatic
Selanjutnya #
Lima tipe di atas berasal dari paket java.time bawaan JDK. Pada chapter java.time Interoperability kita akan melihat bagaimana Kotlin mengonsumsi API ini secara langsung tanpa wrapper, termasuk idiom extension function yang membuat pemakaian java.time terasa lebih Kotlin-idiomatic.