Essentials of Java’s Time API (JSR-310)

 · 14 min
Photo by Nile on Pixabay

Dealing with date and time is a cumbersome task in many programming languages. But with Java 8, the JDK provides us with a comprehensive and completely new API, changing the way we deal with time-related concepts.

Even though JSR310 was introduced with Java 8, the code example will use a Java 10 feature, local variable type inference, to improve readability.
The code examples themselves will all be Java 8 compatible, though.
The // => part of code examples will show the toString() output of the previous line/variable.


Pre-JSR310

Before the new API, the JDK provided only a few classes to handle date and time.

Here are the most commonly known ones:

java.util.Date
a precise instant of time, millisecond precision, relative to January 1st, 1970, 00:00:00 GMT
java.util.Calendar
the bridge between an instant of time and calendar fields (e.g., month, year, day, etc.)
java.util.TimeZone
responsible for zone offset and handling daylight saving time (DST)
java.text.DateFormat
formatting and parsing

At first glance, these four seem to cover the most common scenarios.

But are they really able to handle all delicate specialties of date and time?

Milliseconds

With java.util.Date being based on milliseconds since Unix timestamp “0”, we tend to think of date and time in sums of milliseconds:

Using milliseconds might seem intuitive, but it will lead to bugs eventually.

Working with time is challenging because of our assumptions about date and time that aren’t actually true. We have many misconceptions about date and time. Dave DeLong is maintaining a great list of these with quick explanations. Here are a few wrong assumptions:

The problem with edge cases is that we often don’t realize they exist until we trigger them. If our code only breaks in leap years, it might run fine up to four years.

Not everything is a perfect point in time

A Date represents a single point in time with millisecond precision. But what about broader units?

What day is January 2021? What time is January 6?

We need to be able to represent different concepts of date and time, besides a single point:

  • Dates without time: December 30, 2020
  • Time without dates: 12:24
  • Month and years: December 2020
  • Years: 2020
  • Periods: seven days
  • Different calendar systems: Japanese calendar
  • and more

These concepts had to be implemented by ourselves via code contracts, like representing a timeless-date date as a java.util.Date at midnight. Such contracts make our data not easily exchangeable and are too easy to break. These edge cases and considerations can easily lead to many bugs that don’t immediately become apparent.

Joda Time

Thankfully, a third-party framework developed a better concept of date and time representation, making it the de facto standard date-and-time library for Java before Java 8: Joda-Time.

With Java 8, though, the JDK embraced their work and provided a new Time API based on their concepts.


The Java Time API (JSR310)

To address the previously mentioned shortcomings, a completely new API was necessary.

It was built from scratch, with the author of Joda-Time, Stephen Colebourne, co-leading the effort.

The result was a complete and comprehensive addition to the JDK. But what makes this API so much better than its predecessor?

Design goals

The new API was built with a few core principles in mind:

Immutability
Every type should be immutable, if possible. I’ve written before about the importance and advantages of immutability: thread-safety, fewer bugs due to no mutable state, and it’s even friendlier to the garbage collector.

Fluent API
Fluent code is comprehensible code that’s easier to reason with.

Clear, explicit, and expected
Each method should be well-defined, stand on its own, and make its intentions easily visible. A domain-driven design with coherent method name prefixes should lead to more clarity and readability.

Extensible Even though ISO 8601 is the most commonly used calendar system and the primary calendar system of the new API, it should be open for others. And they should be providable by application developers, not just the JDK itself.


Local Types

There are many different types in the new package java.time.*, each with a specific purpose.

First, we learn about Local types that don’t know about the concept of timezones.

LocalDate

As the name suggests, this type represents a time-zone-less date without time. It’s just a day, month, and year:

java
var date = LocalDate.of(2021, 1, 1);
// => 2021-01-01

Documentation

LocalTime

This is a time without a date or time zone offset.

The standard definition of time applies: time within a day, based on the 24-hour clock, starting at midnight.

