Menghitung tanggal terasa mudah sampai panjang bulan, tahun kabisat, dan zona waktu ikut campur. Kelas Date dan Calendar warisan Java awal menangani hal itu dengan gaya yang rawan salah, antara lain bulan yang dihitung mulai dari nol dan objek yang isinya bisa berubah kapan saja. Sejak Java 8, penggantinya adalah java.time, yaitu API tanggal dan jam modern yang menjadi standar kode Java masa kini. Topik ini memperkenalkan LocalDate, LocalDateTime, Duration, Period, dan ZonedDateTime lewat contoh yang dapat dijalankan penuh.
Keluarga kelas java.time untuk tanggal dan jam#
java.time dibangun di atas kelas-kelas immutable (imutabel), yaitu objek yang nilainya tidak pernah berubah setelah dibuat. Metode seperti plusDays atau minusMonths tidak mengubah objek lamanya, melainkan mengembalikan objek baru berisi hasil perhitungannya. Sifat ini menguntungkan kode konkuren, karena satu objek tanggal yang dibagikan ke banyak thread tidak dapat dirusak oleh siapa pun. Tiga kelas inti yang paling sering dipakai adalah LocalDate untuk tanggal tanpa jam, LocalTime untuk jam tanpa tanggal, dan LocalDateTime untuk keduanya tanpa zona waktu. Semuanya dibuat dengan metode pabrik of yang menerima angka tanggal secara eksplisit.
Program berikut menyusun tenggat pekerjaan dari tanggal mulainya, mengujinya dengan pembanding isAfter, lalu memformat tanggal untuk pembaca manusia.
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class DemoWaktu {
public static void main(String[] args) {
LocalDate mulai = LocalDate.of(2026, 11, 2);
LocalDate tenggat = mulai.plusDays(21);
System.out.println("Mulai : " + mulai);
System.out.println("Tenggat : " + tenggat);
LocalDate serahTerima = LocalDate.of(2026, 11, 24);
System.out.println("Sudah lewat tenggat? " + serahTerima.isAfter(tenggat));
DateTimeFormatter pola = DateTimeFormatter.ofPattern("dd/MM/yyyy");
System.out.println("Diformat: " + tenggat.format(pola));
LocalDate dicari = LocalDate.of(2026, 11, 14);
System.out.println("14 Nov 2026 jatuh pada " + dicari.getDayOfWeek());
}
}Mulai : 2026-11-02
Tenggat : 2026-11-23
Sudah lewat tenggat? true
Diformat: 23/11/2026
14 Nov 2026 jatuh pada SATURDAYMetode of(2026, 11, 2) menghasilkan 2 November 2026 tanpa jebakan bulan karena angkanya ditulis apa adanya, tidak dihitung dari nol. Pemanggilan mulai.plusDays(21) mengembalikan objek baru bernilai 23 November 2026, sedangkan mulai tetap menunjuk 2 November, dan itulah alasan hasil penjumlahannya harus ditampung. Pemformat memakai pola dd/MM/yyyy yang isinya murni angka, sehingga keluarannya sama di mesin mana pun, sedangkan pola yang memuat nama hari atau nama bulan mengikuti pengaturan bahasa (locale) mesin yang menjalankan program.
Selisih waktu dengan Duration dan Period#
Dua kelas menangani selisih waktu dengan pembagian tugas yang jelas. Duration menghitung selisih berbasis waktu, yaitu jam, menit, dan detik, dan dipakai untuk LocalTime maupun LocalDateTime. Period menghitung selisih berbasis kalender, yaitu tahun, bulan, dan hari, dan dipakai untuk LocalDate. Pembagian ini penting karena panjang satu bulan tidak tetap dalam satuan detik, sehingga pernyataan “dua bulan” hanya bisa dijawab oleh kalender, bukan oleh hitungan detik.
Contoh berikut menghitung lama buka sebuah layanan dan masa kerja seseorang, lalu menurunkan tanggal pengingat dari operasi plus yang dirangkai.
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Period;
public class DemoSelisih {
public static void main(String[] args) {
LocalTime buka = LocalTime.of(8, 0);
LocalTime tutup = LocalTime.of(17, 30);
Duration lamaBuka = Duration.between(buka, tutup);
System.out.println("Lama buka : " + lamaBuka);
System.out.println("Dalam menit : " + lamaBuka.toMinutes());
LocalDateTime mulaiKerja = LocalDateTime.of(2024, 8, 5, 9, 0);
LocalDate perhitungan = LocalDate.of(2026, 11, 2);
Period masaKerja = Period.between(mulaiKerja.toLocalDate(), perhitungan);
System.out.println("Masa kerja : " + masaKerja.getYears() + " tahun "
+ masaKerja.getMonths() + " bulan " + masaKerja.getDays() + " hari");
LocalDate pengingat = perhitungan.plusMonths(1).plusDays(10);
System.out.println("Pengingat : " + pengingat);
}
}Lama buka : PT9H30M
Dalam menit : 570
Masa kerja : 2 tahun 2 bulan 28 hari
Pengingat : 2026-12-12Duration.between menjawab pertanyaan pertama dengan PT9H30M, yaitu notasi standar ISO-8601 untuk sembilan jam tiga puluh menit, dan metode toMinutes menerjemahkannya menjadi 570 menit. Untuk masa kerja, Period.between mengerjakan hal yang tidak bisa dijawab oleh detik. Selisih 5 Agustus 2024 sampai 2 November 2026 dipecah menjadi dua tahun, dua bulan, dan dua puluh delapan hari sesuai panjang bulan yang sebenarnya. Rangkaian plusMonths(1).plusDays(10) pada baris terakhir menunjukkan gaya berantai yang lazim dipakai karena setiap operasi mengembalikan objek siap pakai.
Waktu lintas zona dengan ZonedDateTime#
ZonedDateTime melengkapi tanggal dan jam dengan ZoneId, yaitu identitas zona waktu seperti Asia/Jakarta atau America/New_York. Konsep kuncinya adalah instant, yakni satu titik waktu absolut yang sama untuk seluruh dunia, sedangkan angka jam yang tampil bergantung pada zona yang memandangnya. Metode withZoneSameInstant memindahkan pandangan tanpa mengubah titik waktunya, sehingga wajar bila angka jamnya ikut bergeser.
Program berikut menjadwalkan rapat pada pukul 09:00 waktu Jakarta dan menampilkannya pada zona Tokyo, lalu memperlihatkan perilaku java.time ketika pergantian daylight saving time (DST) memakan satu jam di New York.
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class DemoZona {
public static void main(String[] args) {
ZonedDateTime rapatJakarta = LocalDateTime.of(2026, 11, 3, 9, 0)
.atZone(ZoneId.of("Asia/Jakarta"));
ZonedDateTime rapatTokyo = rapatJakarta.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
System.out.println("Rapat di Jakarta : " + rapatJakarta);
System.out.println("Rapat di Tokyo : " + rapatTokyo);
ZonedDateTime lalu = LocalDateTime.of(2026, 3, 8, 1, 30)
.atZone(ZoneId.of("America/New_York"));
ZonedDateTime sesudah = lalu.plusHours(1);
System.out.println("Sebelum pergantian DST : " + lalu);
System.out.println("Sesudah plus satu jam : " + sesudah);
}
}Rapat di Jakarta : 2026-11-03T09:00+07:00[Asia/Jakarta]
Rapat di Tokyo : 2026-11-03T11:00+09:00[Asia/Tokyo]
Sebelum pergantian DST : 2026-03-08T01:30-05:00[America/New_York]
Sesudah plus satu jam : 2026-03-08T03:30-04:00[America/New_York]Pukul 09:00 di Jakarta memang setara pukul 11:00 di Tokyo karena selisih dua jam, dan kedua baris itu menggambarkan satu titik waktu yang sama. Contoh kedua lebih menarik. Pada 8 Maret 2026, jam di New York digeser maju dari pukul 02:00 langsung menjadi 03:00, sehingga pukul 02:30 pada tanggal itu tidak pernah ada. Menambahkan satu jam pada 01:30 pun menghasilkan 03:30, dan perpindahan offset dari -05:00 ke -04:00 terlihat langsung pada keluarannya. Perhitungan semacam inilah yang dulu sering meleset di tangan Calendar.
Mengganti kode Date dan Calendar#
Migrasi dari kelas lama menjadi mudah bila setiap kebutuhan dipetakan ke padanannya, sebagaimana tabel berikut.
| Kebutuhan | Gaya lama | Gaya java.time |
|---|---|---|
| Tanggal hari ini | new Date() | LocalDate.now() |
| Tanggal tertentu | new GregorianCalendar(2026, Calendar.NOVEMBER, 2) | LocalDate.of(2026, 11, 2) |
| Menambah 21 hari | cal.add(Calendar.DAY_OF_MONTH, 21) | tenggat = mulai.plusDays(21) |
| Selisih dua tanggal | hitung manual dalam milidetik | Period.between(a, b) |
| Format untuk pembaca | SimpleDateFormat | DateTimeFormatter |
Potongan berikut memperlihatkan gaya lama yang menjadi sumber keluhan, ditulis hanya agar bentuknya dapat dikenali.
// tidak-dijalankan
Date dulu = new GregorianCalendar(2026, Calendar.NOVEMBER, 2).getTime();
// menambah 21 hari lewat milidetik pada objek yang sama
dulu.setTime(dulu.getTime() + 21L * 24 * 60 * 60 * 1000);Dua keluhan utama terlihat jelas pada potongan itu. Pertama, angka bulan pada Calendar dihitung dari nol, sehingga pemanggilan new GregorianCalendar(2026, 11, 2) yang terlihat seperti November sebenarnya jatuh pada Desember. Kedua, setTime mengubah objek yang sudah ada, sehingga kode lain yang masih memegang referensinya mendadak membaca tanggal yang sudah berubah. Kelas SimpleDateFormat menambah satu masalah karena tidak aman dipakai bersama thread, sedangkan DateTimeFormatter aman karena imutabel seperti kelas tanggalnya.
Kesalahan umum#
- Mengabaikan nilai kembalian operasi tanggal. Karena kelas
java.timeimutabel, pemanggilanmulai.plusDays(21)tanpa penugasan tidak mengubah apa pun. Tangkap hasilnya, misalnyatenggat = mulai.plusDays(21). - Memakai
Duration.betweenuntuk duaLocalDate. Kelas itu bekerja pada satuan waktu, sehingga pemanggilannya pada tanggal murni melemparUnsupportedTemporalTypeException. GunakanPeriod.betweenuntukLocalDate. - Membuat
ZonedDateTimedarinow()tanpa zona eksplisit. JVM memakai zona default mesin, sehingga hasilnya berbeda antar mesin dan programnya sulit diuji. SebutkanZoneIdsecara eksplisit pada kode yang peduli zona. - Salah membaca huruf pola
DateTimeFormatter. Huruf pola punya arti tetap, misalnyaMMuntuk bulan dua digit danmmuntuk menit, sehingga pola yang tertukar menghasilkan angka yang salah pula tanpa pesan error.
Ringkasan#
- Kelas
java.timeimutabel, sehingga setiap operasi mengembalikan objek baru dan objeknya aman dibagikan antar-thread. LocalDate,LocalTime, danLocalDateTimemenampung tanggal dan jam tanpa zona, dibuat denganofdan diubah lewatplussertaminus.Durationmenghitung selisih berbasis waktu, sedangkanPeriodmenghitung selisih berbasis kalender.ZonedDateTimemelengkapinya denganZoneId, danwithZoneSameInstantmemindahkan pandangan zona tanpa mengubah titik waktunya.
Referensi#
- java.time package summary — Oracle
— dokumentasi resmi paket
java.timebeserta prinsip desainnya. - Trail: Date Time — Oracle
— tutorial resmi pemakaian
LocalDate,ZonedDateTime,Duration, danPeriod.