Chapter ELEVEN
The Date/Time API


Exam Objectives

Manipulate date, time, duration, period, instant and time-zone objects including daylight saving time using Date-Time API.

Chapter Content


Core Date/Time Classes

Java 8 introduced a new Date/Time API in the java.time package. The classes that belong to this API are immutable and thread-safe.

In this section, we’ll review the following classes:

With the exception of Instant, these classes don’t store or represent a time zone.

Also, LocalDate, LocalTime, LocalDateTime, and Instant implement the interface java.time.temporal.Temporal, so they all have similar methods. Period and Duration implement the interface java.time.temporal.TemporalAmount, which also makes them very similar.

Here’s a diagram to help you visualize the classes of the Date/Time API:

┌─────────────────────────────────────────────────────────┐
│                  Java Date/Time API                     │
│                                                         │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐  │
│  │  LocalDate  │    │  LocalTime  │    │LocalDateTime│  │
│  │ (Date only) │    │ (Time only) │    │(Date + Time)│  │
│  └──────┬──────┘    └──────┬──────┘    └──────┬──────┘  │
│         │                  │                  │         │
│         └──────────────────┼──────────────────┘         │
│                            │                            │
│                     ┌──────┴──────┐                     │
│                     │   Instant   │                     │
│                     │ (Time-stamp)│                     │
│                     └──────┬──────┘                     │
│                            │                            │
│              ┌─────────────┴─────────────┐              │
│              │                           │              │
│        ┌─────┴─────┐               ┌─────┴─────┐        │
│        │  Period   │               │ Duration  │        │
│        │(Date-based│               │(Time-based│        │
│        │  amount)  │               │  amount)  │        │
│        └───────────┘               └───────────┘        │
│                                                         │
└─────────────────────────────────────────────────────────┘

The LocalDate Class

The key to learning how to use this class is to understand that it holds the year, month, day, and derived information of a date. All of its methods use this information or have a version to work with each of these components.

The following are the most important methods of this class.

To create an instance, we can use the static method of:

// With year (-999999999 to 999999999), month (1 to 12), day of the month (1 - 31)
LocalDate newYear2001 = LocalDate.of(2001, 1, 1);
// This version uses the enum java.time.Month
LocalDate newYear2002 = LocalDate.of(2002, Month.JANUARY, 1);

Notice that, unlike java.util.Date, months start from one. If you try to create a date with invalid values (like February 29 on a non-leap year), an exception will be thrown. For today’s date, use now():

LocalDate today = LocalDate.now();

Once we have an instance of LocalDate, we can get the year, the month, and the day with methods like the following:

int year = today.getYear();
int month = today.getMonthValue();
Month monthAsEnum = today.getMonth(); // As an enum: JANUARY, FEBRUARY, etc.
int dayYear = today.getDayOfYear();
int dayMonth = today.getDayOfMonth();
DayOfWeek dayWeekEnum = today.getDayOfWeek(); // As an enum: MONDAY, TUESDAY, etc.

We can also use the get method:

int get(java.time.temporal.TemporalField field); // value as int
long getLong(java.time.temporal.TemporalField field); // value as long

This method takes an implementation of the interface java.time.temporal.TemporalField to access a specific field of a date. java.time.temporal.ChronoField is an enumeration that implements this interface, so we can have, for example:

int year2 = today.get(ChronoField.YEAR);
int month2 = today.get(ChronoField.MONTH_OF_YEAR);
int dayYear2 = today.get(ChronoField.DAY_OF_YEAR);
int dayMonth2 = today.get(ChronoField.DAY_OF_MONTH);
int dayWeek = today.get(ChronoField.DAY_OF_WEEK);
long dayEpoch = today.getLong(ChronoField.EPOCH_DAY);

The supported values for ChronoField are:

Using an unsupported value will throw an exception. The same is true when getting a value that doesn’t fit into an int with get(TemporalField).

To compare a LocalDate against another instance, we have three methods and another one for leap years:

boolean after = newYear2001.isAfter(newYear2002); // false
boolean before = newYear2001.isBefore(newYear2002); // true
boolean equal = newYear2001.equals(newYear2002); // false
boolean leapYear = newYear2001.isLeapYear(); // false

Once an instance of this class is created, it cannot be modified, but we can create another instance from an existing one.

One way is by using the with() method and its variations:

LocalDate newYear2003 = newYear2001.with(ChronoField.YEAR, 2003);
LocalDate newYear2004 = newYear2001.withYear(2004);
LocalDate december2001 = newYear2001.withMonth(12);
LocalDate february2001 = newYear2001.withDayOfYear(32);
// Since these methods return a new instance, we can chain them!
LocalDate xmas2001 = newYear2001.withMonth(12).withDayOfMonth(25);

Another way is by adding or subtracting years, months, days, or even weeks:

// Adding
LocalDate newYear2005 = newYear2001.plusYears(4);
LocalDate march2001 = newYear2001.plusMonths(2);
LocalDate january15_2001 = newYear2001.plusDays(14);
LocalDate lastWeekJanuary2001 = newYear2001.plusWeeks(3);
LocalDate newYear2006 = newYear2001.plus(5, ChronoUnit.YEARS);

// Subtracting
LocalDate newYear2000 = newYear2001.minusYears(1);
LocalDate nov2000 = newYear2001.minusMonths(2);
LocalDate dec30_2000 = newYear2001.minusDays(2);
LocalDate lastWeekDec2000 = newYear2001.minusWeeks(1);
LocalDate newYear1999 = newYear2001.minus(2, ChronoUnit.YEARS);

Notice that the plus and minus methods take a java.time.temporal.ChronoUnit enumeration, which is different from java.time.temporal.ChronoField. The supported values are:

Finally, the method toString() returns the date in the format uuuu-MM-dd:

System.out.println(newYear2001.toString()); // Prints 2001-01-01

The LocalTime Class

The key to learning how to use this class is to keep in mind that it holds the hour, minutes, seconds, and nanoseconds. All of its methods use this information or have a version to work with each of them.

The following are the most important methods of this class. As you can see, they are the same (or very similar) methods as LocalDate, adapted to work with time (hours, minutes, seconds) instead of date (days, months, years).

