Skip to content

Note: the following explanations don’t take into account the accuracy or synchronicity of clocks. That is a much more complex topic. Also, discussing leap seconds is out of scope as it should not matter here.

General remarks about storing and using time values

  • When thinking about which of the following classes to use as a date/time value for a particular purpose, it helps to ignore how the value looks when converted into a character string representation. Rather, keep the concept in mind and consider the semantics.

  • When you’re in need of storing a date (day level granularity, no need for hours or shorter divisions), don’t make the mistake of using a class that can store higher precision and set the hour and smaller parts to zero. Use a LocalDate that can only store day precision to make the purpose clear and self-documenting. You then don’t risk your mis-used datetime value to be accidentally subject to timezone conversions: A value of midnight, July 14th, might suddenly become 17 hours into July 13th, changing your date.

  • Similar to the previous advice, when storing the end of an interval, try not to use the “last” value in the interval (compatible with a comparison for “less-or-equals”, <=), but the first value “after” the interval (compatible with a comparison for “strictly less”, <).

    That way, your comparisons will have a uniform a <= t < b form and work independently on the resolution of the clock. If you have a milliseconds resolution in your clock, then the “one hour interval” from 16:00:00 to 16:59:59 is nearly one second shorter than an hour.
    You will reduce the complexity to reason about your code by avoiding such hidden implicit dependencies influencing the correctness, and by using a uniform comparison approach.

  • Unfortunately, Java’s time classes only have the isBefore() and isAfter() methods to compare time values. If you want to negate a.isBefore(b) (to express “a is at or after b”), don’t be tempted to use a.isAfter(b) or (equivalently) b.isBefore(a): both are wrong and different from the correct expression !a.isBefore(b), even though reading your code will be a bit harder due to the negation. You could write some auxiliary methods to mend that. Correctness should come first!

The last two advices, taken together, are illustrated by the following:

Instead of writing

  Instant start = Instant.parse("2023-03-27T13:00:00Z");
Instant end = Instant.parse("2023-03-27T13:59:59Z"); // <- supposedly "latest" point in time inside the interval
Instant now = Instant.now();
if (!now.isBefore(start) && !now.isAfter(end)) { // start <= now <= end
// ...
}

you should write

  Instant start = Instant.parse("2023-03-27T13:00:00Z");
Instant end = Instant.parse("2023-03-27T14:00:00Z"); // <- correct earliest point in time after the interval
Instant now = Instant.now();
if (!now.isBefore(start) && now.isBefore(end)) { // start <= now < end
// ...
}

Discussion of classes that store date and/or time

Class java.time.Instant

An Instant represents a point on the physical timeline. You can always tell if one Instant lies before or after another Instant, or if they are identical.

Possible use

Instant is usually the best choice for concepts like log entries or events: concepts where it is important to know in what order they happen.

Possible text representation

The character string representation of Instant is usually given by the point in time as observed in UTC, written as defined in the ISO 8601 format. Sticking to that “universal time”, and including the corresponding Z offset indicator, reduces possible confusion. For example:

    2023-03-27T16:38:53.98Z

Class java.time.LocalDateTime

A LocalDateTime is the date and time you can observe on a mantelpiece in a movie when there’s a clock and a calendar sitting on it. It represents a single point on the timeline, but you cannot tell which one.

You cannot infer a point on the timeline from a LocalDateTime, because you would additionally need information about the time zone or offset where the fireplace is located. A fireplace in Boston, MA., and one in Ljubljana, Slovenia, showing the same LocalDateTime, will mean different Instants on the timeline.

The other way around, it is similar: you cannot infer a LocalDateTime from an Instant, because you need to know the time zone or offset.

You can consider a LocalDateTime to be an aggregation of a LocalDate and a LocalTime (see below). As such it contains information about year, month, day, hour, minutes, seconds, and fraction of a second. It is able to hold the fraction of a second information up to nanosecond precision.

It makes only sense to compare two LocalDateTime values when they are happening in the same time zone/offset.

Possible use

LocalDateTime is of lesser practical use. You can use one when arranging a rendezvous, if the place is known and not too far away, and you're in a romantic mood; but technically, an Instant, ZonedDateTime or OffsetDateTime would be better for this.

In my experience, a LocalDateTime is mostly used for constructing a ZonedDateTime or an OffsetDateTime (see below). It’s also what the PostgreSQL JDBC driver considers a timestamp without time zone SQL value when sending to or reading from the database.

Possible text representation

A character string representation of LocalDateTime does of course not contain time zone/offset information, for example

    2023-03-27T18:38:53.98

Class java.time.LocalDate

