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:

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.

java.time.LocalDate

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

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

Documentation

java.time.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.

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:

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

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

Documentation

java.util.LocalDateTime

This is the combination of LocalDate and LocalTime.

It can also easily be downgraded to its parts:

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:

java.time.OffsetDateTime / java.time.OffsetTime

OffsetDateTime is a simpler version of 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:

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

java.time.ZonedDateTime

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

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.

java.time.Instant

An 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).

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 LocalDateTime, we need to provide the appropriate ZoneId, so any rules, like DST and the offset, can be applied:

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

java.time.Duration

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

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

java.time.Period

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

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

java.time.Year

A year in the ISO calendar:

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

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

java.time.YearMonth

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

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

Documentation

java.time.MonthDay

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

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:

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:

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

with

Returns a copy with the specified change:

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:

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 Duration/Period:

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

to

Conversion between types:

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:

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:

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

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

from

Static factory methods with conversion:

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 LocalDateTime from a LocalDate:

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:

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:

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

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

java.time.TemporalAdjuster

With the functional interface TemporalAdjuster, we can define strategies on how to adjust types implementing 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 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.

java.time.temporal.TemporalUnit and java.time.temporal.ChronoUnit

The interface 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 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 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 LocalDate, it will affect its month (and related units like its year):

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 ChronoUnit. In the case of LocalDate:

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:

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:

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 Duration, with the ISO calendar definition:

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 Instant can be converted to another type if appropriate time-zone-related data is provided:

Conversion between different types
Conversion between different types

The corresponding code is quite self-explanatory:

// 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:

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.

Resources