To create an instance, we can use the static method of:

// With hour (0-23) and minutes (0-59)
LocalTime fiveThirty = LocalTime.of(5, 30);
// With hour, minutes, and seconds (0-59)
LocalTime noon = LocalTime.of(12, 0, 0);
// With hour, minutes, seconds, and nanoseconds (0-999_999_999)
LocalTime almostMidnight = LocalTime.of(23, 59, 59, 999_999_999);

If you try to create a time with an invalid value (like LocalTime.of(24, 0)), an exception will be thrown. To get the current time, use now():

LocalTime now = LocalTime.now();

Once we have an instance of LocalTime, we can get the hour, the minutes, and other information with methods like the following:

int hour = now.getHour();
int minute = now.getMinute();
int second = now.getSecond();
int nanosecond = now.getNano();

We can also use the get() method:

int value = now.get(java.time.temporal.TemporalField field); // value as int
long valueLong = now.getLong(java.time.temporal.TemporalField field); // value as long

Just like in the case of LocalDate, we can have, for example:

int hourAMPM = now.get(ChronoField.HOUR_OF_AMPM); // 0 - 11
int hourDay = now.get(ChronoField.HOUR_OF_DAY); // 0 - 23
int minuteDay = now.get(ChronoField.MINUTE_OF_DAY); // 0 - 1,439
int minuteHour = now.get(ChronoField.MINUTE_OF_HOUR); // 0 - 59
int secondDay = now.get(ChronoField.SECOND_OF_DAY); // 0 - 86,399
int secondMinute = now.get(ChronoField.SECOND_OF_MINUTE); // 0 - 59
long nanoDay = now.getLong(ChronoField.NANO_OF_DAY); // 0-86_399_999_999
int nanoSecond = now.get(ChronoField.NANO_OF_SECOND); // 0-999_999_999

The supported values for ChronoField are:

Using a different value will throw an exception. The same is true when getting a value that doesn’t fit into an int using get(TemporalField).

To check a time object against another one, we have three methods:

boolean after = fiveThirty.isAfter(noon); // false
boolean before = fiveThirty.isBefore(noon); // true
boolean equal = noon.equals(almostMidnight); // false

Like LocalDate, once an instance of LocalTime is created we cannot modify it, but we can create another instance from an existing one.

One way is through the with method and its versions:

LocalTime ten = noon.with(ChronoField.HOUR_OF_DAY, 10);
LocalTime eight = noon.withHour(8);
LocalTime twelveThirty = noon.withMinute(30);
LocalTime thirtyTwoSeconds = noon.withSecond(32);
// Since these methods return a new instance, we can chain them!
LocalTime secondsNano = noon.withSecond(20).withNano(999_999);

Of course, another way is by adding or subtracting hours, minutes, seconds, or nanoseconds:

// Adding
LocalTime sixThirty = fiveThirty.plusHours(1);
LocalTime fiveForty = fiveThirty.plusMinutes(10);
LocalTime plusSeconds = fiveThirty.plusSeconds(14);
LocalTime plusNanos = fiveThirty.plusNanos(99_999_999);
LocalTime sevenThirty = fiveThirty.plus(2, ChronoUnit.HOURS);

// Subtracting
LocalTime fourThirty = fiveThirty.minusHours(1);
LocalTime fiveTen = fiveThirty.minusMinutes(20);
LocalTime minusSeconds = fiveThirty.minusSeconds(2);
LocalTime minusNanos = fiveThirty.minusNanos(1);
LocalTime fiveTwenty = fiveThirty.minus(10, ChronoUnit.MINUTES);

Notice that the plus and minus versions take a java.time.temporal.ChronoUnit enumeration, which is different from java.time.temporal.ChronoField. The supported values are:

Finally, the method toString() returns the time in the format HH:mm:ss.SSSSSSSSS, omitting the parts with value zero (for example, just returning HH:mm if it has zero seconds/nanoseconds):

System.out.println(fiveThirty.toString()); // Prints 05:30

This should cover all the necessary corrections and improvements for the provided text.

The LocalDateTime Class

The key to learning how to use this class is to remember that it combines LocalDate and LocalTime classes.

It represents both a date and a time, with information like year, month, day, hours, minutes, seconds, and nanoseconds. Other fields, such as day of the year, day of the week, and week of year can also be accessed.

To create an instance, we can use either the static method of() or from a LocalDate or LocalTime instance:

// Setting seconds and nanoseconds to zero
LocalDateTime dt1 = LocalDateTime.of(2024, 9, 19, 14, 5);
// Setting nanoseconds to zero
LocalDateTime dt2 = LocalDateTime.of(2024, 9, 19, 14, 5, 20);
// Setting all fields
LocalDateTime dt3 = LocalDateTime.of(2024, 9, 19, 14, 5, 20, 9);
// Assuming this date
LocalDate date = LocalDate.now();
// And this time
LocalTime time = LocalTime.now();
// Combine the above date with the given time like this
LocalDateTime dt4 = date.atTime(14, 30, 59, 999999);
// Or this
LocalDateTime dt5 = date.atTime(time);
// Combine this time with the given date. Notice that LocalTime
// only has this method to be combined with a LocalDate
LocalDateTime dt6 = time.atDate(date);

If you try to create an instance with an invalid value or date, an exception will be thrown. To get the current date/time use now():

LocalDateTime now = LocalDateTime.now();

Once we have an instance of LocalDateTime, we can get the information with the methods we know from LocalDate and LocalTime, such as:

int year = now.getYear();
int dayYear = now.getDayOfYear();
int hour = now.getHour();
int minute = now.getMinute();

We can also use the get() method:

int get(java.time.temporal.TemporalField field)
long getLong(java.time.temporal.TemporalField field)

For example:

int month = now.get(ChronoField.MONTH_OF_YEAR);
int minuteHour = now.get(ChronoField.MINUTE_OF_HOUR);

The supported values for ChronoField are:

Using a different value will throw an exception. The same is true when getting a value that doesn’t fit into an int with get(TemporalField).

To check a LocalDateTime object against another one, we have three methods:

