Banyak tanggal dalam kehidupan sehari-hari tidak terikat zona waktu: tanggal lahir seseorang, tanggal faktur, jam buka toko, atau jadwal rapat yang belum diikat ke kota tertentu. Untuk kasus seperti ini, java.time menyediakan tiga tipe terpisah: LocalDate (tanggal saja), LocalTime (jam saja), dan LocalDateTime (gabungan keduanya). Bab ini memperkenalkan cara membuat ketiga tipe tersebut, mengakses propertinya, serta mengurai string ISO-8601 menjadi instance valid.

Tipe LocalDate dan LocalTime #

LocalDate merepresentasikan tanggal tanpa jam dan tanpa zona. LocalTime merepresentasikan jam tanpa tanggal dan tanpa zona. Keduanya tidak membawa informasi offset sehingga tidak dapat diubah menjadi Instant tanpa menyertakan zona secara eksplisit. Tabel berikut memetakan perbedaannya:

TipeMengandungTidak MengandungCocok Untuk
LocalDatetahun, bulan, harijam, zonatanggal lahir, hari libur
LocalTimejam, menit, detik, nanotanggal, zonajam buka, jam alarm
LocalDateTimegabungan keduanyazonajadwal abstrak

Pemilihan tipe yang tepat menentukan apakah tanggal bisa dibandingkan, dihitung selisihnya, atau dikonversi ke zona tertentu. LocalDate cocok untuk input pengguna yang tidak peduli jam, sedangkan LocalDateTime dipakai bila kita butuh jam namun belum tahu zona pengguna.

Konstruksi LocalDate, LocalTime, LocalDateTime #

Kode berikut membuat ketiga tipe dengan beberapa cara, lalu mengakses properti harian seperti dayOfWeek, dayOfYear, month, dan sebagainya:

kotlin
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.Month

fun main() {
    // 1. LocalDate.of(year, month, day)
    val tanggalLahir: LocalDate = LocalDate.of(2026, 1, 15)
    println("=== LocalDate ===")
    println("tanggalLahir    : $tanggalLahir")
    println("tahun           : ${tanggalLahir.year}")
    println("bulan (Month)   : ${tanggalLahir.month}")
    println("bulan (Int)     : ${tanggalLahir.monthValue}")
    println("hari            : ${tanggalLahir.dayOfMonth}")
    println("hariKeDalamTahun: ${tanggalLahir.dayOfYear}")
    println("hariDalamPekan : ${tanggalLahir.dayOfWeek}")

    // 2. LocalDate.of dengan enum Month (lebih deskriptif)
    val hariLibur: LocalDate = LocalDate.of(2026, Month.AUGUST, 17)
    println()
    println("=== LocalDate dengan Month enum ===")
    println("hariLibur       : $hariLibur (Hari Kemerdekaan)")

    // 3. LocalTime.of(hour, minute) atau of(hour, minute, second, nano)
    val jamBuka: LocalTime = LocalTime.of(8, 30)
    val jamTutup: LocalTime = LocalTime.of(21, 0, 0)
    println()
    println("=== LocalTime ===")
    println("jamBuka         : $jamBuka")
    println("jamTutup        : $jamTutup")
    println("jam             : ${jamBuka.hour}")
    println("menit           : ${jamBuka.minute}")
    println("detik           : ${jamTutup.second}")

    // 4. LocalDateTime.of(date, time) atau of(year,month,day,hour,minute,second)
    val jadwalPesta: LocalDateTime = LocalDateTime.of(tanggalLahir, jamBuka)
    val jadwalRapat: LocalDateTime = LocalDateTime.of(2026, 3, 1, 14, 30, 0)
    println()
    println("=== LocalDateTime ===")
    println("jadwalPesta     : $jadwalPesta")
    println("jadwalRapat     : $jadwalRapat")
    println("tanggal diambil: ${jadwalRapat.toLocalDate()}")
    println("jam diambil     : ${jadwalRapat.toLocalTime()}")

    // 5. Parsing ISO-8601 string
    val dariString1: LocalDate = LocalDate.parse("2026-01-15")
    val dariString2: LocalTime = LocalTime.parse("08:30:00")
    val dariString3: LocalDateTime = LocalDateTime.parse("2026-01-15T08:30:00")
    println()
    println("=== Parsing ISO-8601 ===")
    println("dariString1     : $dariString1")
    println("dariString2     : $dariString2")
    println("dariString3     : $dariString3")

    // 6. Perbandingan tanggal
    println()
    println("=== Perbandingan ===")
    println("sebelum? ${tanggalLahir.isBefore(hariLibur)}")
    println("setelah? ${tanggalLahir.isAfter(hariLibur)}")
    println("sama?    ${tanggalLahir == LocalDate.of(2026, 1, 15)}")

    // 7. Cek hari kerja / akhir pekan
    val hariPekan: DayOfWeek = tanggalLahir.dayOfWeek
    val akhirPekan: Boolean = hariPekan.value >= 6
    println()
    println("=== Hari Pekan ===")
    println("hariPekan       : $hariPekan (value=${hariPekan.value})")
    println("akhirPekan?     : $akhirPekan")
}

