JSR-310: Instant vs ZonedDateTime
Table of Contents
Java is getting better #
I started my software development career around the time Java 8 was released. I remember this very vividly since the upgrade from Java 7 to Java 8 brought with it a world of usability improvements. Maybe I’m just biased since this had a big impact at the start of my career, but in my opinion, the introduction of Java 8 kicked off a usability revolution for Java. Old Java was stable but clunky, verbose, and enterprise-y. It was a language used begrudgingly due to its ubiquity and the fact that “nobody ever got fired for choosing Java”. But it was not a sexy language. Starting with Java 8, however, we began getting improvements which brought the usability of Java up to par with more traditionally “easy” languages like Python, Ruby, and JavaScript. To list a few significant improvements we’ve seen in the last decade:
- Better standard toolchains (keytool, jshell, jpackage)
- Native compilation (GraalVM)
- Pattern matching and virtual threads
- Modern-feeling APIs like
java.time
Ok, maybe Java still isn’t a sexy language, and people aren’t passionately talking about it like they do, say, Rust. But in my humble opinion, Java is in an extremely good place right now. It’s never been easier to develop in Java. Its long-term ubiquity and stability, coupled with all the recent usability improvements, put it at the top of my list for virtually any type and scale of project, be it prototype or production, personal or work, web app, mobile, or headless. Pretty much the only area where Java doesn’t excel right now is in the UI and frontend world. But projects like Vaadin are looking to change that.
Java Time API #
With the possible exception of JShell, the java.time
package has probably had the largest impact on my day-to-day.
Before this, we were either forced to use a 3rd-party API like Joda Time to represent different time structures, or use java.util.Date
, which is extremely limited in what it can represent and can therefore cause all sorts of subtle problems depending on what you choose to do with it.
I love java.time
.
I think this is a pretty common sentiment - it is a really exceptional library that builds off of the standards represented in ISO 8601.
But the vast majority of uses I’ve seen is just using java.time.Instant
instead of java.util.Date
.
Don’t get me wrong, this is already a huge improvement.
But for such a rich and powerful library, it bears delving into a little bit further.
In particular, I think it helps to understand the relationships between the different data types.
Which data types implicitly contain which other data types?
Let’s have a look at what’s available. The following diagram shows which types of data can be combined to get another type. Information content increases as you move down the graph.
Here are code samples for the above:
// LocalDate + LocalTime => LocalDateTime
LocalDateTime localDateTime = LocalDate.now().atTime(LocalTime.now());
// LocalDateTime + ZoneOffset => OffsetDateTime
OffsetDateTime offsetDateTime = LocalDateTime.now().atOffset(ZoneOffset.UTC);
// Instant + ZoneOffset => OffsetDateTime
OffsetDateTime offsetDateTime = Instant.now().atOffset(ZoneOffset.of("-0700"));
// LocalDateTime + ZoneId => ZonedDateTime
ZonedDateTime zonedDateTime = LocalDateTime.now().atZone(ZoneId.of("America/Los_Angeles"));
// Instant + ZoneId => ZonedDateTime
ZonedDateTime zonedDateTime = Instant.now().atZone(ZoneId.of("Asia/Taipei"));
In terms of information hierarchy, Instant
is surprisingly not on the lowest (ie. most information-ful) rung.
Although it’s possible to convert a ZonedDateTime
into Instant
, that’s actually a lossy conversion.
It doesn’t retain any information about the ZoneId
or ZoneOffset
used.
On the other hand, both OffsetDateTime
and ZonedDateTime
contain enough information to convert to Instant
without any other inputs.
So these types contain the most information.
Timezones #
But what is the difference between OffsetDateTime
and ZonedDateTime
?
Why are there two different ways to represent timezone?
To answer this, we must find out what is the difference between a ZoneId
and ZoneOffset
.
Let’s consider the Daylight Savings transition.
An Instant
isn’t subject to Daylight Savings time since it’s defined in UTC.
Therefore it doesn’t have any problems with gaps or overlap periods when we transition into or out of Daylight Savings time.
Similarly, a ZoneOffset
handles Daylight Savings transition by simply ignoring it.
The offset stays the same:
ZoneOffset pacificTz = ZoneOffset.of("-0700");
Instant.parse("2023-11-05T08:00:00Z").atOffset(pacificTz) = 2023-11-05T01:00-07:00
Instant.parse("2023-11-05T08:30:00Z").atOffset(pacificTz) = 2023-11-05T01:30-07:00
Instant.parse("2023-11-05T09:00:00Z").atOffset(pacificTz) = 2023-11-05T02:00-07:00
If a ZonedDateTime
contains all the same information, then how does it handle the transition?
How will we be able to distinguish between 1:00 am in America/Los_Angeles
before and after the transition?
ZoneId pacificTz = ZoneId.of("America/Los_Angeles");
Instant.parse("2023-11-05T08:00:00Z").atZone(pacificTz) = 2023-11-05T01:00-07:00[America/Los_Angeles]
Instant.parse("2023-11-05T08:30:00Z").atZone(pacificTz) = 2023-11-05T01:30-07:00[America/Los_Angeles]
Instant.parse("2023-11-05T09:00:00Z").atZone(pacificTz) = 2023-11-05T01:00-08:00[America/Los_Angeles]
Instant.parse("2023-11-05T09:30:00Z").atZone(pacificTz) = 2023-11-05T01:30-08:00[America/Los_Angeles]
Instant.parse("2023-11-05T10:00:00Z").atZone(pacificTz) = 2023-11-05T02:00-08:00[America/Los_Angeles]
Interesting!
The ZoneId
stayed the same, but the offset changed.
Even though I only specified the ZoneId
, the ZonedDateTime
actually contains both id and offset, and thus the times during the transition out of Daylight Savings are distinct.
The offset is derived from the ZoneId
and Instant
using the ZoneRules
and the Time Zone Database.
The reverse isn’t true - you can’t derive the ZoneId
from ZoneOffset
alone, since there are many zones which share the same offset (eg. America/Vancouver
).
But this offset can only be derived because America/Los_Angeles
is a real zone id.
What if I made one up? It doesn’t let me:
Instant.parse("2023-11-05T08:00:00Z").atZone(ZoneId.of("foobar"));
Unknown time-zone ID: foobar
It’s possible to get a ZoneId
without a standard name, but you have to specify the offset:
ZoneId.ofOffset("UTC", ZoneOffset.of("-0200"));
Conclusion #
So when should you use each type?
Instant
represents a single infinitesimal point on the timeline, but contains no information regarding the observer’s timezone (ie. always UTC). This is useful for systems since you don’t typically need to be concerned with timezones or Daylight Savings.OffsetDateTime
represents a single instant at some (maybe zero) offset from UTC. This contains a little more information that a simple Instant, but doesn’t allow any meaningful calculations since you don’t know if the offset may change for a given user.ZonedDateTime
represents a single instant at some timezone subject to the laws and regulations of the local governing body. It is useful for human use-cases since it may be used to calculate a future date or time relative to the rules in a given jurisdiction.
So if timezone is important (ie. for a user-facing feature), use ZonedDateTime
. Otherwise use Instant
.
Further Reading #
- JSR-310 Date and Time API Guide (including “Rubber Seconds”)