author-pic

Ferry S

An ISTJ, Type 5, Engineer, Gamer, and Thriller-Movies-Lover
Hijrah dari Java Date & Calendar ke Java 8 Time (JSR-310)
Sun. Feb 7th, 2021 10:08 PM6 mins read
Hijrah dari Java Date & Calendar ke Java 8 Time (JSR-310)
Source: Freepik - Premium Vector | Time management concept

Sejak Java 8 dirilis, gw mulai jarang menggunakan API java.util.Date dan java.util.Calendar karena Java 8 telah mengadopsi Date/Time API dari JodaTime yang memiliki kode fitur JSR-310. API ini mengatasi berbagai permasalahan dari API lama. Meskipun ini dirilis beberapa tahun lalu berbarengan dengan rilisnya Java 8, masih banyak mungkin yang belum kenal atau belum terbiasa menggunakannya. Beberapa framework atau library juga masih full support Date/Calendar dari library lama, makanya sebagian orang enggan untuk beralih ke API yang baru. Walaupun sekarang Spring dan Hibernate versi terbaru udah mulai support JSR-310.

Ada beberapa alasan yang menjadi latar belakang terwujudnya fitur JSR-310 ini. Pertama, tentang immutability. Date dan Calendar tidak seperti String, Integer, Boolean, dan lainnya yang immutable. Objek ini jadi ga thread-safe. Sangat tidak aman jika digunakan sebagai constant. Datanya bisa tidak konsisten, karena state dari objek tersebut bisa diubah di sembarang tempat. Contohnya seperti ini:

private static void mutableDate(){
	Date date = new Date();
	System.out.println("date = " + date);
	doSomething(date);
   //date object will be different
	System.out.println("date = " + date);
}

private static void doSomething(Date date){
	date.setTime(1000000);
}

Sedangkan jika kita menggunakan JSR-310, objeknya konsisten dan sudah pasti thread-safe sehingga aman banget jika dijadikan constant. Setiap perubahan akan menghasilkan objek baru sehingga objek asli nilainya tetap. Contohnya seperti ini:

private static void immutable(){
	LocalDateTime date = LocalDateTime.now();
	System.out.println("date = " + date);
	doSomething(date);
   //date object never change
	System.out.println("date = " + date);
}

private static void doSomething(LocalDateTime date){
	date.withYear(2000);
  //this created new date object
  //no side effect to existing object
}

Salah satu alasan paling kuat gw migrasi dari Date dan Calendar adalah API-nya yang rumit. Misalkan ketika kita ingin mengambil nilai dari 1 tahun 2 bulan dan 7 hari dari sekarang.

private static void complex(){
	Calendar c = Calendar.getInstance();
	c.add(Calendar.DATE, 7);
	c.add(Calendar.MONTH, 2);
	c.add(Calendar.YEAR, 1);
	Date time = c.getTime();
	System.out.println("time = " + time);
}

Rumit banget🤯. Pada JSR-310 API-nya selain lebih lengkap juga lebih simple, mudah dimengerti, dan method-nya chainable. Sekarang perhatikan LocalDateTime berikut:

private static void simple(){
	LocalDateTime localDateTime = LocalDateTime.now().plusDays(7).plusMonths(2).plusYears(1);
	System.out.println("localDateTime = " + localDateTime);
}

Dari awal rilis, Class Date ini udah banyak deprecated method-nya. Date sejatinya hanya menyimpan milliseconds dari UTC berdasarkan EPOCH (dimulai dari 1 Januari 1970 pukul 00:00). Sedangkan pada JSR-310 lebih spesifik menyimpan value sesuai tugas dari masing-masing objek. Calendar yang diciptakan untuk menutupi kekurangan Date juga ga lebih baik. Beberapa method di Calendar menggunakan magic number, bukan enum sehingga membingungkan. Date dan Calendar hanya presisi hingga milliseconds, sedangkan JSR-310 support hingga nanoseconds. Date dan Calendar berbasis inheritance, sedangkan JSR-310 berbasis strategy pattern jadi lebih fleksibel.

Difference between Legacy Date/Time with Modern
Difference between Legacy Date/Time with Modern Date/Time

Sekarang lupakan Date & Calendar dan fokus ke JSR-310. Di sini, objeknya dipecah menjadi lebih spesifik sesuai responsibility dan fungsi yang dibutuhkan. Diantaranya:

  • Instant. Sama seperti Date, menyimpan EPOCH dari UTC, tapi objeknya immutable, API-nya lebih clean dan support hingga nanoseconds;
  • LocalDate. Hanya menyimpan value tentang tanggal, bulan dan tahun saja tanpa waktu dan zona;
  • LocalDateTime. Gabungan LocalDate dan LocalTime, menyimpan tanggal, bulan, tahun, jam, menit, detik, dan nanoseconds. Tapi tanpa zona;
  • LocalTime. Hanya menyimpan value tentang jam, menit, detik, hingga nanoseconds tanpa penanggalan;
  • OffsetDateTime. Sama seperti LocalDateTime, menyimpan tanggal, bulan, tahun, jam, menit, detik, dan nanoseconds. Tapi include offset zona (selisih waktu dari UTC, antara -18 jam hingga +18 jam dari UTC) tanpa informasi zona wilayah;
  • ZonedDateTime. Sama seperti OffsetDateTime, menyimpan tanggal, bulan, tahun, jam, menit, detik, nanoseconds dan offset zona. Tapi include informasi zona wilayah (Asia/Bangkok, Asia/Tokyo, UTC, dll);
  • OffsetTime. Sama seperti LocalTime, Hanya menyimpan value tentang jam, menit, detik, hingga nanoseconds tanpa penanggalan. Tapi include zona waktu dalam bentuk offset (selisih waktu dari UTC);
  • MonthDay. Hanya menyimpan bulan dan tanggal saja;
  • YearMonth. Hanya menyimpan tahun dan bulan saja;
  • Year. Hanya menyimpan value tentang tahun;
  • Month. Enumeration nama-nama bulan;
  • DayOfWeek. Enumeration nama-nama hari;
  • ZoneId. Menyimpan value zona wilayah (Asia/Jakarta, UTC, Europe/Berlin, dll);
  • ZoneOffset. Menyimpan value zona dalam bentuk selisih waktu (+7, -3, dll);
  • Duration. Pengukur selisih waktu dalam satuan detik hingga nanoseconds;
  • Period. Pengukur selisih tanggal dalam satuan hari, bulan, dan tahun;
  • DateTimeFormatter. Pengganti legacy SimpleDateFormat untuk menentukan format waktu. Tapi immutable dan throw RuntimeException;

Untuk bikin objek terdapat static factory method pada masing-masing objek:

private static void instance(){
	LocalDate date = LocalDate.now();
	ZonedDateTime dateTime = ZonedDateTime.now();
	Year year = Year.now();
}

Masing-masing objek bisa dikonversi ke objek terdekatnya melalui beberapa method yang sudah built-in dengan menambahkan value yang required pada objek yang ingin dikonversi:

private static void convert(){
	LocalDate date = LocalDate.now();
	//convert date to LocalDateTime at time 00:00:00
	LocalDateTime localDateTime = date.atStartOfDay();
	//convert localDateTime to offsetDateTime with offset time +9 hours
	OffsetDateTime offsetDateTime = localDateTime.atOffset(ZoneOffset.ofHours(9));
}

Konversi objek juga bisa menggunakan static method from() dari objek yang akan dikonversi dengan syarat objek tersebut minimal memiliki value yang required pada objek yang dikonversi. Contohnya LocalDate tidak bisa dikonversi ke LocalDateTime menggunakan method from() karena LocalDate tidak memiliki value time. Untuk hal ini, hanya bisa dikonversi melalui built-in method dari LocalDate seperti cara sebelumnya. Sedangkan LocalDateTime bisa dikonversi menjadi LocalDate menggunakan method from() karena LocalDate hanya membutuhkan tanggal saja dan LocalDateTime include value tanggal di dalamnya.

private static void from(){
	LocalDateTime localDateTime = LocalDateTime.now();
	LocalDate localDate = LocalDate.from(localDateTime);
	System.out.println("localDate = " + localDate);
}

Untuk konversi objek menjadi String dengan pattern tertentu bisa menggunakan DateTimeFormatter:

private static void format(){
	String pattern = "EEEE, dd MMMM yyyy";
	LocalDate localDate = LocalDate.now();
	String format = DateTimeFormatter.ofPattern(pattern).format(localDate);
	System.out.println("format = " + format);
}

Sebaliknya, untuk konversi tanggal dari bentuk String dengan format tertentu menjadi sebuah objek JSR-310 juga menggunakan DateTimeFormatter:

private static void parse(){
	String pattern = "EEEE, dd MMMM yyyy";
	String dateString = "Sunday, 07 February 2021";
	LocalDate localDate = LocalDate.parse(dateString, DateTimeFormatter.ofPattern(pattern));
	System.out.println("accessor = " + localDate);
}

Untuk mencari selisih waktu antara dua waktu bisa menggunakan berbagai cara. Pertama bisa menggunakan Period untuk menghitung tanggal saja, atau Duration untuk menghitung waktu yang lebih presisi.

private static void staticRange(){
	LocalDate december2nd = LocalDate.of(2020, Month.DECEMBER, 2);
	LocalDate now = LocalDate.now();
	Period period = Period.between(december2nd, now);
	System.out.println("period between date is = " + period.getYears() + " years " + period.getMonths() + " " +
			"months " + period.getDays() + " days");

	LocalTime lastHour = LocalTime.now().minusHours(1).plusNanos(20);
	LocalDateTime dateTime = LocalDateTime.now();
	Duration between = Duration.between(lastHour, dateTime);
	System.out.println("duration between time is = " + between.getSeconds() + " seconds " +
			between.getNano() + " nano");
}

Selain menggunakan static factory dari Period dan Duration, mencari selisih waktu juga bisa langsung menggunakan instance method yang udah built-in.

private static void builtInRange(){
	LocalDate today = LocalDate.now();
	LocalDate yesterday = LocalDate.now().minusDays(1);
	Period period = yesterday.until(today);
	System.out.println("period between today and yesterday is = " + period.getDays() + " days");

	LocalDateTime lastYear = LocalDateTime.now().minusYears(1);
	LocalDateTime now = LocalDateTime.now();
	long until = lastYear.until(now, ChronoUnit.DAYS);
	System.out.println("range between last year and now is = " + until + " days");
}

Untuk mencari selisih waktu yang lebih spesifik dan lebih lengkap, bisa menggunakan enum ChronoUnit.

private static void rangeInChronoUnit(){
	LocalDate december2nd = LocalDate.of(2020, Month.DECEMBER, 2);
	LocalDate today = LocalDate.now();
	long rangeInDays = ChronoUnit.DAYS.between(december2nd, today);
	System.out.println("range between december2nd until today is = " + rangeInDays + " days");

	LocalDateTime dateTime = LocalDateTime.now();
	LocalDateTime lastMonth = LocalDateTime.now().minusMonths(1);
	long rangeInWeeks = ChronoUnit.WEEKS.between(lastMonth, dateTime);
	System.out.println("range between lastmonth until today time is = " + rangeInWeeks + " weeks");
}

Kelemahannya sih ga terlalu besar menurut gw, diantaranya:

  • Cukup banyak objek yang dipelajari. Karena objeknya dipecah-pecah menjadi lebih spesifik. Tapi di lain sisi malah jadi poin plus karena Single Responsibility;
  • Resiko RuntimeException jika pemakaiannya kurang tepat. Ada beberapa RuntimeException yang terjadi jika menggunakan pendekatan yang salah. Seperti konversi LocalDate menjadi LocalDateTime melalui method LocalDateTime.from(), atau memberikan pattern tanggal yang tidak sesuai dengan objek yang diinginkan, misalnya menggunakan pattern "hh:mm" pada LocalDate karena LocalDate tidak memiliki value waktu. Jika memahami JSR-310 harusnya sih bisa dihindari;

Secara umum itulah fitur-fitur yang paling sering digunakan. Untuk lebih detailnya, bisa dipraktekan sendiri-sendiri, karena terlalu banyak jika harus dikupas semuanya. Selamat mencoba🤓.