Why do you need to know about Java 8 date and time classes? In a lot of
Java applications it is necessary to calculate business days, or
manipulate dates for display or export: eg. calendar events. There is
also a lot of old Java code which uses the old date and calendar
classes, that needs to be upgraded for when those classes are no longer
supported. Furthermore, Java 8 makes working with date and time easier.
Definitions
Here are some commonly used terms:
- Greenwich Mean Time: abbreviated GMT. This was the zero offset
timezone. It has now been superseded by Universal Coordinated Time
(see next term) although the two terms are often used
interchangeably. See
Wikipedia. - Coordinated Universal Time: abbreviated UTC. This is zero offset
timezone but should be used instead of GMT, because it is more
precise than GMT, and not just to keep the French happy. See
Wikipedia.
Storing date and time values
ZonedDateTime
First is java.time.ZonedDateTime
. This class lets you represent date and
time for anywhere in the world, and it knows the daylight savings
transitions for a great many regions. You can create one by reading the
system clock as shown:
ZonedDateTime now = ZonedDateTime.now();
or by parsing an ISO datetime string as shown in the example below,
which includes zone offset and region. The format is:
yyyy-MM-dd\’T\’HH:mm:ss.nnnnnnnnnXXX\'[\’VV\’]\’ and is called
ISO_ZONED_DATE_TIME
in Java. See the Java 8 Javadoc for
java.time.format.DateTimeFormatter
for more information. The three ZonedDateTime
s below are equivalent.
final String australiaDayStr = "2020-01-26T00:00:00.000000000+11:00[Australia/Sydney]";
final ZonedDateTime australiaDay1 = ZonedDateTime.parse(australiaDayStr);
final ZonedDateTime australiaDay2 = ZonedDateTime.parse(australiaDayStr, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.nnnnnnnnnXXX'['VV']'"));
final ZonedDateTime australiaDay3 = ZonedDateTime.parse(australiaDayStr, DateTimeFormatter.ISO_ZONED_DATE_TIME);
OffsetDateTime
If you don\’t know the zone region for a datetime value, but you still
want to handle zone offsets then Java provides java.time.OffsetDateTime
.
Because it lacks a zone region, it doesn\’t know the daylight savings
transitions. You can create one by reading the system clock as shown:
OffsetDateTime now = OffsetDateTime.now();
or by parsing an ISO datetime string as shown in the example below,
which includes zone offset but not the region. The format is:
yyyy-MM-dd\’T\’HH:mm:ss.nnnnnnnnnXXX and is called
ISO_OFFSET_DATE_TIME
in Java. See the Java 8 Javadoc for
java.time.format.DateTimeFormatter
for more information. The three OffsetDateTimes below are equivalent.
final String australiaDayStr = "2020-01-26T00:00:00.000000000+11:00";
final OffsetDateTime australiaDay1 = OffsetDateTime.parse(australiaDayStr);
final OffsetDateTime australiaDay2 = OffsetDateTime.parse(australiaDayStr, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.nnnnnnnnnXXX"));
final OffsetDateTime australiaDay3 = OffsetDateTime.parse(australiaDayStr, DateTimeFormatter.ISO_OFFSET_DATE_TIME;
OffsetDateTime
is convertable to ZonedDateTime
via the method:
final ZonedDateTime nowZdt = OffsetDateTime.now().toZonedDateTime();
but be aware that the converted object will not contain the daylight
savings transitions because you didn\’t supply a zone id! If you want to
include the zone id, then use this technique:
final ZonedDateTime nowZdt = OffsetDateTime.now().atZoneSameInstant(ZoneId.of("Australia/Sydney"));
LocalDateTime
If you don\’t need to deal with offsets and regions then Java provides
java.time.LocalDateTime
. This class stores dates and times that are not
connected to a particular place. You can create one by reading the
system clock as shown:
LocalDateTime now = LocalDateTime.now();
or by parsing an ISO datetime string as shown in the example below,
which includes neither the zone offset nor the region. The format is:
yyyy-MM-dd\’T\’HH:mm:ss.nnnnnnnnn and is called ISO_LOCAL_DATE_TIME
in
Java. See the Java 8 Javadoc for
java.time.format.DateTimeFormatter
for more information. The three LocalDateTimes below are equivalent.
final String australiaDayStr = "2020-01-26T00:00:00.000000000";
final LocalDateTime australiaDay1 = LocalDateTime.parse(australiaDayStr);
final LocalDateTime australiaDay2 = LocalDateTime.parse(australiaDayStr, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.nnnnnnnnn"));
final LocalDateTime australiaDay3 = LocalDateTime.parse(australiaDayStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
LocalDateTime
can be combined with a Zone Id to create a ZonedDateTime
via:
final LocalDateTime australiaDayLdt = LocalDateTime.parse("2020-01-26T00:00:00.000000000");
final ZonedDateTime australiaDayZdt = ZonedDateTime.of(australiaDayLdt, ZoneId.of("Australia/Sydney"));
LocalDate and LocalTime
If you only need to deal with whole days then the java.time.LocalDate
class is very handy. Similarly if you only need to deal with times, then
Java provides the java.time.LocalTime
class. Both can be created by
reading the system clock, or by parsing a string in the correct format.
LocalDate today = LocalDate.now();
LocalTime now = LocalTime.now();
These two classes can be combined with a Zone Id to create a
ZonedDateTime
:
LocalDate australiaDay = LocalDate.parse("2020-01-26");
LocalTime midnight = LocalTime.parse("00:00:00");
ZonedDateTime australiaDayZdt = ZonedDateTime.of(australiaDay, midnight, ZoneId.of("Australia/Sydney"));
Be careful when doing calculations on LocalDate
and LocalTime
! If you
try, for example, to calculate the number of hours difference between
two LocalDate
s, the code might compile but then fail at runtime, because
LocalDate
has no hours component and doesn\’t know how to work with
them.
Instant
Java represents Unix time with java.time.Instant
. Objects of this class
store the number of seconds since midnight, 1st Jan 1970 UTC, to
nanosecond (10-9 s) precision. Instants are always in UTC and are not
region-aware. An instant can be created by reading from the system
clock, parsing a datetime string, or reading in a whole number of
seconds, for example:
Instant now = Instant.now();
Instant australiaDayUnix = Instant.parse("2020-01-25T13:00:00Z");
Instant australiaDayUnix2 = Instant.ofEpochSecond(1579957200);
Note the use of the capital \’Z\’ at the end of the datetime string on
line 2, which denotes UTC.
An instant can be combined with a Zone Id to create a ZonedDateTime
as
shown below:
ZonedDateTime australiaDayZdt = ZonedDateTime.ofInstant(australiaDayUnix, ZoneId.of("Australia/Sydney"));
Test Your Knowledge
Question 1
What happens if you omit the zone information when you try to create a
ZonedDateTime
, as shown in the example below?
ZonedDateTime australiaDay = ZonedDateTime.parse("2020-01-26T00:00:00");
An exception, java.time.format.DateTimeParseException
, is thrown at
runtime. The three components necessary to create a ZonedDateTime
are:
- date
- time
- offset
The zone region (\"[Australia/Sydney]\") is actually optional, but
without it the ZonedDateTime
will not contain the daylight savings
transitions.
Question 2
What happens if the offset is wrong for the region? eg the offset for
Sydney during summer is UTC+11:00 but the offset given in the example
below is UTC-05:00.
ZonedDateTime weird = ZonedDateTime.parse("2020-01-26T00:00:00-05:00[Australia/Sydney]");
A ZonedDateTime
is created! Java uses the region to decide what the
offset should be and adjusts the time accordingly. So in the above
example:
- The time is first taken at the offset (UTC-05:00).
-
The time is then adjusted for the region rules, which say the offset
should be UTC+11:00:+11 hours – –5 hours = +16 hours to adjust by.
So the resulting
ZonedDateTime
is equivalent to:
ZonedDateTime.parse("2020-01-26T16:00:00+11:00[Australia/Sydney]");
Manipulating Dates and Times
Suppose you want to watch the final stage of Le Tour de France. At the
time of writing (Tuesday 15th Sep 2020), the race starts at 11:30 pm
this Sunday, Sydney time. You could do that many ways, but here is one
way that illustrates a few of the classes that we\’ve already discussed:
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.ZonedDateTime;
import java.time.ZoneId;
...
ZonedDateTime tdfFinalSydney = LocalDate.now()
.with(TemporalAdjusters.next(DayOfWeek.SUNDAY))
.atTime(23,30,0)
.atZone(ZoneId.of("Australia/Sydney"));
Line 1 starts with today\’s date in a LocalDate object. Line 2 then uses
Java\’s very handy
TemporalAdjusters
class with the DayOfWeek.SUNDAY
enum to advance the date to this Sunday.
TemporalAdjusters
provides methods to calculate the next and previous
days of the week, the first and last days of the month and the year, and
the first date of next year, amongst others. Do check it out. Line 3
then adds a time component in 24 h time, which is 11:30 pm. This method
returns a LocalDateTime object. Finally, line 4 adds a zone component to
internationalise the date and time in a ZonedDateTime
object. The
classes LocalDate
, LocalTime
, LocalDateTime
, OffsetDateTime
, and
ZonedDateTime
share a lot of methods in common and can be transformed
from one to the other, so it is very easy work with them once you learn
the patterns.
Note here that the Java 8 date and time objects are immutable! Each of
the methods which operates on a Java 8 date or time object (in the
java.time
package), such as is shown in the example above, creates a new
object and returns it. In the four lines of code above, we performed
four operations and created four new objects, although we only retained
the last one created. This design seems strange compared to the old Java
Date
and Calendar
classes, but it actually simplifies the programming
model greatly, and lends itself towards a functional programming style
of working, which is the direction that Java has been going recently.
When is the Final of Le Tour De France, Paris Time?
Now suppose that you have a friend in Paris and that you had planned to
watch the race together. You would calculate the correct time in Paris
for the start of the race as follows:
import java.time.ZonedDateTime;
import java.time.ZoneId;
...
ZonedDateTime tdfFinalParis = tdfFinalSydney.withZoneSameInstant(ZoneId.of("Europe/Paris"));
You can prove that two objects tdfFinalSydney
and tdfFinalParis
, which
represent the start of the race in Sydney and Paris respectively,
represent the same moment in time and have no time difference between
them. Java provides at least two ways to do this, first using the
ChronoUnit.HOURS
enum:
import org.junit.jupiter.api.Assertions; //only import this in a JUnit test, not in your main code!
import java.time.temporal.ChronoUnit;
import java.time.ZonedDateTime;
...
final long difference = ChronoUnit.HOURS.between(tdfFinalParis, tdfFinalSydney);
Assertions.assertEquals(0, difference);
and secondly using the
Duration
class as follows:
import org.junit.jupiter.api.Assertions; //only import this in a JUnit test, not in your main code!
import java.time.Duration;
import java.time.ZonedDateTime;
...
Duration difference = Duration.between(tdfFinalParis, tdfFinalSydney);
Assertions.assertEquals(0, difference.getSeconds());
Be Careful!
Both the between methods above accept objects that implement the
Temporal
interface, which includes instances of the date and time classes we have
already introduced. Be careful, however, because not all of those
classes will work here, even if the code compiles! For example, if you
try to calculate the difference between two LocalDate
objects as a
Duration
, you will get an UnsupportedTemporalTypeException
at runtime,
with a message \"Unsupported unit: Seconds\", because LocalDate
is does
not have a time component and does not know what seconds are. eg:
import org.junit.jupiter.api.Assertions; //only import this in a JUnit test, not in your main code!
import java.time.Duration;
import java.time.LocalDate;
import java.time.temporal.UnsupportedTemporalTypeException;
...
Assertions.assertThrows(UnsupportedTemporalTypeException.class, () -> {
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plusDays(1);
Duration.between(today, tomorrow); //throws here!
});
Similarly, be careful when comparing Temporal
s of different implementing
classes. For example, comparing a ZonedDateTime
with a LocalDateTime
doesn\’t work with the ZonedDateTime
on the left hand side of the
comparison, because the LocalDateTime
doesn\’t know about zone regions.
eg the following example compiles but fails with a DateTimeException
,
\"Unable to obtain ZonedDateTime from TemporalAccessor…\":
import org.junit.jupiter.api.Assertions; //only import this in a JUnit test, not in your main code!
import java.time.DateTimeException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
...
Assertions.assertThrows(DateTimeException.class, () -> {
ZonedDateTime todayZdt = ZonedDateTime.now();
LocalDateTime laterLdt = todayZdt.toLocalDateTime().plusMinutes(1);
Duration.between(todayZdt, laterLdt); // throws here!
});
If you swap the comparison order such that the LocalDateTime
is on the
left hand side, however, the comparison works:
import org.junit.jupiter.api.Assertions; //only import this in a JUnit test, not in your main code!
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
...
LocalDateTime todayLdt = LocalDateTime.now();
ZonedDateTime laterZdt = todayLdt.plusMinutes(1).atZone(ZoneId.of("Australia/Sydney"));
Assertions.assertEquals("PT1M", Duration.between(todayLdt, laterZdt).toString())
Throw a Party, not an Exception!
Back to Le Tour de France. Suppose you want to generate an invitation
for a party via your favourite video conferencing application, and send
it to your friend in Paris. The party starts one hour before the race
starts. Java provides at least three ways for you to do this:
import java.time.temporal.ChronoUnit;
import java.time.Duration;
import java.time.ZonedDateTime;
...
ZonedDateTime zoomPartyParis1 = tdfFinalParis.minusHours(1);
ZonedDateTime zoomPartyParis2 = tdfFinalParis.minus(Duration.ofHours(1));
ZonedDateTime zoomPartyParis3 = tdfFinalParis.minus(1, ChronoUnit.HOURS);
The date and time classes have many methods for adding and subtracting
time, a few of which are shown above. They range from minusNanos(long)
to plusYears(long)
, as well as general purpose methods which accept a
Period
(days, months, years or longer) or a Duration
(hours, minutes, seconds,
or shorter), or an arbitrary ChronoUnit
(from nanoseconds to millenia).
Now suppose that you want to generate a reminder for the party the day
before, so that you have sufficient time to go shopping for drinks and
snacks. Java provides at least three ways for you to do this:
import java.time.temporal.ChronoUnit;
import java.time.Period;
import java.time.ZonedDateTime;
...
ZonedDateTime zoomPartyReminderParis1 = zoomPartyParis1.minusDays(1);
ZonedDateTime zoomPartyReminderParis2 = zoomPartyParis1.minus(Period.ofDays(1));
ZonedDateTime zoomPartyReminderParis3 = zoomPartyParis1.minus(1, ChronoUnit.DAYS);
Old to New
When Oracle added the Java 8 date and time classes to the Java standard
library, it deprecated many of the methods in the old Date
and Calendar
classes, and also made the old classes aware of some of the new classes,
to enable easy conversion between them. So if you need to support APIs
which rely on the old classes, you can still expose the old classes in
your API while using the new classes underneath. Note, however, that the
new classes have an entirely different set of methods, so your old code
will need to be reengineered heavily. Java lets you convert between old
and new classes as shown in the example below, with Date
being replaced
by Instant
:
import java.util.Date;
import java.time.Instant;
...
Date now = Date.from(Instant.now());
Instant now2 = new Date().toInstant();
and GregorianCalendar
being replaced by ZonedDateTime
:
import java.util.GregorianCalendar;
import java.time.ZonedDateTime;
...
GregorianCalendar tdfFinalSydneyGregorian = GregorianCalendar.from(tdfFinalSydney);
ZonedDateTime tdfFinalSydney2 = tdfFinalSydneyGregorian.toZonedDateTime();
Further Reading
Sierra, Bates, and Robson, OCP Java SE 8 Programmer II Exam Guide,
Oracle Press, 2018, pp.207-220
https://docs.oracle.com/javase/8/docs/api/
Acknowledgements
The author acknowledges the traditional custodians of the Daruk and the
Eora People and pays respect to the Elders past and present.
Oracle® and Java are registered trademarks of Oracle and/or its
affiliates.