Exploring the Date API
The java.time
package was introduced in Java 8 and was designed to replace the previous java.util.Date
, java.util.Calendar
, and java.text.DateFormat
classes. The classes in java.time
represent dates, times, timezones, instants, periods, and durations. The ISO calendar system is followed, which is the de facto world calendar (following Gregorian rules). All the classes are immutable and thread-safe.
It is a large API (https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/package-summary.html) with a large number of classes for dealing with dates, with relatively fewer classes dealing with times. Thankfully, despite the large number of methods available, the consistent use of method prefixes makes this manageable. We will look at these API prefixes shortly. But before we do that, let’s discuss the more important date and time classes.
Coordinated Universal Time (UTC)
UTC is the standard by which the world regulates clocks and time. It is effectively a successor to Greenwich Mean Time (GMT). UTC makes no adjustment for daylight savings time.
The time zone uses UTC+/-00:00, which is sometimes denoted by the letter Z – a reference to the equivalent nautical time zone (GMT). Since the NATO phonetic alphabet word for Z is “Zulu”, UTC is sometimes referred to as “Zulu time.”
Dates and times
There are five important classes here. Let’s examine each in turn:
Instant
: An instant is a numeric timestamp. It is useful for logging and persistence. Historically,System.currentTimeMillis()
would have been used.System.currentTimeMillis()
returns the number of milliseconds since the “epoch day” (Jan 1st, 1970 at 00:00:00 UTC). The epoch is a fixed time from which all timestamps are calculated.LocalDate
: Stores a date without a time. This is useful for representing birthdays such as 2000-10-21. As it follows ISO-8601, the format is year-month-day.LocalTime
: Stores a time without a date. This is useful for representing opening/closing hours such as 09:00.LocalDateTime
: Stores a date and time such as 2000-10-21T17:00. Note the “T” used as a date and time separator. This is useful for representing the date and time of a scheduled event, such as a concert.ZonedDateTime
: Represents a “full” date-time with a time zone and resolved offset from UTC. For example, 2023-02-14T16:45+01:00[Europe/Zurich] is the date-time for the Europe/Zurich time zone and is 1 hour ahead of UTC.
Duration and Period
In addition to dates and times, the API also represents durations and periods of time. Let’s look at these now.
Duration
: An amount of time, represented in seconds (and nanoseconds); for example, “54 seconds.”Period
: Represents an amount of time in units more meaningful to humans, such as years or days. For example, “3 years, 6 months, and 12 days.”
Additional interesting types
Other types are interesting also. Let’s examine some of these now.
Month
: Represents a month on its own; for example, JANUARY.DayOfWeek
: Represents a day-of-week on its own; for example, FRIDAY.YearMonth
: Represents a year and month, without a day or time; for example, 2025-12. This could be useful for a credit card expiry date.MonthDay
: Represents a month and day, without a year or time; for example, --08-09. This could be useful for an annual event, such as an anniversary.ZoneOffset
: Represents a time zone offset from GMT/UTC, such as +2:00.
As stated earlier, there are a large number of methods across the classes. However, as the prefixes are consistently applied, this is manageable. Table 12.5 represents these prefixes.
Method Prefix |
Description Note: |
|
A static factory method for creating instances – for example,
|
|
A static factory method for creating instances – for example,
|
|
Gets the value of something – for example,
|
|
Checks if something is true – for example,
|
|
The immutable equivalent of a setter method – for example,
|
|
Adds an amount to an object – for example,
|
|
Subtracts an amount from an object – for example,
|
|
Combines this object with another – for example,
|
Table 12.5 – Date API method prefixes
Now that we have had a look at the prefixes in the API, let’s look at some sample code to reinforce them. Figure 12.21 shows some code for manipulating dates and times:
Figure 12.21 – Code for manipulating dates and times
In this figure, line 13 creates LocalDate
using the factory now()
method. This creates a LocalDate
object based on the system clock setting for the default locale. Also, using the now()
method, lines 14-15 create LocalTime
and LocalDateTime
objects, respectively. Line 16 shows another way to create LocalDateTime
objects by using the of()
factory method to pass in both LocalDate
and LocalTime
objects. Line 17 shows the output of the LocalDateTime
object to be yyyy-mm-ddThh:mm:ss:nnnnnnnnn
. The date part comes first, then "T"
, which separates the date from the time, where n
in the time part represent nanoseconds.
Next, we want to create LocalDate
values representing St. Patrick’s Day (March 17), 2025. Line 20 uses the of()
factory method and passes in numeric values for the year, month, and day. Note that the months start at 1 and not 0. Thus, March is represented as 3.
Line 21 uses an alternative factory method, namely parse(String)
, which accepts a String
and creates a LocalDate
accordingly. If the string cannot be parsed, an exception will occur.
Line 22 outputs what day of the week, March 17, 2025, occurs (which is a Monday). Line 23 “modifies” the months, changing it from 3 to 5 (March to May). As the Date API types are immutable, the change is made to a new object in the background (ld2
is untouched). The ld3
reference refers to this new object (2025-05-17
).
Line 25 adds a year, so we now have 2026-05-17. Line 27 subtracts 5 days, so we now have 2026-05-12. Lastly, on line 29, we “change” our LocalDate
to LocalDateTime
. As we already have a date, we just provided the time elements. The nanoseconds, which are not provided, are set to 0 and are not displayed as a result.
Now, let’s look at a ZonedDateTime
example in Figure 12.22:
Figure 12.22 – ZonedDateTime example
In this figure, a flight leaves Dublin for Paris at 1 PM local time. The flight duration is 1 hour 45 minutes. We are trying to calculate the local time in Paris when the flight lands. The solution presented here is one option.
Lines 34-38 create a LocalDateTime
object for the departure date and time (November 24th, 2023, at 1 P.M.). Line 39 zones the date-time object using the atZone()
method by passing in the relevant time zone (a ZoneId
). To get the time zone ZoneId
object, simply call the factory of()
method while passing in the relevant time zone string. In this example, it is "Europe/Dublin"
. Line 40 shows the format of the ZonedDateTime
object. Note "Z"
for Zulu time (UTC). At that time of year, as summertime has ended, Dublin is in line with UTC.
Lines 42-45 represent the calculation of the local arrival time in Paris. Line 43 calculates what time is it in Paris when the flight leaves Dublin using the withZoneSameInstant()
method. Now, all we have to do is add on the flight time of 1 hour and 45 minutes.
Line 47 shows the ZonedDateTime
for the arrival time. The time and zoned offset elements are interesting. The local time allows for the fact that Paris is 1 hour ahead of Dublin. This time difference is reflected in the offset of +1:00
. Thus, Paris is 1 hour ahead of UTC.
Now, let’s look at some code that uses Period
and Duration
. Figure 12.23 presents an example:
Figure 12.23 – An example using Period and Duration
In this figure, both Period
and Duration
are demonstrated. Period
is suited for time blocks of greater than 1 day; for example, 2 years, 5 months, and 11 days. Duration
is more suited to blocks of time of less than 1 day; for example, 8 hours and 20 seconds.
Lines 57-63 calculate and output the number of years, months, and days the American Civil War lasted. Firstly, we create LocalDate
objects for the start and end dates (lines 57-58). Line 59 creates a Period
object using the static Period.between()
method, passing in the relevant start and end dates. Line 60 outputs the period object, P3Y11M28D
, which represents a Period
of 3 years, 11 months, and 28 days (weeks are represented in days). Lines 61-63 output the years, months, and days values separately.
Next, we will look at Duration
. In this case, we use two LocalTime
objects; one representing 12:00:20 (line 66) and the other representing 14:45:40 (line 67). Line 68 calculates the time difference between both and line 69 outputs the result. Note that there is no Y, M, or D (years, months, or days) as there was on line 60 (Period
). Now, on line 69, we have a Duration
of PT2H45M20S
representing 2 hours, 45 minutes, and 20 seconds.
Lastly, let’s look at how to format dates and times.
Formatting dates and times
A formatter can work in both directions: formatting your temporal (time-related) object as a string or parsing a string into a temporal object. Both approaches work with formatters. This is represented by the following code from the API:
LocalDate date = LocalDate.now();String text = date.format(formatter); LocalDate parsedDate = LocalDate.parse(text, formatter);
We will focus on how to create formatters for the format()
method. However, as formatters are common to both formatting and parsing, what we say for one applies to the other.
We have a lot of flexibility in how we specify the format for our dates and times. Firstly, there are pre-defined standard formats available for us. In addition, we can specify custom formats. When specifying custom formats, the letters A-Z and a-z are reserved and have specific semantics. Importantly, the number of format letters is important – for example, MMM formats the month to Aug, whereas MM produces 08.
There are two common approaches to formatting your dates and times. One is to use format(DateTimeFormatter)
in the LocalDate
, LocalTime
, LocalDateTime
, and ZonedDateTime
temporal classes. Its signature accepts a parameter of the DateTimeFormatter
type. The other approach is to use format(TemporalAccessor)
in the DateTimeFormatter
class itself. TemporalAccessor
is an interface that’s implemented by the temporal classes just mentioned.
Before we look at some example code, we must cover the more popular pre-defined formatters and format patterns. There are quite a few and we encourage you to look up the API for further details.
Pre-defined formatters
The easiest way to access these formatters is to use the constants in the DateTimeFormatter
class or by calling the factory “of” methods in DateTimeFormatter
. Table 12.6 presents an overview of the more popular ones. Please see the API for further details. Note that ISO stands for International Organization for Standardization:
Formatter |
Description |
Example |
|
Formatter with the date style from the locale |
This depends on the style that’s passed in. An example is “Monday 10 July 2023”. |
|
Formatter with the time style from the locale |
This depends on the style that’s passed in. An example is “15:47”. |
|
Formatter with the date and time styles from the locale |
This depends on the style that’s passed in. An example is “3 July 2018 09:19”. |
|
ISO date (may contain offset) |
“2023-07-10”, “2023-07-10+01:00”. |
|
ISO time (may contain offset) |
“15:47:13”, “15:47:13+01:00”. |
|
ISO local date (no offset) |
“2023-07-10”. |
|
ISO local time (no offset) |
“16:00:03”. |
|
Zoned date time |
“2023-07-12T09:33:03+01:00 [Europe/Dublin]”. |
Table 12.6 – Date API pre-defined formatters
Now, let’s examine some code that uses pre-defined formatters.
Figure 12.24 presents code that uses these pre-defined formatters:
Figure 12.24 – Code example using pre-defined formatters
In this figure, we represent the current date (line 74) and the current time (line 81) in various formats, based on the pre-defined formats available in DateTimeFormatter
. First up is ISO_DATE
(line 75). Its output (in comments on line 76) is 2023-07-10
, which is in yyyy-mm-dd format.
Line 78 uses the ofLocalizedDate()
factory method to create a format. By passing in the FormatStyle.FULL
enum constant, we are requesting as much detail as possible. As a result, this format outputs (line 79) Monday 10 July 2023
. As can be seen, this is more detailed than the ISO_DATE
format.
Line 82 creates an ISO_TIME
formatter and applies it (line 83) to the time object that’s already been created (line 81). Line 85 uses the ofLocalizedTime()
factory method. The FormatStyle.SHORT
enum returns the fewest details, typically numeric.
That covers the pre-defined formatters. Now, let’s discuss how to specify custom formatters.
Custom formatters
Custom formatters are defined using pattern letters, where the number of letters used is significant. Let’s discuss the most commonly used pattern letters first and then present some code that utilizes them. Table 12.7 presents a summary of the pattern letters:
Letter |
Description |
Examples |
y |
Year |
2023; 23 |
M |
Month |
8; 08; Aug; August |
d |
Day of the month |
16 |
E |
Day of the week |
Wed; Wednesday |
D |
Day of the year |
145 |
h |
Hour of the day; 12-hour clock (1-12) |
10 |
H |
Hour of the day; 24-hour clock (0-23) |
19 |
m |
Minute of the hour |
32 |
s |
Second of the minute |
55 |
a |
A.M. or P.M. |
PM |
z |
Timezone |
GMT |
G |
Era |
AD |
Table 12.7 – Date API pattern letters overview
This table is best explained with the aid of an example. Figure 12.25 presents an example that uses the pattern letters from Table 12.7:
Figure 12.25 – Code example using pattern letters
In this figure, line 91 gets the current date and time for this timezone, which is Irish Standard Time (IST).
Irish Standard Time (IST)
This is the timezone that’s used in Ireland. In Ireland, we utilize daylight savings time (“summertime”). This means that during the summer months, we advance the clocks forward 1 hour so that darkness falls at a later clock time. Therefore, in March, we put the clocks forward 1 hour, and in October, we put the clocks back 1 hour.
There is no “summertime” in UTC. Because of this and the fact that it is July right now, IST is +1:00 hours ahead of UTC.
The output for line 92 is in a comment to the right. The date and time are separated, as usual, by “T.” The zone offset is “+1:00,” indicating that this zoned time is 1 hour ahead of UTC. The zone ID is “[Europe/Dublin].”
We will first look at a date-related formatter. Line 93 creates a formatter using the yy-MMM-dd E D
pattern. The output it generates is 23-Jul-11 Tue 192
(line 94). Thus, the current year of 2023 is output as 23
because we only provided yy
in the format (as opposed to yyyy
). Note that, had it been yyyy
in the format, 2023
would have been output. This is why the number of pattern letters is important. The capital M
is for the month. M
produces 7
, MM
produces 07
, MMM
(as in the pattern) produces Jul
, and MMMM
produces July
. Again, this demonstrates that the number of pattern letters is important.
The dd
pattern outputs the day of the month. This gives us 11
for the 11th
. E
gives us the day of the week, which is Tue
. Note that EEEE
returns Tuesday
. D
represents the day of the year; the 192nd in this example.
Note that the dashes and spaces are simply inserted into the output. This is because, unlike letters, they are not reserved. We will learn how to insert words (containing letters) into the output without causing exceptions shortly.
Now, let’s examine a time-related formatter. Line 96 creates a formatter using the hh:mm:ss a z G
pattern, which generates the output (line 97) of 09:05:50 a.m. IST AD
. The hh:mm:ss
pattern returns the current time in hours (12-hour clock), minutes, and seconds format. a
returns whether it is A.M. or P.M. Right now, it is the morning, so am
is returned. The z
pattern letter returns the abbreviated zone name, IST
. Expanding this to zzzz
returns Irish Standard Time
. Lastly, G
returns the era, AD
(Anno Domini).
Now, let’s learn how to insert text into our formatter. As we know, the letters a-z and A-Z are reserved. So, how do we insert letters as regular letters and not pattern letters? To do this, we must surround the regular letters with single quotes. Line 101 specifies a pattern that uses both regular letters and pattern letters. The pattern is “’Year: ‘yyyy’. Month: ‘MMMM’. Day: ‘dd’.’”. The pattern letters are in italics. Any other characters are enclosed in single quotes.
Year: 2023. Month: July. Day: 11.
is generated as output.
As we can see, the year value, 2023
, is preceded by the text "Year: "
. This was achieved by surrounding the text with single quotes: 'Year: '
. Following the year pattern yyyy
, the regular text '. Month: '
is inserted. Thus, the capital M
is treated as simply a capital M, instead of a month pattern letter. After that, '. Day: '
is inserted to precede the day of the month, which is 11
. Lastly, a period is inserted at the end by enclosing it in single quotes also. Note that the period without single quotes is also fine as it is not a reserved character.
Lastly, let’s look at an example of parsing where we can create temporal objects from String
values. Line 105 declares a string of "2023-07-10 22:10"
. Line 106 then declares a pattern that will be able to parse this string. The pattern is "yyyy-MM-dd HH:mm"
. Note that "HH"
represents the 24-hour clock. This will enable us to parse the time of "22"
in the string.
Line 107 creates a LocalDateTime
object by parsing the string according to the pattern provided. Line 108 outputs the LocalDateTime
object, producing "2023-07-10T22:10"
, which is what the string represents.
That completes our discussion on custom formatters and concludes Chapter 12. Now, let’s put that knowledge into practice to reinforce the concepts we’ve covered.