java.time.LocalTime stores hours, minutes, seconds, and nanoseconds. Even though the type supports nanosecond precision, be aware that the actual precision depends on the JVM/JDK implementation:

java
var now = LocalTime.now();
// => 12:45:38.896793

var time = LocalTime.of(12, 45);
// => 12:45

Documentation

LocalDateTime

This is the combination of java.time.LocalDate and java.time.LocalTime.

It can also easily be downgraded to its parts:

java
var dt = LocalDateTime.of(2021, 1, 1, 12, 45);
// => 2021-01-01T12:45

Documentation


Time Zones and Offsets

Time zones and their offsets are the banes of everyone working with time. So much can (and usually will) go wrong.

To soothe the pain of dealing with them, the Java Time API separates the responsibility into multiple classes:

ZoneOffset
Offset from the time in UTC/GMT, +14:00 to -12:00
ZoneRules
Rules for how the offset changes for a single time zone (e.g., DST, historical changes)
ZoneId
time zone identifier, e.g., “Europe/Berlin”

There are twp different kinds of time-zoned types available:

OffsetDateTime / OffsetTime

java.time.OffsetDateTime is a simpler version of java.time.ZonedDateTime, unburdened by a relationship to a specific time zone, only defined by its offset. This way, it’s more suited for exchange formats, like saving in databases or JSON/XML:

java
var dt = LocalDateTime.of(2021, 1, 1, 12, 45);
// => 2021-01-01T12:45

var offset = ZoneOffset.of("+02:00");
// => +02:00

var odt = OffsetDateTime.of(dt, offset);
// => 2021-01-01T12:45+02:00

Documentation

ZonedDateTime

Even though an offset is often enough, sometimes we need to handle time-zone-specific data. For such use cases, we have java.time.ZonedDateTime:

java
var dt = LocalDateTime.of(2021, 1, 1, 12, 45);
// => 2021-01-01T12:45

var zoneId = ZoneId.of("Europe/Berlin");
// => Europe/Berlin

var zdt = ZonedDateTime.of(dt, zoneId);
// => 2021-01-01T12:45+01:00[Europe/Berlin]

Documentation


Other Date and Time Types

Besides date, time, and date-time types, the new API has specific classes for other date- and time-related concepts.

Instant

