Time is a complex topic. Many programmers are unfamiliar with the difficulties and common bugs associated with time. This has in the past lead to time APIs that allowed “sloppy” code which can lead to bugs in edge-cases that will never be caught during testing but will hit you two years down the line. The old time APIs –
and
– did exactly that.
With Java 8, the standard library now contains one of the best date/time APIs in any language. java.time
does not allow “sloppy” code anymore – it is very explicit. This can help reduce bugs if you know how to use it. Unfortunately, many online resources still use the old APIs and many programmers are confused by the different types in java.time
(
,
and so on). The goal of this article is providing a basic overview of the most important classes, how to use them and most importantly when to use which.
It should be said that all types in the java.time
package are immutable. If you are not familiar with the concept of immutability, I recommend reading my previous article Immutability in Java.
This article is about time in java. For a general resource on time in programming, I recommend “Falsehoods programmers believe about time”.
Time-of-day, or just time, is the 24-hour (or 12-hour am/pm) time we are all familiar with. 0:00 is midnight, and 12:00 is noon. Because the earth is a globe and it isn't noon for everyone at the same time (unfortunate choice of words there), time varies by time zone.
java.time
offers the
and
classes for working with time. To get the current time, you need to know the time zone.
LocalTime.of ({{now.tz(userZone).format("H")}}, {{now.tz(userZone).format("m")}});
LocalTime.of ({{now.tz(userZone).format("H")}},
{{now.tz(userZone).format("m")}},
{{now.tz(userZone).format("s")}});
LocalTime.of ({{now.tz(userZone).format("H")}},
{{now.tz(userZone).format("m")}},
{{now.tz(userZone).format("s")}}, {{now.tz(userZone).format("SSS")}}{{nanoMixin}});
// with nanoseconds
LocalTime.now (ZoneId.of ("Pacific/Honolulu")); // {{now.tz("Pacific/Honolulu").format("HH:mm:ss.SSS")}}
LocalTime.now (ZoneId.of ("{{userZone}}")); // {{now.tz(userZone).format("HH:mm:ss.SSS")}}
LocalTime.now (ZoneId.of ("Asia/Kathmandu")); // {{now.tz("Asia/Kathmandu").format("HH:mm:ss.SSS")}}
LocalTime.now (ZoneId.of ("Australia/Sydney")); // {{now.tz("Australia/Sydney").format("HH:mm:ss.SSS")}}
OffsetTime.now (ZoneId.of ("Pacific/Honolulu")); // {{now.tz("Pacific/Honolulu").format("HH:mm:ss.SSSZ")}}
OffsetTime.now (ZoneId.of ("{{userZone}}")); // {{now.tz(userZone).format("HH:mm:ss.SSSZ")}}
OffsetTime.now (ZoneId.of ("Asia/Kathmandu")); // {{now.tz("Asia/Kathmandu").format("HH:mm:ss.SSSZ")}}
OffsetTime.now (ZoneId.of ("Australia/Sydney")); // {{now.tz("Australia/Sydney").format("HH:mm:ss.SSSZ")}}
As you can see, the only difference between the two classes is that
includes offset information (more on offsets and zones below). In the majority of cases
is sufficient.
A date is just what you'd expect — the combination of year, month and day. Just like time, the date is timezone-specific — it might be monday in Australia but still sunday in Europe. It might even be three different dates across the world!
To represent dates, you use the
class:
LocalDate.of ({{now.tz(userZone).format("YYYY")}},
{{now.tz(userZone).format("M")}},
{{now.tz(userZone).format("D")}});
LocalDate.now (ZoneId.of ("Pacific/Samoa"));
// {{now.tz("Pacific/Samoa").format("YYYY-MM-DD")}}
LocalDate.now (ZoneId.of ("{{userZone}}")); // {{now.tz(userZone).format("YYYY-MM-DD")}}
LocalDate.now (ZoneId.of ("Pacific/Kiritimati")); // {{now.tz("Pacific/Kiritimati").format("YYYY-MM-DD")}}
Datetime is just the combination of date and time. There are three classes that represent this
concept:
,
and
. Just like with time,
and
are just a
with additional time zone info attached (a
and
respectively, more on that below).
LocalDateTime.of ({{now.tz(userZone).format("YYYY")}},
{{now.tz(userZone).format("M")}},
{{now.tz(userZone).format("D")}},
{{now.tz(userZone).format("H")}},
{{now.tz(userZone).format("m")}}); // {{now.tz(userZone).format("YYYY-MM-DDTHH:mm")}}
LocalDateTime.of ({{now.tz(userZone).format("YYYY")}},
{{now.tz(userZone).format("M")}},
{{now.tz(userZone).format("D")}},
{{now.tz(userZone).format("H")}},
{{now.tz(userZone).format("m")}},
{{now.tz(userZone).format("s")}}); // {{now.tz(userZone).format("YYYY-MM-DDTHH:mm:ss")}}
LocalDateTime.of ({{now.tz(userZone).format("YYYY")}},
{{now.tz(userZone).format("M")}},
{{now.tz(userZone).format("D")}},
{{now.tz(userZone).format("H")}},
{{now.tz(userZone).format("m")}},
{{now.tz(userZone).format("s")}},
{{now.tz(userZone).format("SSS")}}{{nanoMixin}}); // {{now.tz(userZone).format("YYYY-MM-DDTHH:mm:ss.SSS")}}{{nanoMixin}}
LocalDateTime.now (ZoneId.of ("Pacific/Samoa")); // {{now.tz("Pacific/Samoa").format("YYYY-MM-DDTHH:mm:ss.SSS")}}
LocalDateTime.now (ZoneId.of ("{{userZone}}")); // {{now.tz(userZone).format("YYYY-MM-DDTHH:mm:ss.SSS")}}
LocalDateTime.now (ZoneId.of ("Pacific/Chatham")); // {{now.tz("Pacific/Chatham").format("YYYY-MM-DDTHH:mm:ss.SSS")}}
ZonedDateTime.now (ZoneId.of ("{{userZone}}")); // {{now.tz(userZone).format("YYYY-MM-DDTHH:mm:ss.SSSZ")}}[{{userZone}}]
OffsetDateTime.now (
ZoneId.of ("{{userZone}}")); // {{now.tz(userZone).format("YYYY-MM-DDTHH:mm:ss.SSSZ")}}
In java.time
, there are two distinct concepts of time zones: Offsets (
) and zone IDs (
). The difference is that while a
is always a fixed offset from UTC, a
can have different offsets depending on time of year (usually because of daylight savings time).
// normal time, +01:00
LocalDateTime.of (2018,
1,
1,
12,
0).atZone (ZoneId.of ("Europe/Berlin")).getOffset ();
// daylight saving time, +02:00
LocalDateTime.of (2018,
8,
1,
12,
0).atZone (ZoneId.of ("Europe/Berlin")).getOffset ();
Every
is also
(
extends
).
Now to the by far most useful class:
. This is how machines typically work with times but it's not very intuitive to humans. The most important feature of
s is that they do not depend on time zone — it is always the same
across the whole world. Traditionally, there have been two representations of instants: Either epoch-based time (the number of seconds passed since 1970-01-01 00:00:00 UTC, excluding leap seconds) or as an ISO 8601 timestamp like {{now.tz("UTC").format("YYYY-MM-DDTHH:mm:ss")}}Z
. An
is basically a nicer wrapper around
.
Note that while the ISO 8601 timestamp may look like it, an
is not a datetime. You always need a zone to convert from
to a datetime — the ISO 8601 timestamp just uses UTC as the zone.
Instant.now (); // {{now.tz("UTC").format("YYYY-MM-DDTHH:mm:ss")}}Z
Instant.now ().getEpochSecond (); // {{now.format("X")}}
Instant.now ().toEpochMilli ();
// {{now.format("x")}}, same as System.currentTimeMillis()
is the “natural” way of representing time for computers but it is often useless to humans. For storage (for example in a database),
is usually preferred but you may need to use other classes like
when presenting data to a user.
In this example, Alice is entering a datetime, maybe for when a phone conference will take place. Alice lives in the {{userZone}} time zone, but the conference is at the same
across the world, so we will use
for persistence.
// Alice (in time zone {{userZone}}) is entering a datetime
String aliceInput = "{{now.tz(userZone).format("YYYY-MM-DD HH:mm:ss")}}";
ZoneId aliceZone = ZoneId.of ("{{userZone}}");
ZonedDateTime parsed = LocalDateTime .parse(aliceInput, DateTimeFormatter.ofPattern ("yyyy-MM-dd HH:mm:ss"))
.atZone (aliceZone);
Instant instant = parsed.toInstant ();
// instant is stored in the DB
// Bob (in time zone Australia/Sydney) is viewing the datetime Alice has entered
ZoneId bobZone = ZoneId.of ("Australia/Sydney");
ZonedDateTime atZone = ZonedDateTime.ofInstant (instant, bobZone);
output(atZone.format (DateTimeFormatter.ofPattern ("yyyy-MM-dd HH:mm:ss"))); // {{now.tz("Australia/Sydney").format("YYYY-MM-DD HH:mm:ss")}}
When doing arithmetic on time types, you have to be very specific in what you want. When a user says “one day from now” that is a different statement than “24 hours from now” - there might be a daylight savings time switchover in between! This is where
// 2018-03-24T12:00+01:00[Europe/Berlin]
ZonedDateTime zonedA =
LocalDateTime.of (2018,
3,
24,
12,
0).atZone (ZoneId.of ("Europe/Berlin"));
OffsetDateTime offsetA = zonedA.toOffsetDateTime (); // 2018-03-24T12:00+01:00
Instant instantA = zonedA.toInstant (); // 2018-03-24T11:00:00Z
instantA.equals (offsetA.toInstant ()) // true
ZonedDateTime zonedB = zonedA.plusDays (1); // 2018-03-25T12:00+02:00[Europe/Berlin]
OffsetDateTime offsetB = offsetA.plusDays (1); // 2018-03-25T12:00+01:00
instantB.equals (offsetB.toInstant ()) // false
As you can see, even though we called the same method (plusDays
), the two different classes produced different
s (plusHours(24)
behaves the same way). Both are correct – you have to decide which of the two you need for your use case.
I have... | |||||
---|---|---|---|---|---|
|
|
|
|
||
I want... |
|
ldt. |
odt. |
zdt. |
|
|
|
odt. |
zdt. |
||
|
|
ldt. |
zdt. |
||
|
|
ldt. |
odt.
|
For the conversions where a zone is listed, a zone is required. You cannot get around specifying a zone though there may be a sensible default such as
.
(The one exception is
to
, which you can do without, but it's not very useful because it will not pick the right
for most purposes)
is an odd class. It is named date, but it actually wraps a milliseconds timestamp just like
. You can easily convert between the two using
and date.
.
Unfortunately,
was the de facto date/time/instant/everything class of the java ecosystem for a long time. While the various date/time methods such as
were deprecated in java 1.1, many old libraries still use it to represent date/time. A honorable mention also goes to
(and friends) — a class that is made exactly to represent a time but which extends
which is totally unsuited for this. In these cases, libraries typically assume the system time zone (
). You can use this time zone to convert, for example, a
to a
that might be properly recognized by the library you're interacting with, but there is no guarantee.
With java 8, JDBC also supports java.time
types. Interaction with the driver is done through the Object-based methods such as
. JPA 2.2 also supports java.time
by default.
If you are using an older driver, various JDBC time types have factories that can take java.time
objects, for example
. There are also inverse methods available.
{{now.format("X")}}
) has two different dates on different places on earth: it is {{now.tz("Pacific/Samoa").format("dddd, YYYY-MM-DD")}}
on Samoa but {{now.tz("Pacific/Kiritimati").format("dddd, YYYY-MM-DD")}}
on Kiritimati (Christmas Island). Once you have decided on a time zone(ZoneOffset.UTC
might be a good pick — but it depends), you can do LocalDateTime.ofInstant (instant, zone)
{{now.tz(userZone).format("YYYY-MM-DD HH:mm:ss")}}
) and would like the unix time.ZoneId
alone is not sufficient — the same datetime can be two different instants during a daylight savings switchover (or can be no instant at all).
{{now.tz(userZone).format("YYYY-MM-DD HH:mm:ss")}}
. How do I get the java.time
API.