Peter Chng

Java Date and Time APIs

For Java, the third time was a charm with the java.time APIs. This was the third attempt at making a proper set of Date and Time APIs in the Java SDK, and they finally got it right. (The work was done under JSR 310). The APIs under java.time are now the preferred way to work with date and time values in Java, and should be used whenever possible.

Java 8, released back in 2014, was the first version of Java to include java.time. Before then, the date and time APIs in the SDK left much to be desired. As is the case when a standard library leaves a gap, the community usually finds a solution. That solution came in the form of Joda-Time, a third party library that became a de facto standard by virtue of its widespread adoption. (The original author of Joda-Time was also the specification lead for JSR-310) Still, you can find much older code that uses neither java.time nor Joda-Time, and instead uses the older and convoluted Java date/time APIs.

The TL;DR of this article is that if you need to work with dates and times, you should use the APIs in the java.time package. An acceptable alternative might be Joda-Time - if, for example, an existing code base is heavily using Joda-Time and you want to be consistent - though it should be noted that even Joda-Time recommends migrating to java.time. You should avoid using the older Java date/time APIs - namely java.util.Date and java.util.Calendar - since they have numerous drawbacks and are easy to misuse.

The current state of things

If you only started programming in Java after Java 8, you might not realize what all the fuss is about over Date/Time APIs. After all, java.time is pretty sensible and mostly works. However, this ignores all the blood, sweat, and tears (well, maybe not blood) that were shed to get to this point. (This is sort of like how younger web/frontend developers may not realize how crazy it was to develop for browsers in the 2000s)

The Javadoc for the java.time package explains the main concepts behind the API, and I recommend understanding these fundamentals before using the API to build anything non-trivial.

Here are a few examples of using java.time APIs to work with dates and times:

Parse a date from a String:

Parsing a date (or date-time) from a String is probably one of the most common operations you’ll have to do, so seeing how easy it is with an API is a good measure of how well designed that API is.

You start off by constructing an instance of a DateTimeFormatter, usually using a pattern string, the details of which are defined in the Javadoc. For example, we can parse days like 11/30/2019 (as in the style common in the USA) using the following:

public static void dateParsingExample() {
    // The default `ResolverStyle` is `ResolverStyle.SMART`, which has some implications outlined below.
    // You should always aim to specify the Locale, because the default Locale of the system might not be what
    // you expect. Here, it does not matter, but if we were using month names (e.g. "November"), it would.
    final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("MM/dd/yyyy", Locale.US);

    // Works as expected.
    System.out.println(LocalDate.parse("11/30/2019", dateTimeFormatter));

    // 11/31/2019 is not a valid date, however this DOES NOT FAIL. Instead, it gets converted/corrected to 11/30/2019!
    // However, 11/32/2019 would fail with an exception, because "32" is never valid as a day-of-month value.
    System.out.println(LocalDate.parse("11/31/2019", dateTimeFormatter));
}

As you can see from the example above, it mostly works as expected, with the sole exception being that dates like “11/31/2019” (not a valid date, since November has only 30 days) get auto-corrected/converted to be “11/30/2019”. This is because the default ResolverStyle is SMART, which does this correction. This was actually surprising to me, and is questionable behaviour in my opinion.

If you want to prevent this, you should use ResolverStyle.STRICT, but that has a few more implications, as described below.

public static void dateParsingExampleWithStrict() {
    // With `ResolverStyle.STRICT`, you must use "uuuu" since "yyyy" means "year-of-era", which does not map to a LocalDate field!
    // Only "u", meaning "year", maps to the LocalDate field.
    // With the default of SMART, it seems that "year-of-era" can be interpreted as "year".
    final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("MM/dd/uuuu", Locale.US).withResolverStyle(ResolverStyle.STRICT);

    // Works as expected.
    System.out.println(LocalDate.parse("11/30/2019", dateTimeFormatter));

    // 11/31/2019 is not a valid date; `ResolverStyle.STRICT` will trigger an exception on parsing:
    // Caused by: java.time.DateTimeException: Invalid date 'NOVEMBER 31'
    System.out.println(LocalDate.parse("11/31/2019", dateTimeFormatter));
}

For many common date formats (like “YYYY-MM-DD”), there are pre-defined formatters such as DateTimeFormatter.ISO_LOCAL_DATE; these use a STRICT resolver style.

DateTimeFormatter is explicitly defined to be immutable and thus thread-safe.

Convert between time zones

Converting between time zone is probably another frequent operation, and a good date-time API should be able to handle it easily and without error.