boolean after = now.isAfter(dt1); // true
boolean before = now.isBefore(dt1); // false
boolean equal = now.equals(dt1); // false

Once an instance of LocalTime is created, we cannot modify it, but we can create another instance from an existing one.

One way is through the with method and its versions:

LocalDateTime dt7 = now.with(ChronoField.HOUR_OF_DAY, 10);
LocalDateTime dt8 = now.withMonth(8);
// Since these methods return a new instance, we can chain them!
LocalDateTime dt9 = now.withYear(2013).withMinute(0);

Another way is by adding or subtracting years, months, days, weeks, hours, minutes, seconds, or nanoseconds:

// Adding
LocalDateTime dt10 = now.plusYears(4);
LocalDateTime dt11 = now.plusWeeks(3);
LocalDateTime dt12 = now.plus(2, ChronoUnit.HOURS);

// Subtracting
LocalDateTime dt13 = now.minusMonths(2);
LocalDateTime dt14 = now.minusNanos(1);
LocalDateTime dt15 = now.minus(10, ChronoUnit.SECONDS);

In this case, the supported values for ChronoUnit are:

Finally, the method toString() returns the date-time in the format uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSS, omitting the parts with value zero, for example:

System.out.println(dt1.toString()); // Prints 2024-09-19T14:05

The Instant Class

Although in practical terms, a LocalDateTime instance represents an instant in the timeline, there is another class that may be more appropriate.

The java.time.Instant class represents an instant in the number of seconds that have passed since the epoch, a convention used in UNIX/POSIX systems and set at midnight of January 1, 1970 UTC time.

From that date, time is measured in 86,400 seconds per day. This information is stored as a long. The class also supports nanosecond precision, stored as an int.

You can create an instance of this class with the following methods:

// Setting seconds
Instant fiveSecondsAfterEpoch = Instant.ofEpochSecond(5);
// Setting seconds and nanoseconds (can be negative)
Instant sixSecTwoNanBeforeEpoch = Instant.ofEpochSecond(-6, -2);
// Setting milliseconds after (can be before also) epoch
Instant fiftyMilliSecondsAfterEpoch = Instant.ofEpochMilli(50);

For the current instance of the system clock use:

Instant now = Instant.now();

Once we have an instance of Instant, we can get the information with the following methods:

long seconds = now.getEpochSecond(); // Gets the seconds
int nanos1 = now.getNano(); // Gets the nanoseconds
// Gets the value as an int
int millis = now.get(ChronoField.MILLI_OF_SECOND);
// Gets the value as a long
long nanos2 = now.getLong(ChronoField.NANO_OF_SECOND);

The supported ChronoField values are:

Using any other value will throw an exception. The same is true when getting a value that doesn’t fit into an int using get(TemporalField).

To check an Instant object against another one, we have three methods:

boolean after = now.isAfter(fiveSecondsAfterEpoch); // true
boolean before = now.isBefore(fiveSecondsAfterEpoch); // false
boolean equal = now.equals(fiveSecondsAfterEpoch); // false

Once an instance of this object is created, we cannot modify it, but we can create another instance from an existing one.

One way is by using the with method:

Instant i1 = now.with(ChronoField.NANO_OF_SECOND, 10);

Another way is by adding or subtracting seconds, milliseconds, or nanoseconds:

// Adding
Instant i10 = now.plusSeconds(400);
Instant i11 = now.plusMillis(98622200);
Instant i12 = now.plusNanos(300013890);
Instant i13 = now.plus(2, ChronoUnit.MINUTES);

// Subtracting
Instant i14 = now.minusSeconds(2);
Instant i15 = now.minusMillis(1);
Instant i16 = now.minusNanos(1);
Instant i17 = now.minus(10, ChronoUnit.SECONDS);

The supported ChronoUnit values are:

Finally, the method toString() returns the instance in the format uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSS, for example:

// Prints 1970-01-01T00:00:00.050Z
System.out.println(fiftyMilliSecondsAfterEpoch.toString());

Notice that it contains zone time information (Z). This is because Instant represents a point in time from the epoch of 1970-01-01Z in the UTC zone time.

The Period Class

The java.time.Period class represents an amount of time in terms of years, months, and days.

You can create an instance of this class with the following methods:

// Setting years, months, days (can be negative)
Period period5y4m3d = Period.of(5, 4, 3);
// Setting days (can be negative), years and months will be zero
Period period2d = Period.ofDays(2);
// Setting months (can be negative), years and days will be zero
Period period2m = Period.ofMonths(2);
// Setting weeks (can be negative). The resulting period will
// be in days (1 week = 7 days). Years and months will be zero
Period period14d = Period.ofWeeks(2);
// Setting years (can be negative), days and months will be zero
Period period2y = Period.ofYears(2);

A Period can also be thought of as the difference between two LocalDates. Luckily, there’s a method that supports this concept:

LocalDate march2003 = LocalDate.of(2003, 3, 1);
LocalDate may2003 = LocalDate.of(2003, 5, 1);
Period dif = Period.between(march2003, may2003); // 2 months

The start date is included, but not the end date.

Be careful about how the date is calculated.

First, complete months are counted, and then the remaining number of days is calculated. The number of months is then split into years (1 year equals 12 months). A month is considered if the end day of the month is greater than or equal to the start day of the month.

The result of this method can be a negative period if the end is before the start (year, month, and day will have a negative sign).

Here are some examples:

// dif1 will be 1 year 2 months 2 days
Period dif1 = Period.between(LocalDate.of(2000, 2, 10), LocalDate.of(2001, 4, 12));
// dif2 will be 25 days
Period dif2 = Period.between(LocalDate.of(2013, 5, 9), LocalDate.of(2013, 6, 3));
// dif3 will be -2 years -3 days
Period dif3 = Period.between(LocalDate.of(2014, 11, 3), LocalDate.of(2012, 10, 31));

Once we have an instance of Period, we can get the information with the following methods:

int days = period5y4m3d.getDays();
int months = period5y4m3d.getMonths();
int year = period5y4m3d.getYears();
long days2 = period5y4m3d.get(ChronoUnit.DAYS);

Notice that the get method returns a long type.

Also, the supported ChronoUnit values are:

Using any other value will throw an exception.

Once an instance of Period is created, we cannot modify it, but we can create another instance based on an existing one.

One way is by using the with method and its versions:

Period period8d = period2d.withDays(8);
// Since these methods return a new instance, we can chain them!
Period period2y1m2d = period2d.withYears(2).withMonths(1);

Another way is by adding or subtracting years, months, or days:

// Adding
Period period9y4m3d = period5y4m3d.plusYears(4);
Period period5y7m3d = period5y4m3d.plusMonths(3);
Period period5y4m6d = period5y4m3d.plusDays(3);
Period period7y4m3d = period5y4m3d.plus(period2y);

// Subtracting
Period period5y4m1d = period5y4m3d.minusYears(2);
Period period5y3m3d = period5y4m3d.minusMonths(1);
Period period5y4m2d = period5y4m3d.minusDays(1);
Period period3y4m3d = period5y4m3d.minus(period2y);

The methods plus and minus take an implementation of the interface java.time.temporal.TemporalAmount (another instance of Period or an instance of Duration).

Finally, the toString() method returns the period in the format P<years>Y<months>M<days>D, for example:

System.out.println(period5y4m3d.toString()); // Prints P5Y4M3D

A zero period will be represented as zero days, P0D.

The Duration Class

The java.time.Duration class is similar to the Period class, the only thing is that it represents an amount of time in terms of seconds and nanoseconds.

You can create an instance of this class with the following methods:

Duration oneDay = Duration.ofDays(1); // 1 day = 86400 seconds
Duration oneHour = Duration.ofHours(1); // 1 hour = 3600 seconds
Duration oneMin = Duration.ofMinutes(1); // 1 minute = 60 seconds
Duration tenSeconds = Duration.ofSeconds(10);
// Set seconds and nanoseconds (if they are outside the range
// 0 to 999,999,999, the seconds will be altered, like below)
Duration twoSeconds = Duration.ofSeconds(1, 1000000000);
// Seconds and nanoseconds are extracted from the passed millisecs
Duration oneSecondFromMillis = Duration.ofMillis(2);
// Seconds and nanoseconds are extracted from the passed nanos
Duration oneSecondFromNanos = Duration.ofNanos(1000000000);
Duration oneSecond = Duration.of(1, ChronoUnit.SECONDS);

Valid values of ChronoUnit for the method Duration.of(long amount, TemporalUnit unit) are:

A Duration can also be created as the difference between two implementations of the interface java.time.temporal.Temporal, as long as they support seconds (and for more accuracy, nanoseconds), like LocalTime, LocalDateTime, and Instant. So we can have something like this:

Duration diff = Duration.between(Instant.ofEpochSecond(123456789), Instant.ofEpochSecond(99999));

The result can be negative if the end is before the start. A negative duration is indicated by a negative sign in the seconds part. For example, a duration of -100 nanoseconds is stored as -1 second plus 999,999,900 nanoseconds.

If the objects are of different types, then the duration is calculated based on the type of the first object. This only works if the first argument is a LocalTime and the second is a LocalDateTime (because it can be converted to LocalTime). Otherwise, an exception is thrown.

Once we have an instance of Duration, we can get the information with the following methods:

// The nanoseconds part the duration, from 0 to 999,999,999
int nanos = oneSecond.getNano();
// The seconds part of the duration, positive or negative
long seconds = oneSecond.getSeconds();
// It supports SECONDS and NANOS. Other units throw an exception
long oneSec = oneSecond.get(ChronoUnit.SECONDS);

Note that the methods getSeconds() and get(TemporalUnit) return a long type. Additionally, the latter only supports SECONDS and NANOS as arguments.

Once an instance of Duration is created, we cannot modify it, but we can create another instance from an existing one. One way is to use the with method and its versions:

Duration duration1sec8nan = oneSecond.withNanos(8);
Duration duration2sec1nan = oneSecond.withSeconds(2).withNanos(1);

Another way is by adding or subtracting days, hours, minutes, seconds, milliseconds, or nanoseconds:

// Adding
Duration plus4Days = oneSecond.plusDays(4);
Duration plus3Hours = oneSecond.plusHours(3);
Duration plus3Minutes = oneSecond.plusMinutes(3);
Duration plus3Seconds = oneSecond.plusSeconds(3);
Duration plus3Millis = oneSecond.plusMillis(3);
Duration plus3Nanos = oneSecond.plusNanos(3);
Duration plusAnotherDuration = oneSecond.plus(twoSeconds);
Duration plusChronoUnits = oneSecond.plus(1, ChronoUnit.DAYS); 

// Subtracting
Duration minus4Days = oneSecond.minusDays(4);
Duration minus3Hours = oneSecond.minusHours(3);
Duration minus3Minutes = oneSecond.minusMinutes(3);
Duration minus3Seconds = oneSecond.minusSeconds(3);
Duration minus3Millis = oneSecond.minusMillis(3);
Duration minus3Nanos = oneSecond.minusNanos(3);
Duration minusAnotherDuration = oneSecond.minus(twoSeconds);
Duration minusChronoUnits = oneSecond.minus(1, ChronoUnit.DAYS);

Methods plus and minus take either another Duration or a valid ChronoUnit value (the same values used to create an instance).

Finally, the method toString() returns the duration with the format PTnHnMnS. Any fractional seconds are placed after a decimal point in the seconds section. If a section has a zero value, it’s omitted. For example:

2 days 4 minutes PT48H4M
45 seconds 99 milliseconds PT45.099S

Time Zones and Daylight Savings

If you want to work with time zone information, the Date/Time API has the following classes:

Just like the classes in the previous section, these are located in the java.time package and are immutable.

Here’s a diagram to visualize the zone-aware classes:

┌────────────────────────────────────────────────────────┐
│             Java Time Zone-Aware Classes               │
│                                                        │
│  ┌─────────────┐    ┌─────────────┐                    │
│  │  ZoneId     │    │ ZoneOffset  │                    │
│  │ (Time zone) │    │(UTC offset) │                    │
│  └──────┬──────┘    └──────┬──────┘                    │
│         │                  │                           │
│         └──────────────────┼────────────────┐          │
│                            │                │          │
│                     ┌──────┴──────┐  ┌──────┴───────┐  │
│                     │ZonedDateTime│  │OffsetDateTime│  │
│                     │(Date + Time │  │ (Date + Time │  │
│                     │ + Time Zone)│  │ + UTC offset)│  │
│                     └─────────────┘  └──────┬───────┘  │
│                                             │          │
│                                       ┌─────┴─────┐    │
│                                       │OffsetTime │    │
│                                       │ (Time +   │    │
│                                       │UTC offset)│    │
│                                       └───────────┘    │
│                                                        │
└────────────────────────────────────────────────────────┘

Key Points:
- ZoneId represents a time zone (like "America/New_York")
- ZoneOffset represents a fixed offset from UTC (like "+05:00")
- ZonedDateTime combines LocalDateTime with a ZoneId
- OffsetDateTime combines LocalDateTime with a ZoneOffset
- OffsetTime combines LocalTime with a ZoneOffset

The ZoneId and ZoneOffset Classes

The world is divided into time zones in which the same standard time is kept. By convention, a time zone is expressed as the number of hours different from the Coordinated Universal Time (UTC). Since the Greenwich Mean Time (GMT) and the Zulu time (Z), used in the military, have no offset from UTC, they’re often used as synonyms.

Java uses the Internet Assigned Numbers Authority (IANA) database of time zones, which keeps a record of all known time zones around the world and is updated many times per year.

Each time zone has an ID, represented by the class java.time.ZoneId. There are three types of ID:

The first type just states the offset from UTC/GMT time. They are represented by the class ZoneOffset and they consist of digits starting with + or -, for example, +02:00.

The second type also states the offset from UTC/GMT time, but with one of the following prefixes: UTC, GMT, and UT, for example, UTC+11:00. They are also represented by the class ZoneOffset.

The third type is region-based. These IDs have the format area/city, for example, Europe/London.

You can get all the available zone IDs with the static method:

Set<String> getAvailableZoneIds()

For example, to print them to the console:

ZoneId.getAvailableZoneIds().stream().forEach(System.out::println);

To get the zone ID of your system, use the static method:

ZoneId.systemDefault()

Under the hood, it uses java.util.TimeZone.getDefault() to find the default time zone and converts it to a ZoneId.

If you want to create a specific ZoneId object use the method of():

ZoneId singaporeZoneId = ZoneId.of("Asia/Singapore");

This method parses the ID producing a ZoneOffset or a ZoneRegion (both extend from ZoneId).

In fact, the above line produces a ZoneRegion. A ZoneOffset is returned if, for example, ID is Z, or starts with + or -. For example:

ZoneId zoneId = ZoneId.of("Z"); // Z represents the zone ID for UTC
ZoneId zoneId2 = ZoneId.of("-2"); // -02:00

The rules for this method are:

Remember that a ZoneOffset represents an offset, generally from UTC. This class has a lot more constructors than ZoneId:

// The offset must be in the range of -18 to +18
ZoneOffset offsetHours = ZoneOffset.ofHours(1);
// The range is -18 to +18 for hours and 0 to ± 59 for minutes
// If the hours are negative, the minutes must be negative or zero
ZoneOffset offsetHrMin = ZoneOffset.ofHoursMinutes(1, 30);
// The range is -18 to +18 for hours and 0 to ± 59 for mins and secs
// If the hours are negative, mins and secs must be negative or zero
ZoneOffset offsetHrMinSe = ZoneOffset.ofHoursMinutesSeconds(1, 30, 0);
// The offset must be in the range -18:00 to +18:00
// Which corresponds to -64800 to +64800
ZoneOffset offsetTotalSeconds = ZoneOffset.ofTotalSeconds(3600);
// The range must be from +18:00 to -18:00
ZoneOffset offset = ZoneOffset.of("+01:30:00");

The formats accepted by the of() method are:

If you pass an invalid format or an out-of-range value to any of these methods, an exception is thrown.

To get the value of the offset, you can use:

// Gets the offset as int
int offsetInt = offset.get(ChronoField.OFFSET_SECONDS);
// Gets the offset as long
long offsetLong= offset.getLong(ChronoField.OFFSET_SECONDS);
// Gets the offset in seconds
int offsetSeconds = offset.getTotalSeconds();

ChronoField.OFFSET_SECONDS is the only accepted value of ChronoField, so the three statements above return the same result. Other values throw an exception.

Anyway, once you have a ZoneId object, you can use it to create a ZonedDateTime instance.

The ZonedDateTime Class

A java.time.ZonedDateTime object represents a point in time relative to a time zone.

A ZonedDateTime object has three parts:

This means that it stores all date and time fields to a precision of nanoseconds, and a time zone with a zone offset.

Here’s an example:

2025-08-31 T08:45:20.000 +02:00[Africa/Cairo]

Where the parts are as follows:

Date Time Offset Time zone
2025-08-31 T08:45:20.000 +02:00 [Africa/Cairo]

Once you have a ZoneId object, you can combine it with a LocalDate, a LocalDateTime, or an Instant to transform it into ZonedDateTime:

ZoneId australiaZone = ZoneId.of("Australia/Victoria");

LocalDate date = LocalDate.of(2020, 7, 3);
ZonedDateTime zonedDate = date.atStartOfDay(australiaZone);

LocalDateTime dateTime = LocalDateTime.of(2020, 7, 3, 9, 0);
ZonedDateTime zonedDateTime = dateTime.atZone(australiaZone);

Instant instant = Instant.now();
ZonedDateTime zonedInstant = instant.atZone(australiaZone);

Or using the of method:

ZonedDateTime zonedDateTime2 = 
    ZonedDateTime.of(LocalDate.now(), LocalTime.now(), australiaZone);
ZonedDateTime zonedDateTime3 = 
    ZonedDateTime.of(LocalDateTime.now(), australiaZone);
ZonedDateTime zonedDateTime4 = 
    ZonedDateTime.ofInstant(Instant.now(), australiaZone);
// year, month, day, hours, minutes, seconds, nanoseconds, zoneId
ZonedDateTime zonedDateTime5 = 
    ZonedDateTime.of(2025, 1, 30, 13, 59, 59, 999, australiaZone);

You can also get the current date/time from the system clock in the default time zone with:

ZonedDateTime now = ZonedDateTime.now();

From a ZonedDateTime you can get LocalDate, LocalTime, or a LocalDateTime (without the time zone part) with:

LocalDate currentDate = now.toLocalDate();
LocalTime currentTime = now.toLocalTime();
LocalDateTime currentDateTime = now.toLocalDateTime();

ZonedDateTime also has most of the methods of LocalDateTime that we reviewed in the previous section:

// To get the value of a specified field
int day = now.getDayOfMonth();
int dayYear = now.getDayOfYear();
int nanos = now.getNano();
Month monthEnum = now.getMonth();
int year = now.get(ChronoField.YEAR);
long micro = now.getLong(ChronoField.MICRO_OF_DAY);
// This is new, gets the zone offset such as "-03:00"
ZoneOffset offset = now.getOffset();
// To create another instance
ZonedDateTime zdt1 = now.with(ChronoField.HOUR_OF_DAY, 10);
ZonedDateTime zdt2 = now.withSecond(49);
// Since these methods return a new instance, we can chain them!
ZonedDateTime zdt3 = now.withYear(2023).withMonth(12);

// The following two methods are specific to ZonedDateTime
// Returns a copy of the date/time with a different zone, retaining the instant
ZonedDateTime zdt4 = now.withZoneSameInstant(australiaZone);
// Returns a copy of this date/time with a different time zone,
// retaining the local date/time if it's valid for the new time zone
ZonedDateTime zdt5 = now.withZoneSameLocal(australiaZone);

// Adding
ZonedDateTime zdt6 = now.plusDays(4);
ZonedDateTime zdt7 = now.plusWeeks(3);
ZonedDateTime zdt8 = now.plus(2, ChronoUnit.HOURS);

// Subtracting
ZonedDateTime zdt9 = now.minusMinutes(20);
ZonedDateTime zdt10 = now.minusNanos(99999);
ZonedDateTime zdt11 = now.minus(10, ChronoUnit.SECONDS);

The toString() method returns the date/time in the format of a LocalDateTime followed by a ZoneOffset, optionally, a ZoneId if it is not the same as the offset, and omitting the parts with value zero:

// Prints 2024-09-19T00:30Z
System.out.println(
    ZonedDateTime.of(2024, 9, 19, 0, 30, 0, 0, ZoneId.of("Z")));
// Prints, for example, 2024-06-17T19:48:39.113332-04:00[America/Montreal]
System.out.println(
    ZonedDateTime.now(ZoneId.of("America/Montreal")));

Daylight Savings

Many countries in the world adopt what is called Daylight Saving Time (DST), the practice of advancing the clock by an hour in the summer (though not exactly in the summer in all countries but let’s bear with this) when the daylight savings time starts.

When daylight saving time ends, clocks are set back by an hour. This is done to make better use of natural daylight.

ZonedDateTime is fully aware of DST.

For example, let’s take a country where DST is fully observed, like Italy (UTC/GMT +2).

In 2023, DST started in Italy on March 26th and ended on October 29th. This means that on:

March 26, 2023 at 2:00:00 A.M. clocks were turned forward 1 hour to
March 26, 2023 at 3:00:00 A.M. local daylight time instead
(So a time like March 26, 2023 2:30:00 A.M. didn’t actually exist!)

October 29, 2023 at 3:00:00 A.M. clocks were turned backward 1 hour to
October 29, 2023 at 2:00:00 A.M. local daylight time instead
(So a time like October 29, 2023 2:30:00 A.M. actually existed twice!)

If we create an instance of LocalDateTime with this date/time and print it:

LocalDateTime ldt = LocalDateTime.of(2023, 3, 26, 2, 30);
System.out.println(ldt);

The result will be:

2023-03-26T02:30 // Wrong

But if we create an instance of ZonedDateTime for Italy (notice that the format uses a city, not a country) and print it:

ZonedDateTime zdt = ZonedDateTime.of(
   2023, 3, 26, 2, 30, 0, 0, ZoneId.of("Europe/Rome"));
System.out.println(zdt);

The result will be just like in the real world when using DST:

2023-03-26T03:30+02:00[Europe/Rome] // Correct

But be careful. We have to use a regional ZoneId, a ZoneOffset won’t do the trick because this class doesn’t have the zone rules information to account for DST:

ZonedDateTime zdt1 = ZonedDateTime.of(
   2023, 3, 26, 2, 30, 0, 0, ZoneOffset.ofHours(2));
System.out.println(zdt1);

ZonedDateTime zdt2 = ZonedDateTime.of(
   2023, 3, 26, 2, 30, 0, 0, ZoneId.of("UTC+2"));
System.out.println(zdt2);

The result will be:

2023-03-26T02:30+02:00              // Wrong
2023-03-26T02:30+02:00[UTC+02:00]   // Wrong

When we create an instance of ZonedDateTime for Italy, we have to add an hour to see the effect:

ZonedDateTime zdt3 = ZonedDateTime.of(
   2023, 10, 29, 2, 30, 0, 0, ZoneId.of("Europe/Rome"));
System.out.println(zdt3);

ZonedDateTime zdt4 = zdt3.plusHours(1);
System.out.println(zdt4);

The result will be:

2023-10-29T02:30+02:00[Europe/Rome]
2023-10-29T02:30+01:00[Europe/Rome]

Otherwise we will be creating the ZonedDateTime at 3:00 of the new time:

ZonedDateTime zdt5 = ZonedDateTime.of(
        2023, 10, 29, 3, 30, 0, 0, ZoneId.of("Europe/Rome"));
System.out.println(zdt5); // Prints 2023-10-29T03:30+01:00[Europe/Rome]

We also need to be careful when adjusting the time across the DST boundary using the plus() and minus() methods that take a TemporalAmount implementation, in other words, a Period or a Duration. This is because both differ in their treatment of daylight savings time.

Consider one hour before the start of DST in Italy:

ZonedDateTime zdt6 = ZonedDateTime.of(
   2023, 3, 26, 1, 0, 0, 0, ZoneId.of("Europe/Rome"));

When we add a Duration of one day:

System.out.println(zdt6.plus(Duration.ofDays(1)));

The result is:

2023-03-27T02:00+02:00[Europe/Rome]

When we add a Period of one day:

System.out.println(zdt6.plus(Period.ofDays(1)));

The result is:

2023-03-27T01:00+02:00[Europe/Rome]

The reason is that Period adds a conceptual date, while Duration adds exactly one day (24 hours or 86,400 seconds) and when it crosses the DST boundary, one hour is added, and the final time is 02:00 instead of 01:00.

The OffsetDateTime and OffsetTime Classes

OffsetDateTime represents an object with date/time information and an offset from UTC, for example, 2025-01-01T11:30-06:00.

You may think Instant, OffsetDateTime, and ZonedDateTime are very much alike, after all, they all store the date and time to a nanosecond precision. However, there are subtle but important differences:

OffsetTime represents a time with an offset from UTC, for example, 11:30-06:00. The common way to create an instance of these classes is:

OffsetDateTime odt = OffsetDateTime.of(
    LocalDateTime.now(), ZoneOffset.of("+03:00"));
OffsetTime ot = OffsetTime.of(
    LocalTime.now(), ZoneOffset.of("-08:00"));

System.out.println(odt);
System.out.println(ot);

If you run the above example, the output would be similar to this:

2024-06-17T19:19:32.645941+03:00
19:19:32.648413-08:00

Both classes have practically the same methods as their LocalDateTime, ZonedDateTime, and LocalTime counterparts. With an offset from UTC and without time zone variations, they always represent an exact instant in time, which may be more suitable for certain types of applications (the Java documentation recommends OffsetDateTime when communicating with a database or in a network protocol).

Parsing and Formatting

java.time.format.DateTimeFormatter is the class used for parsing and formatting dates. It can be used in two ways:

All format methods throw the runtime exception java.time.DateTimeException.

All parse methods throw the runtime exception java.time.format.DateTimeParseException.

DateTimeFormatter provides three ways to format date/time objects:

Predefined Formatters

Formatter Description Example
BASIC_ISO_DATE Date fields without separators 20250803
ISO_LOCAL_DATE
ISO_LOCAL_TIME
ISO_LOCAL_DATE_TIME
Date fields with separators 2025-08-03
13:40:10
2025-08-03T13:40:10
ISO_OFFSET_DATE
ISO_OFFSET_TIME
ISO_OFFSET_DATE_TIME
Date fields with separators and zone offset 2025-08-03+07:00
13:40:10+07:00
2025-08-03T13:40:10+07:00
ISO_ZONED_DATE_TIME A zoned date and time 2025-08-03T13:40:10+07:00[Asia/Bangkok]
ISO_DATE
ISO_TIME
ISO_DATE_TIME
Date or Time with or without offset
DateTime with ZoneId
2025-08-03+07:00
13:40:10
2025-08-03T13:40:10+07:00[Asia/Bangkok]
ISO_INSTANT Date and Time of an Instant 2025-08-03T13:40:10Z
ISO_ORDINAL_DATE Year and day of the year 2025-200
ISO_WEEK_DATE Year, week and day of the week 2025-W34-2
RFC_1123_DATE_TIME RFC 1123 / RFC 822 date format Sun, 3 Aug 2025 13:40:10 GMT

Locale-specific Formatters

Style Date Time
SHORT 8/3/15 1:40 PM
MEDIUM Aug 03, 2025 1:40:00 PM
LONG August 03, 2025 1:40:00 PM PDT
FULL Monday, August 03, 2025 1:40:00 PM PDT

Custom Patterns

Symbol Meaning Examples
G Era AD; Anno Domini; A
u Year 2025; 15
y Year of Era 2025; 15
D Day of Year 150
M / L Month of Year 7; 07; Jul; July; J
d Day of Month 20
Q / q Quarter of year 2; 02; Q2; 2nd quarter
Y Week-based Year 2025; 15
w Week of Week-based Year 30
W Week of Month 2
E Day of Week Tue; Tuesday; T
e / c Localized Day of Week 2; 02; Tue; Tuesday; T
F Week of Month 2
a AM/PM of Day AM
h Hour (1-12) 10
K Hour (0-11) 1
k Hour (1-24) 20
H Hour (0-23) 23
m Minute 10
s Second 11
S Fraction of Second 999
A Milli of Day 2345
n Nano of Second 865437987
N Nano of Day 12986497300
V Time Zone ID Asia/Manila; Z; -06:00
z Time Zone Name Pacific Standard Time; PST
O Localized Zone Offset GMT+4; GMT+04:00; UTC-04:00;
X Zone Offset (‘Z’ for zero) Z; -08; -0830; -08:30
x Zone Offset +0000; -08; -0830; -08:30
Z Zone Offset +0000; -0800; -08:00
' Escape for Text  
'' Single Quote  
[ ] Optional Section Start / End  
# { } Reserved for future use  

Assuming:

LocalDate ldt = LocalDate.of(2025, 1, 20);

These are examples of using a predefined formatter:

System.out.println(DateTimeFormatter.ISO_DATE.format(ldt));
System.out.println(ldt.format(DateTimeFormatter.ISO_DATE));

The output will be:

2025-01-20
2025-01-20

These are examples of using a localized style:

DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT);
// With the current locale
System.out.println(formatter.format(ldt));
System.out.println(ldt.format(formatter));
// With another locale
System.out.println(formatter.withLocale(Locale.GERMAN).format(ldt));

One output can be:

1/20/25
1/20/25
20.01.25

And these are examples of using a custom pattern:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("QQQQ Y");
// With the current locale
System.out.println(formatter.format(ldt));
System.out.println(ldt.format(formatter));
// With another locale
System.out.println(formatter.withLocale(Locale.GERMAN).format(ldt));

One output can be:

1st quarter 2025
1st quarter 2025
1. Quartal 2025

If the formatter uses information that is not available, a DateTimeException will be thrown. For example, using a DateTimeFormatter.ISO_OFFSET_DATE with a LocalDate instance (it doesn’t have offset information).

To parse a date and/or time value from a string, use one of the parse methods. For example:

// Format according to ISO-8601
String dateTimeStr1 = "2025-06-29T14:45:30";
// Custom format
String dateTimeStr2 = "2025/06/29 14:45:30";
LocalDateTime ldt = LocalDateTime.parse(dateTimeStr1);
// Using DateTimeFormatter
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
// DateTimeFormatter returns a TemporalAccessor instance
TemporalAccessor ta = formatter.parse(dateTimeStr2);
// LocalDateTime returns an instance of the same type
ldt = LocalDateTime.parse(dateTimeStr2, formatter);

The version of parse() of the date/time objects takes a string in a format according to ISO-8601, which is:

Class Format Example
LocalDate uuuu-MM-dd 2024-12-03
LocalTime HH:mm:ss 10:15
LocalDateTime uuuu-MM-dd'T'HH:mm:ss 2024-12-03T10:15:30
ZonedDateTime uuuu-MM-dd'T'HH:mm:ssXXXXX[VV] 2023-12-03T10:15:30+01:00[Europe/Paris]
OffsetDateTime uuuu-MM-dd'T'HH:mm:ssXXXXX 2023-12-03T10:15:30+01:00
OffsetTime HH:mm:ssXXXXX 10:15:30+01:00

If the formatter uses information that is not available or if the pattern of the format is invalid, a DateTimeParseException will be thrown.

In the Localization chapter, we’ll revisit the DateTimeFormatter class, focusing on its application in localization.

Key Points

Practice Questions

1. Which of the following are valid ways to create a LocalDate object?

A. LocalDate.of(2014);
B. LocalDate.with(2014, 1, 30);
C. LocalDate.of(2014, 0, 30);
D. LocalDate.now().plusDays(5);

2. Which of the following options is the result of executing this line?

LocalDate.of(2014, 1, 2).atTime(14, 30, 59, 999999)

A. A LocalDate instance representing 2014-01-02
B. A LocalTime instance representing 14:30:59:999999
C. A LocalDateTime instance representing 2014-01-02 14:30:59:999999
D. An exception is thrown

3. Which of the following are valid ChronoUnit values for LocalTime? (Choose all that apply)

A. YEAR
B. NANOS
C. DAY
D. HALF_DAYS

4. Which of the following statements are true? (Choose all that apply)

A. java.time.Period implements java.time.temporal.Temporal
B. java.time.Instant implements java.time.temporal.Temporal
C. LocalDate and LocalTime are thread-safe
D. LocalDateTime.now() will return the current time in UTC zone

5. Which of the following options is a valid way to get the nanoseconds part of an Instant object referenced by i?

A. int nanos = i.getNano();
B. long nanos = i.get(ChronoField.NANOS);
C. long nanos = i.get(ChronoUnit.NANOS);
D. int nanos = i.getEpochNano();

6. Which of the following options is the result of executing this line?

System.out.println(
   Period.between(
       LocalDate.of(2025, 3, 20),
       LocalDate.of(2025, 2, 20))
);

A. P29D
B. P-29D
C. P1M
D. P-1M

7. Which of the following options is the result of executing this line?

System.out.println(
   Duration.between(
       LocalDateTime.of(2025, 3, 20, 18, 0),
       LocalTime.of(18, 5) )
);

A. PT5M
B. PT-5M
C. PT300S
D. An exception is thrown

8. Which of the following are valid ChronoField values for LocalDate?

A. DAY_OF_WEEK
B. HOUR_OF_DAY
C. DAY_OF_MONTH
D. MILLI_OF_SECOND

9. Which of the following are valid ways to create a ZoneId object?

A. ZoneId.ofHours(2);
B. ZoneId.of("2");
C. ZoneId.of("-1");
D. ZoneId.of("America/Canada");

10. Which of the following options is the result of executing these lines?

ZoneOffset offset = ZoneOffset.of("Z");
System.out.println(
   offset.get(ChronoField.HOUR_OF_DAY)
);

A. 0
B. 1
C. 12:00
D. An exception is thrown

11. Assuming a local time zone of +2:00, which of the following options is the result of executing these lines?

ZonedDateTime zdt =
   ZonedDateTime.of(2025, 02, 28, 5, 0, 0, 0,
       ZoneId.of("+05:00"));
System.out.println(zdt.toLocalTime());

A. 05:00
B. 17:00
C. 02:00
D. 03:00

12. Assuming that DST starts on October, 4, 2025 at 0:00:00, which of the following is the result of executing the above lines?

   ZonedDateTime zdt =
       ZonedDateTime.of(2025, 10, 4, 0, 0, 0, 0,
           ZoneId.of("America/Asuncion"))
       .plus(Duration.ofHours(1));
   System.out.println(zdt);

A. 2025-10-04T00:00-03:00[America/Asuncion]
B. 2025-10-04T01:00-03:00[America/Asuncion]
C. 2025-10-04T02:00-03:00[America/Asuncion]
D. 2025-10-03T23:00-03:00[America/Asuncion]

13. Which of the following statements are true? (Choose all that apply)

A. java.time.ZoneOffset is a subclass of java.time.ZoneId.
B. java.time.Instant can be obtained from java.time.ZonedDateTime.
C. java.time.ZoneOffset can manage DST.
D. java.time.OffsetDateTime represents a point in time in the UTC time zone.

14. Which of the following options is the result of executing these lines?

DateTimeFormatter formatter =
   DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
System.out.println(
   formatter
   .withLocale(Locale.ENGLISH)
   .format(LocalDateTime.of(2025, 5, 7, 16, 0))
);

A. 5/7/15 4:00 PM
B. 5/7/15
C. 4:00 PM
D. 4:00:00 PM

15. Which of the following statements is true about these lines?

DateTimeFormatter formatter =
   DateTimeFormatter.ofPattern("HH:mm:ss X");
OffsetDateTime odt =
   OffsetDateTime.parse("11:50:20 Z", formatter);

A. The pattern HH:mm:ss X is invalid.
B. An OffsetDateTime is created successfully.
C. Z is an invalid offset.
D. An exception is thrown at runtime.

Do you like what you read? Would you consider?


Do you have a problem or something to say?

Report an issue with the book

Contact me