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:
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:
LocalDateTime
This is the combination of java.time.LocalDate
and java.time.LocalTime
.
It can also easily be downgraded to its parts:
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:
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
:
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).
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:
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:
Period
java.time.Period
is a date-based (years, months, days) counterpart to the time-based java.time.Duration
:
Year
A year in the ISO calendar:
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:
MonthDay
A year-less representation of a date, e.g., January 6:
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:
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:
with
Returns a copy with the specified change:
plus/minus
Returns a copy with the result of the calculation:
multipliedBy/dividedBy/negated
Additional calculations for java.time.Duration
/java.time.Period
:
to
Conversion between types:
at
Returns new object with time-/time-zone-related changes:
of
Static factory methods without conversion:
from
Static factory methods with conversion:
Be aware that the conversion works for downgrades only. For example, we can’t create a java.time.LocalDateTime
from a java.time.LocalDate
:
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:
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:
It can also be used for parsing, by providing a formatter to a parse
method:
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):
Differences can also be easily calculated with java.time.temporal.ChronoUnit
.
In the case of LocalDate
:
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:
And with a static import
, it becomes even more readable:
Durations
The enum constants are also representable as java.time.Duration
, with the ISO calendar definition:
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:
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:
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)