Output yang Dihasilkan:

text
=== LocalDate ===
tanggalLahir    : 2026-01-15
tahun           : 2026
bulan (Month)   : JANUARY
bulan (Int)     : 1
hari            : 15
hariKeDalamTahun: 15
hariDalamPekan : THURSDAY

=== LocalDate dengan Month enum ===
hariLibur       : 2026-08-17 (Hari Kemerdekaan)

=== LocalTime ===
jamBuka         : 08:30
jamTutup        : 21:00
jam             : 8
menit           : 30
detik           : 0

=== LocalDateTime ===
jadwalPesta     : 2026-01-15T08:30
jadwalRapat     : 2026-03-01T14:30
tanggal diambil: 2026-03-01
jam diambil     : 14:30

=== Parsing ISO-8601 ===
dariString1     : 2026-01-15
dariString2     : 08:30
dariString3     : 2026-01-15T08:30

=== Perbandingan ===
sebelum? true
setelah? false
sama?    true

=== Hari Pekan ===
hariPekan       : THURSDAY (value=4)
akhirPekan?     : false

Mari kita telaah baris-baris kode di atas:

  1. Baris 8 (LocalDate.of(2026, 1, 15)): membuat LocalDate dengan tahun, bulan (numerik 1-12), dan hari. Konstruktor melakukan validasi sehingga tanggal seperti 2026-02-30 akan melempar DateTimeException saat runtime.
  2. Baris 16 (LocalDate.of(2026, Month.AUGUST, 17)): varian yang memakai enum Month. Lebih deskriptif karena tidak ada ambiguitas apakah angka 8 berarti Agustus atau September (kesalahan klasik off-by-one dari programmer C lama yang memakai Calendar.MONTH berbasis nol).
  3. Baris 22-23 (LocalTime.of(8, 30) dan LocalTime.of(21, 0, 0)): ada empat overload — (h,m), (h,m,s), (h,m,s,n). Tidak menyertakan detik berarti detik diisi 0. Pemanggilan dengan jam 25 akan melempar DateTimeException.
  4. Baris 31 (LocalDateTime.of(tanggalLahir, jamBuka)): menggabungkan instance LocalDate dan LocalTime menjadi LocalDateTime. Komposisi ini lebih jelas daripada konstruktor 6 argumen.
  5. Baris 41-43 (LocalDate.parse("2026-01-15")): parsing ISO-8601. Format default LocalDate adalah yyyy-MM-dd, LocalTime adalah HH:mm:ss (atau HH:mm bila detik 0), dan LocalDateTime adalah gabungan dengan pemisah T. Pemanggilan parse tanpa formatter hanya menerima format ISO standar.
  6. Baris 51 (tanggalLahir.isBefore(hariLibur)): method isBefore, isAfter, dan isEqual membandingkan tanggal murni tanpa zona. Hasilnya deterministik karena LocalDate tidak terpengaruh zona.
  7. Baris 58 (hariPekan.value >= 6): enum DayOfWeek memberi nilai 1-7 (Senin-Minggu) sesuai ISO-8601, bukan 0-6 seperti java.util.Calendar. Sabtu (6) dan Minggu (7) adalah akhir pekan.

Parsing dan Komposisi #

LocalDate.parse(...) memakai DateTimeFormatter.ISO_LOCAL_DATE sebagai default. Format ini ketat: tahun 4 digit, bulan 2 digit dengan leading zero, hari 2 digit dengan leading zero. Untuk format lain (mis. 15/01/2026) kita harus menyediakan DateTimeFormatter eksplisit, yang akan dibahas tuntas di chapter Parsing Date-Time.

Komposisi LocalDate dan LocalTime dapat dipisah kembali lewat toLocalDate() dan toLocalTime(). Operasi ini tidak membuat objek baru — hanya memancarkan view ke field yang sudah ada. Hal ini aman dari segi performa.

Referensi Lanjutan #

  • java.time package — dokumentasi paket java.time untuk LocalDate, LocalTime, dan LocalDateTime

Selanjutnya #

LocalDate dan LocalTime tidak membawa zona. Saat kita perlu merepresentasikan momen spesifik di UTC atau di zona tertentu, gunakan Instant. Bab Instant dan Time Zones akan membahas konversi antar zona dengan aman.