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

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.
Table of Contents
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:
var date = LocalDate.of(2021, 1, 1);
// => 2021-01-01
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:
var now = LocalTime.now();
// => 12:45:38.896793
var time = LocalTime.of(12, 45);
// => 12:45
LocalDateTime
This is the combination of java.time.LocalDate
and java.time.LocalTime
.
It can also easily be downgraded to its parts:
var dt = LocalDateTime.of(2021, 1, 1, 12, 45);
// => 2021-01-01T12:45
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:
ZonedDateTime
: bound to a specificZoneId
OffsetDateTime
/OffsetTime
: date/time with an offset, but not bound to a particular timezone
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:
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
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
:
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]
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).
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:
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
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:
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
Period
java.time.Period
is a date-based (years, months, days) counterpart to the time-based java.time.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
Year
A year in the ISO calendar:
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.
YearMonth
A day-less date type, e.g., January 2021:
var jan2021 = YearMonth.of(YearMonth.of(2021, Month.JANUARY));
// => 2021-01
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)
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 java.time.Duration
/java.time.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 java.time.LocalDateTime
from a java.time.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
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):
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
:
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:
<R extends Temporal> R addTo(R temporal, long amount)
long between(Temporal temporal1Inclusive, Temporal temporal2Exclusive)
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 java.time.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 java.time.Instant
can be converted to another type if appropriate time-zone-related data is provided:
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
- JSR-310 Date and Time API Guide (JCP.org)
- Package java.time (Oracle)
- ISO 8601 (Wikipedia)
- ThreeTen Backport
- Your Calendrical Fallacy Is… (Dave DeLong)