An introduction to java.time

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 java.util.Calendar – 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 ( Instant , 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”.

Concepts

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.

Honolulu

({{now.tz("Pacific/Honolulu").format("Z")}})

{{now.tz("Pacific/Honolulu").format("HH:mm")}} {{userZone.substring(userZone.indexOf("/") + 1)}}

({{now.tz(userZone).format("Z")}})

{{now.tz(userZone).format("HH:mm")}} Kathmandu

({{now.tz("Asia/Kathmandu").format("Z")}})

{{now.tz("Asia/Kathmandu").format("HH:mm")}} Sydney

({{now.tz("Australia/Sydney").format("Z")}})

{{now.tz("Australia/Sydney").format("HH:mm")}}

java.time offers the LocalTime and OffsetTime 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}} );



LocalTime.now ( ZoneId.of ( "Pacific/Honolulu" ));

LocalTime.now ( ZoneId.of ( "{{userZone}}" ));

LocalTime.now ( ZoneId.of ( "Asia/Kathmandu" ));

LocalTime.now ( ZoneId.of ( "Australia/Sydney" ));



OffsetTime.now ( ZoneId.of ( "Pacific/Honolulu" ));

OffsetTime.now ( ZoneId.of ( "{{userZone}}" ));

OffsetTime.now ( ZoneId.of ( "Asia/Kathmandu" ));

OffsetTime.now ( ZoneId.of ( "Australia/Sydney" ));



As you can see, the only difference between the two classes is that OffsetTime includes offset information (more on offsets and zones below). In the majority of cases LocalTime is sufficient.

Date

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!

Samoa

{{now.tz("Pacific/Samoa").format("dddd, YYYY-MM-DD")}} {{userZone.substring(userZone.indexOf("/") + 1)}}

{{now.tz(userZone).format("dddd, YYYY-MM-DD")}} Christmas Island

{{now.tz("Pacific/Kiritimati").format("dddd, YYYY-MM-DD")}}

To represent dates, you use the class:

( {{now.tz(userZone).format("YYYY")}} , {{now.tz(userZone).format("M")}} , {{now.tz(userZone).format("D")}} );



( ZoneId.of ( "Pacific/Samoa" ));

( ZoneId.of ( "{{userZone}}" ));

( ZoneId.of ( "Pacific/Kiritimati" ));



Datetime

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 ZoneOffset and ZoneId respectively, more on that below).

( {{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")}} , {{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")}} , {{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}} );



( ZoneId.of ( "Pacific/Samoa" ));

( ZoneId.of ( "{{userZone}}" ));

( ZoneId.of ( "Pacific/Chatham" ));



( ZoneId.of ( "{{userZone}}" ));

( ZoneId.of ( "{{userZone}}" ));



Zone

In java.time , there are two distinct concepts of time zones: Offsets ( ZoneOffset ) and zone IDs ( ZoneId ). The difference is that while a ZoneOffset is always a fixed offset from UTC, a ZoneId can have different offsets depending on time of year (usually because of daylight savings time).



( 2018 , 1 , 1 , 12 , 0 ). ( ZoneId.of ( "Europe/Berlin" )). ();





( 2018 , 8 , 1 , 12 , 0 ). ( ZoneId.of ( "Europe/Berlin" )). ();

Every ZoneOffset is also ZoneId ( ZoneOffset extends ZoneId ).

Instant

Now to the by far most useful class: Instant . This is how machines typically work with times but it's not very intuitive to humans. The most important feature of Instant s is that they do not depend on time zone — it is always the same Instant 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 Instant is basically a nicer wrapper around System.currentTimeMillis () .

Note that while the ISO 8601 timestamp may look like it, an Instant is not a datetime. You always need a zone to convert from Instant to a datetime — the ISO 8601 timestamp just uses UTC as the zone.

Instant.now ();

Instant.now (). getEpochSecond ();

Instant.now (). toEpochMilli ();

Principles

Instants for computers, DateTime for humans

Instant is the “natural” way of representing time for computers but it is often useless to humans. For storage (for example in a database), Instant 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 Instant across the world, so we will use Instant for persistence.



String aliceInput = "{{now.tz(userZone).format("YYYY-MM-DD HH:mm:ss")}}" ;

ZoneId aliceZone = ZoneId.of ( "{{userZone}}" );

ZonedDateTime parsed = .parse(aliceInput, ( "yyyy-MM-dd HH:mm:ss" ))

. (aliceZone);

Instant instant = parsed. ();







ZoneId bobZone = ZoneId.of ( "Australia/Sydney" );

ZonedDateTime atZone = (instant, bobZone);

output(atZone. ( ( "yyyy-MM-dd HH:mm:ss" )));

Arithmetic

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 and really show their difference:



zonedA = ( 2018 , 3 , 24 , 12 , 0 ). ( ZoneId.of ( "Europe/Berlin" ));

offsetA = zonedA. ();

Instant instantA = zonedA. ();

instantA. equals (offsetA. ())



zonedB = zonedA. ( 1 );

offsetB = offsetA. ( 1 );

instantB. equals (offsetB. ())



As you can see, even though we called the same method ( plusDays ), the two different classes produced different Instant s ( plusHours(24) behaves the same way). Both are correct – you have to decide which of the two you need for your use case.

Conversion

I have... Instant I want... Instant ldt. ( ZoneOffset ) odt. () zdt. () (instant, ZoneId ) odt. () zdt. () (instant, ZoneId ) ldt. ( ZoneOffset ) zdt. () (instant, ZoneId ) ldt. ( ZoneId ) odt. ( ZoneId )

odt. ( ZoneId )

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 ZoneOffset.UTC . (The one exception is to , which you can do without, but it's not very useful because it will not pick the right ZoneId for most purposes)

Compatibility

java.util.Date

is an odd class. It is named date, but it actually wraps a milliseconds timestamp just like Instant . You can easily convert between the two using ( Instant ) 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 java.sql.Time (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 ( ZoneId.systemDefault () ). 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.

SQL

With java 8, JDBC also supports java.time types. Interaction with the driver is done through the Object-based methods such as ResultSet.getObject . 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 java.sql.Time.valueOf(LocalTime) . There are also inverse methods available.

FAQ (or: no, you really can't do that)