public static void timeZoneConversionExample() {
    // First example:
    // Midnight on Feb. 28th, 2020, in Los Angeles (Pacific Time Zone)
    final ZonedDateTime dateTimeInPacific = ZonedDateTime.of(2020, 2, 28, 0, 0, 0, 0, ZoneId.of("America/Los_Angeles"));

    // What is the time in New York at the same instant?
    final ZonedDateTime dateTimeInEastern = dateTimeInPacific.withZoneSameInstant(ZoneId.of("America/New_York"));

    // Prints: 2020-02-28T00:00-08:00[America/Los_Angeles] is equivalent to 2020-02-28T03:00-05:00[America/New_York]
    System.out.println(String.format("%s is equivalent to %s", dateTimeInPacific, dateTimeInEastern));

    // Second example:
    // A more interesting example: Converting from a time in a time zone that hasn't begun DST yet to a time zone
    // that has already entered DST.
    // Reminder: DST begins on 2020-03-08 at 2 AM local time!

    // March 8th, 2020 at 1 AM in Los Angeles.
    final ZonedDateTime dateTimeInPacificNoDst = ZonedDateTime.of(2020, 3, 8, 1, 0, 0, 0, ZoneId.of("America/Los_Angeles"));
    final ZonedDateTime dateTimeInEasternDst = dateTimeInPacificNoDst.withZoneSameInstant(ZoneId.of("America/New_York"));

    // Prints: 2020-03-08T01:00-08:00[America/Los_Angeles] is equivalent to 2020-03-08T05:00-04:00[America/New_York]
    // So, at 1 AM in LA, it actually 5 AM, or four hours ahead, for a brief period of time before DST kicks in for the Pacific time zone!
    System.out.println(String.format("%s is equivalent to %s", dateTimeInPacificNoDst, dateTimeInEasternDst));
}

Converting between time zones is simple and doesn’t require you to memorize offsets or take into account DST - that is done for you.

The second example accounts for the fact that DST adjustments always occur in the local time zone, so the DST adjustments do not occur at the same instant in time. For example, in the spring, the clocks go from 1:59:59 AM to 3:00:00 AM - the hour of 2 AM doesn’t happen! Because of this “staggered adjustment”, the difference between time zones is temporarily different than the norm, i.e. during the day that the DST switch happens, at 1 AM in Los Angeles, the time in New York is already 5 AM - a difference of four hours instead of the normal three. (The difference will correct after one hour)

Calculate the duration between two points in time:

public static void durationExample() {
    // Midnight on Feb. 28th, 2020, in Los Angeles (Pacific Time Zone)
    final ZonedDateTime start = ZonedDateTime.of(2020, 2, 28, 0, 0, 0, 0, ZoneId.of("America/Los_Angeles"));
    // Midnight on March 10th, 2020, in the same time zone.
    final ZonedDateTime end = ZonedDateTime.of(2020, 3, 10, 0, 0, 0, 0, ZoneId.of("America/Los_Angeles"));

    // Because 2020 is a leap year, there is an extra day, so it would seem there are 11 complete days between the start and end datetimes.
    // However there is also the beginning of Daylight Saving Time on March 8th, which reduces the duration by 1 hour.
    // The end result is that this duration is 263 hours exactly.
    final Duration duration = Duration.between(start, end);

    System.out.println(duration.toDays());      // Only 10 complete days
    System.out.println(duration.toHours());     // 263 hours = 10 full days and 23 hours
    System.out.println(duration.toSeconds());   // 946800 = 263*60*60
}

A few things to note here:

  1. Leap years are automatically taken into account, so the extra day of Feb. 29th isn’t missed.
  2. When using geographic ZoneId values (from the Time Zone Database) to represent the time zone (rather than fixed-offsets), these take into account whether that geographic region observes DST, and hence any DST changes are also factored into the duration calculation. (You should read the APIs to confirm which operations involve or take into account DST, and which do not)

The great thing about java.time APIs is that you generally don’t have to worry about these edge cases if you use the API correctly. You should just rely on the APIs there instead of trying to manually do any date or time calculations yourself.

Add a certain number of days to a date:

Somewhat similarly, we can add a certain number of days to a date and get the result:

public static void addDurationExample() {
    final LocalDate start = LocalDate.of(2020, 2, 28);
    final int daysToAdd = 11;
    final LocalDate end = start.plusDays(daysToAdd);

    // Shows: "2020-02-28 plus 11 days is 2020-03-10"
    System.out.println(String.format("%s plus %s days is %s", start, daysToAdd, end));
}

Get the date of all the Mondays in 2020:

// NOTE: This assumes an ISO-8601 Calendar, which is the default for java.time APIs.
public static void allMondaysExample() {
    final int year = 2020;
    // We'll have at most 53 Mondays in any given year.
    final List<LocalDate> mondays = new ArrayList<>(53);

    // Find the first Monday of the year.
    // Perhaps not the most efficient way, but maybe the most straightforward.
    LocalDate monday = LocalDate.of(year, 1, 1);
    while (monday.getDayOfWeek() != DayOfWeek.MONDAY) {
        monday = monday.plusDays(1);
    }

    // Add all Mondays to the list.
    while (monday.getYear() == year) {
        mondays.add(monday);
        // There are 7 days in a week in the default ISO-8601 Calendar. By using the DayOfWeek enum, we're
        // implicitly using this calendar system or chronology.
        monday = monday.plusDays(7);
    }

    for (final LocalDate day : mondays) {
        System.out.println(day);
    }
    System.out.println(String.format("There are %s Mondays in %s", mondays.size(), year));
}

Lastly, pretty much all the classes you’ll work with are immutable and thus thread-safe. For example, when we used start.plusDays(daysToAdd) above, it didn’t mutate the original instance, but instead returned a new instance with the additional days added.

It wasn’t always this way

The first version of any date API in Java was in Java 1.0, with java.util.Date. (You also had System.currentTimeMillis(), which returned the Unix Epoch time in milliseconds). Despite its name, it actually represented a date-time. This single class provided support for getting the year, month, day, hour, minute and second values from its internal representation of a date-time, and also provided the capability to parse a date-time value from a String.

Unfortunately, it did all of this only considering English names for months/days, and so didn’t really work with internationalization. (What if your calendar was French?) It also tried to pack the ability to parse a variety of date-time formats into a single method, which probably made that method quite complicated. The end result was that almost all of the API was deprecated in Java 1.1; whatever remains, you probably still should not use. (Furthermore, Date is mutable, and thus generally not thread-safe own its own)

The second version of a date API was released in Java 1.1, with java.util.Calendar. This fixed some issues, but didn’t address others - namely January is represented by 0 instead of 1, and so December is 11 instead of 12. I also found using the Calendar API to be overly verbose and non-intuitive. Furthermore, in the JDBC part of the JDK, classes like java.sql.Date, java.sql.Time, and java.sql.Timestamp extended the previous java.util.Date class. This was a highly questionable design decision, as something like java.sql.Time doesn’t carry any date information, and so the methods on java.util.Date that were inherited wouldn’t really apply.

Date formatting was also introduced with SimpleDateFormat (and its parent abstract class, DateFormat) in Java 1.1, but this class had some serious gotchas, namely that it was not thread-safe, because the class stored intermediate results in its instance fields. So, if you used a single instance of SimpleDateFormat across multiple threads, the parse() calls could interleave with one another, generating unpredictable results.

This combination of unintuitive APIs and classes that were not thread-safe has probably led to a countless number of bugs when using these APIs.

Java 1.1 was released way back in Feburary 1997. It wasn’t until March 2014, with the release of Java 8, that we finally got a (mostly) sensible set of Date and Time APIs in the Java standard library. Up until then, as I mentioned previously, Joda-Time was more-or-less a de facto standard and one used in any project where you needed to work with date-time values.

In any new code you write, you should avoid using java.util.Date or java.util.Calendar, and stick with java.time APIs. If you have to convert from the old APIs to new APIs, the Javadoc at java.time states:

Instant is the closest equivalent class to java.util.Date. ZonedDateTime is the closest equivalent class to java.util.GregorianCalendar.

Date and Time are inherently complicated

A date or time is one of those things that seems simple, because we encounter it all the time in everyday life, but upon further inspection is not.

For example, consider perhaps the foundation of time, the unit of time known as the second. What exactly is a “second”? There are two ways to think about this:

  1. Define it in terms of a day: A day has 86400 seconds, so a second is simply 1/86400th of a day.
    (Here, a day means “the duration of time between when the Sun casts shadows perfectly north-south until the point in time when it does so again”. This itself is complicated)
  2. Define it as some fundamental constant: A second is a precise length of time that is about equal to 1/86400th of most days.
    This is the definition that atomic clocks use.

The two definitions are not equivalent because the length of a day varies and overall is getting longer. Historically, we mostly used definition (1), but in modern times the “second” has been defined like in (2). The Javadoc on java.time.Instant goes into more detail about these differences. These differences are the reason why we occasionally need extra seconds, called leap seconds, on certain days.

To avoid complexities regarding leap seconds (they aren’t predictable far into the future), the APIs use their own Java Time-Scale to define what a “second” means. This is complicated, but it basically boils down to a leap second being spread equally over the last 1000 seconds of the day on which a leap second occurs, rather than being a whole extra second that occurs at 23:59:60. This is similar (but not identical) to how Google “smears” leap seconds on their NTP servers. This is a simplification so that we never have to actually observe a leap second, because doing so has caused tremendous issues, most publically in 2012 when a leap second caused widespread systems outages.

Beyond the complexity of what a “second” is, there are perhaps more well-known idiosyncrasies of time:

  • Leap years/Leap Days: It’s not every year that is a multiple of four, but instead every year that is a multiple of four but not a muliple of 100, unless it’s also a multiple of 400.
    • So 1996 was a leap year (divisible by 4), but 1900 was not because it was divisible by 100. However, 2000 was a leap year because it was divisible by 400.
  • Time zones: Time zones are not just offsets from UTC. To be more precise, the defition of individual time zones can change through time, so the definition of a time zone at one point in time may not be the same at another point in time.
  • Daylight Saving Time (DST): This is probably the most popular source of confusion. In addition to the time changes it introduces, there are also these complexities:

All of these complexities exist within our single calendar system; we have not even discussed alternative or different calendar systems.

Through time, which calendar system was in widespread use has changed. The most popular of these changes was probably the adoption of the Gregorian Calendar (the current calendar used by most countries, including the USA) over the previous Julian Calendar. The reason for this change was in the inaccurate way in which the Julian Calendar defined leap years; it added too many leap days over time.

The Gregorian Calendar introduced the more complicated leap year rules described above, but because the Julian Calendar had added too many leap days, 11 days had to be “dropped” when adopting the Gregorian Calendar to bring dates back in sync with astronomical observations. For example, when Great Britain adopted the new calendar, Sept. 2nd, 1752 was followed by Sept 14th, 1752.

The adoption was not done universally at the same time, with different countries adopting the Gregorian Calendar at different times. An interesting piece of trivia from this is that because Russia was one of the last countries to adopt the Gregorian Calendar, the Bolshevik Revolution came to be known as the October Revolution, because this happened on October 25th, 1917 of the old Julian Calendar, which was actually November 7th, 1917 under the Gregorian Calendar.

This is not an exhaustive list of complexities with regard to time, but hopefully it should be enough to convince you that time is not simple.

UTC can be helpful, but doesn’t solve everything

When faced with all these complexities (time zones, DST, etc.), I’ve often heard that the solution is to “just use UTC” for everything. This works well for most things, but doesn’t always solve your problem.

UTC is an attractive choice for many developers because it’s often the easiest available: Almost any programming language will have the concept of Unix time, which is the number of seconds that have elapsed since 1970-01-01 00:00:00 UTC (the Unix epoch), and this is the usual way in which developers interact with UTC. This value, also called Unix timestamp, is easy to understand (it is a monotonically increasing counter), easy to use, and doesn’t factor in time zones or DST because the reference point is always UTC.

However, when you use UTC this way, you’re not really using UTC as it was defined in the standard. UTC is itself is a complicated standard that has been adjusted several times. It also has the aforementioned concept of leap seconds, which Unix time does not. So, you are actually dealing with a simplification of UTC, but in most cases this does not matter.

So in most cases using a UTC timestamp (or a Unix timestamp) is helpful because it establishes a common time zone everyone can agree on, and removes the need to think about time zones and DST.

Here are a few examples:

  1. Logging: Log files should have the timestamp on every log line. Using UTC for these timestamps makes sense, as we may have servers spread across multiple time zones and a workforce as equally distributed. Using UTC allows us to standardize on a single time zone that does not observe DST.
  2. Event Ordering: Sometimes, you just want to know whether some event, e1, occurred before or after another event, e2, say, for ordering. In this case, just storing a Unix timestamp for each event (t1 and t2 respectively) is probably the simplest solution. Finding out which event came after another is then just a simple integer comparision, e.g. t1 > t2.

In most cases, you will probably want to store or persist your internal representation of a date-time as a Unix timestamp as it is straightforward - you just need a single 64-bit long integer value. (64-bits or the ability to store more than 32-bits to avoid the Y2038 problem)

However, if your application or service is user-facing, you will probably have to convert this back to a human-readable date format at some point in time. For example, if you are operating a delivery app, it probably has to show the user an estimated delivery time. This would have to be in the user’s local time zone. Once you have to do that, all the rules and complexities described above still apply, and there isn’t a way to escape them. Ideally, you do this conversion at the last moment just before the date-time is rendered or shown, so that you don’t have to store time zoned date-times.

Conclusion

I hope this article has shed some light on why you should rely on the Java Date-Time APIs instead of doing your own date/time calculations. The very fact that it took three tries to develop a proper set of date-time APIs for the Java SDK shows that fundamentally, the concepts behind date and time are difficult. I also hope that you have learned why you should avoid the older APIs (java.util.Date and java.util.Calendar) in favour of the newer APIs.

Much of what I’ve written may be obvious to any seasoned Java developer, but no one started out in an experienced state. Writing this post has helped refresh my memory, and also given me the chance to learn a few new things, and I hope that you, as the reader, were able to do the same.