A java.time.Instant is the closest equivalent to java.util.Date` we have. It’s a classical timestamp in relationship to its epoch. The default epoch starts at Unix timestamp “0” (1970-01-01T00:00:00Z, midnight at the start of January 1st, 1970 UTC).

java
var instant = Instant.ofEpochSecond(1609505123);
// => 2021-01-01T12:45:23Z

It can be converted to other types if the missing information is provided. For example, to create a java.time.LocalDateTime, we need to provide the appropriate java.time.ZoneId, so any rules, like DST and the offset, can be applied:

java
var instant = Instant.ofEpochSecond(1609505123);
// => 2021-01-01T12:45:23Z

var zoneId = ZoneId.of("Europe/Berlin");
// => Europe/Berlin

var dt = LocalDateTime.ofInstant(instant, zoneId);
// 2021-01-01T13:45:23

Documentation

Duration

java.time.Duration represents a time-based amount (hours, minutes, seconds, nanos). It can be created either directly, or as a difference between other types:

java
var sixHours = Duration.ofHours(6);
// => PT6H

var lhs = LocalDateTime.of(2020, 12, 24, 15, 22, 23);
var rhs = LocalDateTime.of(2021, 1, 1, 12, 45, 18);

var diff = Duration.between(lhs, rhs);
// => PT189H22M55S

Documentation

Period

java.time.Period is a date-based (years, months, days) counterpart to the time-based java.time.Duration:

java
var threeQuarters = Period.ofMonths(9);
// => P9M

var lhs = LocalDate.of(2020, 7, 12);
var rhs = LocalDate.of(2021, 1, 1);

var diff = Period.between(lhs, rhs);
// => P5M20D

Documentation

Year

A year in the ISO calendar:

java
var _2021 = Year.of(2021);
// => 2021

Be aware that it matches the Gregorian-Julian calendar for modern years only.

For example, parts of Russia did not switch to the modern Gregorian calendar until 1920. Also, multiple calendar reforms can make calculations with historical dates complicated in general.

Documentation

YearMonth

A day-less date type, e.g., January 2021:

java
var jan2021 = YearMonth.of(YearMonth.of(2021, Month.JANUARY));
// => 2021-01

Documentation

MonthDay

A year-less representation of a date, e.g., January 6:

java
var threeKingsDay = MonthDay.of(Month.JANUARY, 6);
// --01-06

The string output might seem strange, but it’s just as defined by ISO 8601:2000, although the updated ISO 8601:2004 disallows the omittance of the year if a month is present. (Wikipedia)

Documentation

Month / DayOfWeek enums

Another source of a multitude of bugs is one-off errors regarding months and weekdays.

  • Is January represented by 1 or 0?
  • Is December 12 or 11?

When does a week start? Sunday or Monday? What value do they represent?

With Java Time API being ISO 8601-based, the week always starts on Monday. And to be consistent, it begins with the value 1 for Monday and for January.

To make it even more usable, we are provided with two enums, which are interchangeable with the numeric values in most methods:

java
var january2021 = YearMonth.of(2021, Month.JANUARY);
var wednesday = LocalDateTime.now().with(DayOfWeek.WEDNESDAY);

Documentation:


General API Design

Usually, an API with lots of new classes means we have to understand and remember many new methods and concepts. To ease the cognitive load, the API was designed with consistent method name prefixes and shared concepts between its types.

Method name prefixes

We can easily explore the different capabilities of a type by simply triggering autocompletion after starting a prefix.

get

A classical getter, to retrieve parts of the type:

java
var date = LocalDate.of(2021, Month.JANUARY, 1);
var year = date.getYear();
// => 2021

with

Returns a copy with the specified change:

java
var date = LocalDate.of(2021, Month.JANUARY, 1);
date = date.withDayOfMonth(15)
// => 2021-01-15

plus/minus

Returns a copy with the result of the calculation:

java
var date = LocalDate.of(2021, Month.JANUARY, 1);
date = date.plusDays(15L);
date = date.minusYears(10L)
// => 2011-01-16

multipliedBy/dividedBy/negated

Additional calculations for java.time.Duration/java.time.Period:

java
var quarter = Period.ofMonths(3);
var fourQuarters = quarter.multipliedBy(4);
// => P12M

to

Conversion between types:

java
var dt = LocalDateTime.of(2021, 1, 1, 12, 45);

var date = dt.toLocalDate();
// => 2021-01-01

var time = dt.toLocalTime();
// => 12:45

at

Returns new object with time-/time-zone-related changes:

java
var date = LocalDate.of(2021, 1, 1);

LocalDateTime dt = date.atTime(12, 45);
// => 2021-01-01T12:45

var zoneId = ZoneId.of("Europe/Berlin");

ZonedDateTime zdt = dt.atZone(zoneId);
// => 2021-01-01T12:45+01:00[Europe/Berlin]

of

Static factory methods without conversion:

java
var date = LocalDate.of(2021, 1, 1);

var zoneId = ZoneId.of("Europe/Berlin");

from

Static factory methods with conversion:

java
var dt = LocalDateTime.of(2021, 1, 1, 12, 45);

var date = LocalDate.from(dt);
// => 2021-01-01

Be aware that the conversion works for downgrades only. For example, we can’t create a java.time.LocalDateTime from a java.time.LocalDate:

java
var date = LocalDate.of(2021, 1, 1);
var dt = LocalDateTime.from(date);
// Will throw:
// Exception java.time.DateTimeException: Unable to obtain LocalDateTime from TemporalAccessor: 2021-01-01 of type java.time.LocalDate
// |        at LocalDateTime.from (LocalDateTime.java:461)
// |        at do_it$Aux (#47:1)
// |        at (#47:1)
// |  Caused by: java.time.DateTimeException: Unable to obtain LocalTime from TemporalAccessor: 2021-01-01 of type java.time.LocalDate
// |        at LocalTime.from (LocalTime.java:431)
// |        at LocalDateTime.from (LocalDateTime.java:457)
// |        ...

parse

Static factory methods to parse text input:

Parsing and formatting

All types have well-defined toString() methods, based on ISO 8601 and their precision:

 TYPE           | FORMAT  
----------------|-------------------------------------  
LocalDate       | uuuu-MM-dd  
LocalTime       | HH:mm  
                | HH:mm:ss  
                | HH:mm:ss.SSS  
                | HH:mm:ss.SSSSSS  
                | HH:mm:ss.SSSSSSSSS  
LocalDateTime   | uuuu-MM-dd'T'HH:mm  
                | uuuu-MM-dd'T'HH:mm:ss  
                | uuuu-MM-dd'T'HH:mm:ss.SSS  
                | uuuu-MM-dd'T'HH:mm:ss.SSSSSS  
                | uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSS  
Year            | value without leading zeroes  
YearMonth       | uuuu-MM  
MonthDay        | --MM-dd  
OffesetDateTime | uuuu-MM-dd'T'HH:mmXXXXX
                | uuuu-MM-dd'T'HH:mm:ssXXXXX
                | uuuu-MM-dd'T'HH:mm:ss.SSSXXXXX
                | uuuu-MM-dd'T'HH:mm:ss.SSSSSSXXXXX
                | uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSSXXXXX
OffestTime      | HH:mm:ssXXXXX
                | HH:mm:ss.SSSXXXXX
                | HH:mm:ss.SSSSSSXXXXX
                | HH:mm:ss.SSSSSSSSSXXXXX
ZonedDateTime   | LocalDateTime + ZoneOffset
ZoneOffset      | Z (for UTC)
                | +h
                | +hh
                | +hh:mm
                | -hh:mm
                | +hhmm
                | -hhmm
                | +hh:mm:ss
                | -hh:mm:ss
                | +hhmmss
                | -hhmmss
Duration        | PT\[n\]H\[n\]M\[n\]S
Period          | P\[n\]Y\[n\]M\[n\]D

The format produced by toString() is also usable in the respective parse(CharSequence text) methods, which is great for exchange methods or non-localized display.

For more human-friendly and localized representations, the class java.time.format.DateTimeFormatter can be used. It’s thread-safe, immutable, and provides a fluent API:

java
var formatted = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
                                 .withLocale(Locale.GERMAN)
                                 .format(LocalDate.of(2021, 1, 1))
// => Freitag, 1. Januar 2021

It can also be used for parsing, by providing a formatter to a parse method:

java
var formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
                                 .withLocale(Locale.GERMAN);

var parse = LocalDate.parse("Freitag, 1. Januar 2021", formatter);
// => 2021-01-01

TemporalAdjuster

With the functional interface java.time.temporal.TemporalAdjuster, we can define strategies on how to adjust types implementing java.time.temporal.Temporal. This way, we can have well-defined, reusable adjustments for the new Java Time API types.

The Java Time API provides multiple predefined adjusters with its utility class java.time.temporal.TemporalAdjusters. In the general spirit of the API, the method names (mostly) speak for themselves:

For example, in a subscription-based application, we could create customer-specific adjusters like nextBillingDate(Customer customer). The code is easily comprehensible, and the logic for calculating a billing date based on a customer is only in one place.

TemporalUnit and ChronoUnit

The interface java.time.temporal.TemporalUnit provides a way to express units of date and time, which then can be used in calculations.

The most commonly used units are already available with the enum java.time.temporal.ChronoUnit, providing constants for all kinds of different units, like MILLENNIA, DAYS, HOURS , MILLIS, etc.

As a parameter

Besides specific calculation methods, like LocalDate plusDays(long daysToAdd), there are also non-specific ones available needing a java.time.temporal.TemporalUnit, like LocalDate plus(long amountToAdd, TemporalUnit unit).

All Java Time API calculations will try their best to be sensible and stay within their respective unit and the related bigger units. We have to think of the new types as a combination of different units, not as a single value.

This means if we add a month to a java.time.LocalDate, it will affect its month (and related units like its year):

java
var date = LocalDate.of(2021, 1, 31);
// 2021-01-31

date = date.plus(1L, ChronoUnit.MONTHS);
// => 2021-02-28

Differences can also be easily calculated with java.time.temporal.ChronoUnit. In the case of LocalDate:

java
var startDate = LocalDate.of(2021, 1, 1);
// 2021-01-01

var endDate = LocalDate.of(2023, 11, 8);
// 2023-11-08

var amount = startDate.until(endDate, ChronoUnit.WEEKS);
// => 148

Static methods

The enum value itself also contains two methods for more comprehensible calculation code:

The previous examples can also be expressed with these methods:

java
var date = LocalDate.of(2021, 1, 31);
// 2021-01-31

date = ChronoUnit.MONTHS.addTo(date, 1L);
// => 2021-02-28


var startDate = LocalDate.of(2021, 1, 1);
// => 2021-01-01

var endDate = LocalDate.of(2023, 11, 8);
// => 2023-11-08

var amount = ChronoUnit.WEEKS.between(startDate, endDate);
// => 148

And with a static import, it becomes even more readable:

java
import static java.time.temporal.ChronoUnit;

var date = LocalDate.of(2021, 1, 31);
// => 2021-01-31

date = MONTHS.addTo(date, 1L);
// 2021-02-28


var startDate = LocalDate.of(2021, 1, 1);
// => 2021-01-01

var endDate = LocalDate.of(2023, 11, 8);
// => 2023-11-08

var amount = WEEKS.between(startDate, endDate);
// => 148

Durations

The enum constants are also representable as java.time.Duration, with the ISO calendar definition:

java
ChronoUnit.HALF_DAYS.getDuration();
// => PT12H

ChronoUnit.WEEKS.getDuration();
// => PT168H

ChronoUnit.CENTURIES.getDuration();
// => PT876582H

Converting between types

We can’t just replace all java.util.Date instances with one of the new types, so we need to be able to convert between them. The new method java.util.Date#toInstant() provides a vehicle between old and new.

An java.time.Instant can be converted to another type if appropriate time-zone-related data is provided:

Conversion between different types

The corresponding code is quite self-explanatory:

java
// STEP 1: We have a pre-JSR310 date object
var date = new Date();

// STEP 2: Define an appropiate zone
var zone = ZoneId.systemDefault();

// STEP 3: Convert date to instant
var instant = date.toInstant();

// STEP 4: Convert to ZonedDateTime
var zdt = instant.atZone(zone);

From there, we can use another to method to convert it further down the lane.


Android Support

In the past, many of the exciting new Java 8+ features took some time to arrive in Android. We needed either a third-party framework or backports to use them.

Thanks to the Android Gradle Plugin 4.0, many Java 8 features can be used via desugaring, without needing a higher API level. Some changes to the build.gradle and a new dependency are required though:

groovy
android {
  defaultConfig {
    // Required when setting minSdkVersion to 20 or lower
    multiDexEnabled true
  }

  compileOptions {
    // Flag to enable support for the new language APIs
    coreLibraryDesugaringEnabled true
    // Sets Java compatibility to Java 8
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

dependencies {
  coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.9'
}

Here’s a list of all available APIs through desugaring: Android Developers.


Java Time API for Java 6 and 7

Even though Java 8 was released six years ago, in March 2018, not everyone has the luxury of using it.

But do not despair. We can still use the new API, thanks to a backport.

The ThreeTen Backport project provides a Java 6- and 7-compatible way to use the new types without additional dependencies. It’s maintained by the main author of the Java Time API, Stephen Colebourne.


A Functional Approach to Java Cover Image
Interested in using functional concepts and techniques in your Java code?
Check out my book!

Resources