Working With Time in Java

A Brief History of Time in Java

JDK 1.0 introduced the Date API as part of java.util.date. The date API was always weird. For example, let's look at this particular Date API:-

Date date = new Date(2014 /* year */, 1 /* month */, 1 /* day */ );

By looking at the API, you might think since the Date constructor takes year, month and day that this call represents 2014-Jan-1. But this represents 3914-Feb-1! The rules for this constructor are as follows:

  • The integer years parameter is an offset from 1900. So, if you wanted 2014, you need to give 2014-1900, which is 114.
  • The month is 0 indexed. So, months are represented using 0-11.
  • The day parameter works as expected.

Everyone recognized this API was bad. In fact, it was deprecated and replaced just the next year, as part of JDK 1.1, with the Calendar API!


The Calendar class fixed many of these issues. It gave constants to all the Month's and Days of the week. It handled timezones and locales. So, to create a new Calendar object with 2014-Jan-1, you can now do

Calendar cal = Calendar.getInstance();
cal.set(2014, Calendar.JANUARY, 1);

But it still had many issues:

  • The name implied that you get a Calendar, but you get an instant in Date and Time. This may be a minor nit for some, but the name just does not make sense.

  • Even though constants where introduced, the months are still 0 indexed, and it is extremely easy to give the wrong index when constructing the Calendar instance.

  • Common calculations like the difference between days cannot be easily done. You must convert the values into milliseconds calculate the difference and then convert the values back into days. And this has its own pitfalls as the user has to use the DST aware time zones. A complete function will look something like this:

// FROM https://stackoverflow.com/a/31800947/4468788
import java.util.concurrent.TimeUnit;
/**
* Compute the number of calendar days between two Calendar objects.
* The desired value is the number of days of the month between the
* two Calendars, not the number of milliseconds' worth of days.
* @param startCal The earlier calendar
* @param endCal The later calendar
* @return the number of calendar days of the month between startCal and endCal
*/
public static long calendarDaysBetween(Calendar startCal, Calendar endCal) {
// Create copies so we don't update the original calendars.
Calendar start = Calendar.getInstance();
start.setTimeZone(startCal.getTimeZone());
start.setTimeInMillis(startCal.getTimeInMillis());
Calendar end = Calendar.getInstance();
end.setTimeZone(endCal.getTimeZone());
end.setTimeInMillis(endCal.getTimeInMillis());
// Set the copies to be at midnight, but keep the day information.
start.set(Calendar.HOUR_OF_DAY, 0);
start.set(Calendar.MINUTE, 0);
start.set(Calendar.SECOND, 0);
start.set(Calendar.MILLISECOND, 0);
end.set(Calendar.HOUR_OF_DAY, 0);
end.set(Calendar.MINUTE, 0);
end.set(Calendar.SECOND, 0);
end.set(Calendar.MILLISECOND, 0);
// At this point, each calendar is set to midnight on
// their respective days. Now use TimeUnit.MILLISECONDS to
// compute the number of full days between the two of them.
return TimeUnit.MILLISECONDS.toDays(
Math.abs(end.getTimeInMillis() - start.getTimeInMillis()));
}
  • The calendar class is mutable, which means these classes are not thread safe.

  • java.text.DateFormat was introduced for parsing date strings, but it is not thread safe. For example:

// FROM https://java-8-tips.readthedocs.io/en/stable/datetime.html
SimpleDateFormat sdf = new SimpleDateFormat("ddMMyyyy");
ExecutorService es = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
es.submit(() -> {
try {
System.out.println(sdf.parse("15081947"));
} catch (ParseException e) {
e.printStackTrace();
}
});
}
es.shutdown();
Output:
-------
Fri Aug 15 00:00:00 IST 1947
Mon Aug 11 00:00:00 IST 1947
Fri Aug 15 00:00:00 IST 1947
Fri Aug 15 00:00:00 IST 1947

So 7 years after the release of JDK 1.1, in Java 8 new java.time package was introduced to replace Date and Calendar.

Date is Dead, Long Live DateTime!

The ThreeTen project was created in 2014 to introduce a new Date and Time API in Java. This new project was championed by the author of Joda-Time, a popular DateTime library that was used prior to Java 8. This project introduced the java.time package into Java 8.


This provided:-

  • java.time - The main package to work with dates, times and instants.
  • java.time.chrono - API to work with other calendar systems like the Japanese calendar.
  • java.time.format - API to print and parse date and times.
  • java.time.temporal - Gives access to date and time fileds (like Hours, Days etc) and date time adjustors (firstDayOfMonth()).
  • java.time.zone - Support for time-zones and their rules.

As a developer, you would normally interact with the java.time package.

Clock

The Clock class provides access to the current instant. You can picture it as a normal wall clock, whose clock hands are always ticking. All the other classes in the package provide an instant of this date. You can imagine LocalDateTime as you are clicking a picture of that clock. The value of the time at that instant is stored in the class.


This class is particularly useful for testing. You can create a clock this is fixed to a particular date (think of it as a broken clock). With this clock, you can always get the same Date using the DateTime classes.


You can find the example in the tips section.

Instant

The Instant class is an instantaneous point on the time-line. You can imagine it as the number of epoch seconds that have passed when the picture of the clock was taken.

LocalDate / LocalTime / LocalDateTime

The LocalDate / LocalTime / LocalDateTime classes provide access to date time without time zone information.


This class is useful for storing a description of date, rather than the date itself. For example, you could do something like this:

final LocalDateTime firstDayOf2014 = LocalDateTime.of(2014, Month.JANUARY, 1, 0, 0);

The above describes a date that is the first day of 2014. But to know whether the time right now is greater than that date, we would need time zone information. Another example is birthdays. They always occur on the same date, but we would only want to show a "Happy Birthday!" message if that is the date in the user's time zone.

ZonedDateTime

The ZonedDateTime class provides the date and time with a time zone. It is aware of things like Daylight Saving Time (DST) and other anomalies in time zone rules like that. So, if you want to calculate tomorrow's date time in US Central Time, you should use this class.

OffsetTime / OffsetDateTime

The OffsetTime and OffsetDateTime classes provide the instant of time, as well as the offset from UTC. This class is useful when communicating over the network or to a database. You can (and should) convert this to ZonedDateTime if you are doing any time zone sensitive calculations.

Period / Duration

The Period class represents a date-based amount of time. If you want to add '1 year, 5 months and 3 days' to a date, you should make use of the Period API.


The Duration class represents quantity of time in seconds and nanoseconds. You can use this to add '500 seconds' to a time for example.

Tips and Tricks

Usage with JPA / Hibernate

When working with JPA / Hibernate, prefer the Local based classes if you need date time without time zone and Offset based classes if you need date time with time zone.


You can also use ZonedDateTime and Duration / Instant based classes, but this works only with Hibernate and can be dangerous. For a full explanation go here.

Testing

Be incredibly careful when using the .now() method as this can affect testability. For example

public class MyAwesomeDateUtilities {
private MyAwesomeDateUtilities() {
}
public static LocalDateTime get10DaysBeforeNow() {
return LocalDateTime.now().minusDays(10);
}
}

To write a test for this, you would have to do something like

public class MyAwesomeDateUtilitiesTest {
@Test
public void testGetLocalDateTimeFrom10DaysBefore() {
LocalDateTime nowMinus10 = LocalDateTime.now().minusDays(10);
Assert.assertTrue(nowMinus10.equals(MyAwesomeDateUtilities.get10DaysBeforeNow()));
}
}

This is a awfully bad test case. Why? The test code and the actual implementation is the same! If something were wrong with the implementation in the first place it would never have been caught.


Instead, you should convert your class to something along the lines of this:

public class MyAwesomeDateUtilities {
private Clock clock;
public MyAwesomeDateUtilities(Clock clock) {
this.clock = clock;
}
public LocalDateTime get10DaysBeforeNow() {
return LocalDateTime.now(clock).minusDays(10);
}
}

Now you can test like so

public class MyAwesomeDateUtilitiesTest {
@Test
public void testGetLocalDateTimeFrom10DaysBefore() {
final Clock clock = Clock.fixed(Instant.parse("2014-12-22T10:15:30.00Z"), ZoneId.of("UTC"));
final MyAwesomeDateUtilities util = new MyAwesomeDateUtilities(clock);
LocalDateTime nowMinus10 = LocalDateTime.parse("2014-12-12T10:15:30.00Z");
Assert.assertTrue(nowMinus10.equals(MyAwesomeDateUtilities.get10DaysBeforeNow()));
}
}

Much better!

Conclusion

The issue that I see is that unfortunately java.util.Date is what shows up in autocomplete when you start typing out Date in Java IDE's. This leads to Date being used even though it should not be. The key takeaway from this post is avoid Date and Calendar APIs religiously. They lead to a class of bugs that are not obvious at first glance.