Background

Java 8 has been relased for 5 years now but in some projects that I worked on in the past, I still encountered some developers preferring Joda-Time over the new built in java.time APIs even though Joda-Time had encouraged people migrating their code to using the new Java DateTime APIs (JSR-310), which is also an implicit announcement that it is deprecated onwards.

I have been using these new APIs for a while and found the experience of using them are joyful. These new time APIs are easy to use and also adopt some of the best practice in software project that addresses a few cons of previous generation of Java date time APIs (e.g Calendar). I’ll briefly talk about my understanding and experience of using these two new APIS in the rest of blog.

java.time package

There are three classes from the package: java.time that I think suit most of need in developers’ daily life.

LocalDateTime

LocalDateTime is used to represent a particular time in local timezone where it is configured by the jvm. Hence, unlike the other two date time types, it doesn’t store any timezone information as it assuming in the default timezone of this JVM. You can use it in some data that are not timezone sensitive for example: birth date time. You may wonder what if i just want to store the date part or the time part. Along with the LocalDateTime type, Java 8 has provided another two types: LocalDate and LocalTime. With them, you can only store date or time part for your own need.

An example:

private void localDateTimeExample(){
    var localDatetime = LocalDateTime.of(2019, 3, 1, 13, 15, 15, 0);
    System.out.println("Local date time is: " + localDatetime);

    var localDate = localDatetime.toLocalDate();
    System.out.println("Local date is: " + localDate);

    var localTime = localDatetime.toLocalTime();
    System.out.println("Local time is: " + localTime);

    System.out.println();
}
Java

This produces following output:

Local date time is: 2019-03-01T13:15:15
Local date is: 2019-03-01
Local time is: 13:15:15
Bash

OffsetDateTime

OffsetDateTime has a bit more information than the LocalDateTime type. It stores how much time (hours and minutes) are ahead or behind the GMT timezone for the current time. In the ISO 8601, it is defined as suffix to your local time in the format: +XX:XX or -XX:XX. + means how many hours and miniutes ahead and - how many hours and minutes behind. For example, a full OffsetDateTime value is represented as: 2007-12-03T10:15:30+01:00 that indicates my current timezone is one hour ahead of the GMT timezone.

private void offsetDateTimeExample(){
    // +08:00
    var shanghaiTime = ZonedDateTime.of(2019, 3, 1, 13, 00, 00, 0, SHANGHAI_TZ);
    printOffsetDatetime("Shanghai time at GMT+8", shanghaiTime.toOffsetDateTime());

    // +11:00
    var melbourneTime = shanghaiTime.withZoneSameInstant(MELBOURNE_TZ);
    printOffsetDatetime("Melbourne time at GMT+11", melbourneTime.toOffsetDateTime());

    System.out.println("Are they same time? " + (shanghaiTime.isEqual(melbourneTime) ? "Yes" : "No"));

    // +08:00
    var shanghaiTimeWithDst = ZonedDateTime.of(2019, 5, 1, 13, 00, 00, 0, SHANGHAI_TZ);
    printOffsetDatetime("Shanghai time in DST at GMT+8", shanghaiTimeWithDst.toOffsetDateTime());

    // +11:00
    var melbourneTimeWithDST = shanghaiTimeWithDst.withZoneSameInstant(MELBOURNE_TZ);
    printOffsetDatetime("Melbourne time in DST at GMT+11", melbourneTimeWithDST.toOffsetDateTime());

    System.out.println("Are they same time? " + (shanghaiTimeWithDst.toOffsetDateTime().isEqual(melbourneTimeWithDST.toOffsetDateTime()) ? "Yes" : "No"));

    System.out.println();
}

private void printOffsetDatetime(String offset, OffsetDateTime date){
    System.out.println(offset + " is : " + date + " and epoch time is " + date.toEpochSecond());
}
Java

Code above produces the following output:

Shanghai time at GMT+8 is : 2019-03-01T13:00+08:00 and epoch time is 1551416400
Melbourne time at GMT+11 is : 2019-03-01T16:00+11:00 and epoch time is 1551416400
Are they same time? Yes
Shanghai time in DST at GMT+8 is : 2019-05-01T13:00+08:00 and epoch time is 1556686800
Melbourne time in DST at GMT+11 is : 2019-05-01T15:00+10:00 and epoch time is 1556686800
Are they same time? Yes
Bash