A LocalDate is what you read when you only observe the calendar on the mantelpiece. Consisting of year, month, and day, with a resolution no finer than days. It does not even make sense to try and infer a point on the timeline for it.

Possible use

LocalDate is a good choice when you need to remember the birthday (including the year) of a person, or the date when a software manufacturer will stop supporting a product.

Possible text representation

A character string representation of LocalDate in ISO 8601 format looks like

    2023-03-27

Class java.time.LocalTime

A LocalTime represents the time of a day. It consists of hour (0–23), minute (0–59), second (0–59) and a fraction of a second. It is able to hold the fraction of seconds information up to nanosecond precision.

Possible use

You can use LocalTime to express the time of daily recurring events like “every day at 6 o’clock I need to milk the cows.” It’s also the data to use when specifying that a daily database maintenance job should run at 01:15:00, when all users are sleeping, in case you're lucky and have a locally confined user base.

Possible text representation

A character string representation of LocalTime in ISO 8601 format is for example

T06:00:00

or just

06:00:00

Class java.time.OffsetDateTime

An OffsetDateTime holds the same information as LocalDateTime does and extends it with information about how much this value differs from the value that would be shown at the same point on the timeline on a mantel clock at Greenwich, England (really meaning UTC). This additional information is called “offset” and is represented by the class java.time.ZoneOffset.

The offset is usually a multiple of hours, sometimes half an hour, even though it can be stored with seconds precision. The offset is positive if the local time has a higher value (looks like it’s later) than at UTC, and negative if it has a lower value (looks like it’s earlier) than at UTC. Further, the offset has nothing to do with daylight savings time changes (see ZonedDateTime). It is a fixed amount of time attached to the LocalDateTime which you can subtract to calculate the corresponding UTC time, and thus, the Instant.

An OffsetDateTime represents a single point on the timeline and also defines which one. In other words, you can infer the Instant that corresponds to the OffsetDateTime. The other way around, you additionally need the ZoneOffset to construct an OffsetDateTime from an Instant.

Possible use

Not so much for real world use cases, rather for technical purpose like communicating to a database. For example, you should use it when you want to send a timestamp with time zone SQL value to a PostgreSQL database using JDBC.

Possible text representation

A character string representation of OffsetDateTime in ISO 8601 format is for example

    2023-03-28T12:16:28.686074+02:00

Class java.time.ZonedDateTime

A ZonedDateTime holds information as LocalDateTime does, together with information about the timezone where it happens. The time zone is represented by class java.time.ZoneId.

In contrast to ZoneOffset, which contains a fixed offset to UTC, a ZoneId contains the name of a timezone, like "Europe/Berlin". This brings politics into the game, because it is a reference to the laws telling in what part of the world which time offset is effective at a certain point in time. This political information is collected in the tzdata database, previously also known as Olson database, after its founding contributor. This database tries to collect all information (including historical) about the rules when the time offset changed/changes in which part of the world. For example, it will resolve the ZoneId of "Europe/Berlin" at a LocalDateTime of "2023-03-28T12:11" to an offset of 2 hours, because daylight saving time was effective at that time in Germany.

As OffsetDateTime, ZonedDateTime represents a single point in time (there’s a rule which one is chosen in case of ambiguity during the daylight saving time switchback). You can always determine the Instant corresponding to a ZonedDateTime. The other way around, you additionally need the ZoneId to construct a ZonedDateTime from an Instant.

Similarly, you can always convert a ZonedDateTime to an OffsetDateTime, but for the inverse way, you need the ZoneId.

Be aware, however, that the conversion to/from Instant (or OffsetDateTime) can depend on when you do the conversion, because political rules change when new laws are passed, and the tzdata database changes over time to reflect that. So it is wise to not always immediately convert a ZonedDateTime to an Instant and use that, but only convert when it is needed.

In other words, if you want to meet with someone in Paris at 10 o’clock on the 25th of March 2033, you should check the daylight savings time rules a short time before that. Otherwise, you risk being an hour late in case the French legislative chooses to switch to daylight savings time a week earlier than you originally thought.

Possible use

ZonedDateTime is the preferred data to store a point on the timeline when you need to know the timezone and the local date and time at that zone and instant, for example, appointments in a calendar.

Possible text representation

There is no well-established convention for representing a ZonedDateTime as a character string. Java’s toString() method results for example in

    2023-03-28T12:16:28.686074+02:00[Europe/Berlin]

Be aware that the offset information in the above string ("+02:00") is not stored with the ZonedDateTime, it is determined from the tzdata database at the time the string representation is created.

Image of the author

Published by Dirk L.

Visit author page