You may have observed that in the same timezone (Melbourne), the offset has changed depending on the date. This is because China has no Daylight Saving Time but Australia does. In Melbourne, when DST begins, the offset becomes 10 hours and, when it ends, it is changed back to 11 hours. So the hour difference can be 2 or 3 hours between China and Melbourne depending on if the Melbourne is in DST or not. In the case where offset time is not fixed depending on the timezone, I normally recommend people using ZonedDateTime insstead that is covered in the next section.

ZonedDateTime

ZonedDateTime does more work than OfsetDateTime. It represent a particular time in its stored timezone too but it stores a full timezone information so you can use it to do some date time calculation that is dependent on the timezone. Taking the same example from OffsetDateTime, I am living in Melbourne and we have a daylight saving starting in the first Sunday of April and ending in the first Sunday of October. Using OffsetDateTime makes our life very hard to render the date time correctly in the user interface because we have to implement some custom logic. If our application is across global regions, then developers have to possess full knowledge of the entire global timezone complexity. But using ZonedDateTime reduces this complexity. You only need to provide correct timezone then it is able to give correct date time. Following example demonstrates how easy this type is to use.

private void zonedDatetimeExample(){

    var shanghaiTime = ZonedDateTime.of(2019, 3, 1, 13, 00, 00, 0, SHANGHAI_TZ);
    printZonedDateTime("Shanghai", shanghaiTime);

    var melbourneTime = shanghaiTime.withZoneSameInstant(MELBOURNE_TZ);
    printZonedDateTime("Melbourne", melbourneTime);

    System.out.println("Are they same time? " + (shanghaiTime.isEqual(melbourneTime) ? "Yes" : "No"));

    var shanghaiTimeWithDst = ZonedDateTime.of(2019, 5, 1, 13, 00, 00, 0, SHANGHAI_TZ);
    printZonedDateTime("Shanghai in DST", shanghaiTimeWithDst);

    var melbourneTimeWithDST = shanghaiTimeWithDst.withZoneSameInstant(MELBOURNE_TZ);
    printZonedDateTime("Melbourne in DST", melbourneTimeWithDST);

    System.out.println("Are they same time? " + (shanghaiTimeWithDst.isEqual(melbourneTimeWithDST) ? "Yes" : "No"));

}

private void printZonedDateTime(String timezone, ZonedDateTime date){
    System.out.println(timezone + " time is " + date + " and epoch time is: " + date.toEpochSecond());
}
Java

Running the code above, it gives us the following output.

Shanghai time is 2019-03-01T13:00+08:00[Asia/Shanghai] and epoch time is: 1551416400
Melbourne time is 2019-03-01T16:00+11:00[Australia/Melbourne] and epoch time is: 1551416400
Are they same time? Yes
Shanghai in DST time is 2019-05-01T13:00+08:00[Asia/Shanghai] and epoch time is: 1556686800
Melbourne in DST time is 2019-05-01T15:00+10:00[Australia/Melbourne] and epoch time is: 1556686800
Are they same time? Yes
Java

China has no Daylight saving time but Australia does. In Melbourne, when DST begins, local time will be moved backward 1 hour and, when it ends, it will be moved forward 1 hour. So the hour difference can be 2 or 3 hours between China and Melbourne depending on if the Melbourne time is DST or not. The example above demonstrates this difference.

In the first two rows, we picked 2019 March 1st 13:00:00 as our test date. We can tell there are a two-hours difference between them but they are both pointing to same second in the epoch time. In the next two rows, I changed our input date to 2019 May 1st 13:00:00 as the input date. Now, we can tell the difference has become 3 hours for this date.

Conclusion

In this blog, we have briefly discussed the difference of these three main Java time classes. In my own experience, I use ZonedDateTime in most of scenarios for timezone related logic and only use OffsetDateTime for persistent in Postgres. For the other date time that is fixed across global regions (for example, date of birth), I would then use LocalDateTime and its derivative types such as LocalDate and LocalTime. I have put my example code in Github.

In my next blog (Part 2), I’ll cover how to parse and format date time.

Further readings: