Dates in Java 8 and beyond 📆
Using Dates in programming languages makes many people sweat at the thought, they can be tricky to understand and use. In Java, they were badly realised in the core library, and that led to a lot people to use external libraries such as Joda Time. 😥
It was only in Java 8, that it was addressed in the java.time
package, and a lot of the pain has been removed. 🙏
Key temporal classes
These classes are immutable, which means you can’t change values of an instance. Why? We want to ensure thread-safety. If you have different threads updating a date, you will get funky results when you want to lookup the value.
Instant ⚡
An Instant is a moment on the timeline in UTC. It is the count in nanoseconds since the first moment of 1970 UTC (unix epoch time). It is essentially a timestamp (often referred to as “machine time”).
ZoneId 🌎
A ZoneId is a time zone.
A time zone is a set of rules for handling adjustments and anomalies as practiced by a country or region. The most common rule applied is Daylight Saving Time (DST). A time zone has the history of past rules, present rules, and rules confirmed for the near future. 📏
These rules change more often than you might expect. Be sure to keep your date-time library’s rules, usually a copy of the ‘tz’ database, up to date. Keeping up-to-date is easier than ever now in Java 8 with Oracle releasing a Timezone Updater Tool.
Use complete time zone names, as much as possible. These names take the form of continent, a SLASH, and a city or region. Avoid the 3-4 letter codes such as ‘EST’ or ‘IST’. They are neither standardized, nor unique. They further confuse the messiness of DST. ‘UTC’ is the only one that can be used without issue as it is the basis of DST.
Here is a list of the long version of all the zone IDs. The code prints them all out, if you want to do it yourself!
ZoneOffset ➕🌎
ZoneOffset is an offset from UTC, such as +02:00.
For example, Paris is one hour ahead of Greenwich/UTC in winter and two hours ahead in summer. The ZoneId instance for Paris will reference two ZoneOffset instances - a +01:00 instance for winter, and a +02:00 instance for summer
You’re unlikely to use this class directly or it’s related classes: OffsetDateTime, and OffsetTime.
ZonedDateTime 🌎📅
ZonedDateTime is a date-time with a time-zone. Think…
ZonedDateTime = Instant + ZoneId
Local representations 🏠️
These “local” date representations are without timezones.
- LocalDate stores a date only (‘2010-12-03’),
- LocalTime stores a time only (‘18:00’),
- LocalDateTime stores a date and time (‘2010-12-03T11:30’).
Amount of time ⌚
To specify an amount of time, we can use:
- Duration is time-based (seconds with nanosecond accuracy).
- Period is date-based (years, months, days).
Which of these classes should I use?
When choosing a temporal-based class, you first identify what aspects of time you need to represent:
- Do you need a time zone?
- Date and time?
- Date only? If you need a date, do you need month, day, and year, or a subset?
Nearly all of your backend, database, business logic, data persistence, data exchange should be in UTC, you will use Instant
the most.
But for presentation to users you need to adjust into a time zone expected by the user. For this, you will probably use ZonedDateTime
.
For recording something like a birthday, you might use a LocalDate
, because most people observe their birthday on the same day, whether they are in their birth city or somewhere else.
Why would you use OffsetDateTime instead of ZonedDateTime? It would be for special cases. If you are writing complex software that models its own rules for date and time calculations based on geographic locations.
Class/Enum | Year | Month | Day | Hours | Minutes | Seconds | Zone Offset | Zone ID | toString() |
---|---|---|---|---|---|---|---|---|---|
Instant | X | 2013-08-20T15:16:26.355Z | |||||||
LocalDate | X | X | X | 2013-08-20 | |||||
LocalDateTime | X | X | X | X | X | X | 2013-08-20T08:16:26.937 | ||
OffsetDateTime | X | X | X | X | X | X | X | 2013-08-20T08:16:26.954-07:00 | |
ZonedDateTime | X | X | X | X | X | X | X | X | 2013-08-20T08:16:26.937 |
LocalTime | X | X | X | 08:16:26.937 | |||||
OffsetTime | X | X | X | X | 08:16:26.954-07:00 | ||||
MonthDay | X | X | –08-20 | ||||||
Year | X | 2013 | |||||||
YearMonth | X | X | 2013-08 | ||||||
Month | X | AUGUST | |||||||
Duration | ^ | ^ | ^ | X | PT20H (20 hours) | ||||
Period | X | X | X | P10D (10 days) |
^ does not store the value but has methods to access these values
Using Dates 📅
Creating a date 🐣
We use these static methods to get an instance. You can get the current date via now()
; or a fixed date using of()
.
Comparing dates 🔎
Use int compareTo(ChronoLocalDate other)
to assert the equality of dates by returning a comparsion number, which can be: negative (before); zero (equal); or positive (after).
The self-explanatory boolean isAfter(ChronoLocalDate other)
, boolean isBefore(ChronoLocalDate other)
, and boolean isBefore(ChronoLocalDate other)
could be more convenient if you want a more specific test.
These methods are available in LocalDate
, LocalTime
, LocalDateTime
, and ZonedDateTime
(but the datatype of the parameter is ChronoZonedDateTime
).
Difference between 2 dates 🕒…🕕
Use the time units in java.time.temporal.ChronoUnit and between(…)
.
For example, to get the logical calendar days between 2 dates, you can use DAYS.between(Temporal temporal1Inclusive, Temporal temporal2Exclusive)
.
Adding and subtracting time units from dates and times ➕📅
To add time units, you can:
- use the general form:
plus(long amountToAdd,TemporalUnit unit)
; - Or specific forms that (predictably) vary for each class:
plusDays(long days)
plusSeconds(long seconds]
To subtract time units, you can:
- use the general form:
minus(long amountToTake,TemporalUnit unit)
; - Or specific forms that (predictably) vary for each class:
minusDays(long days)
minusSeconds(long seconds]
Adding a Duration
to a ZonedDateTime
, time differences are not observed.
Adding a Period
to a ZonedDateTime
, the time differences are observed.
Parsing dates 🔁
Use parse(CharSequence text)
for dates in the format of “yyyy-mm-dd”.
To parse a date in another format, use the second version, which requires a DateTimeFormatter
, through which you can specify the format: parse(CharSequence text, DateTimeFormatter formatter)
Formatting dates 📁️
DateTimeFormatter provides methods for parsing and printing dates using predefined constants (e.g. ISO_LOCAL_DATE), and patterns (e.g. yyyy-MMM-dd).
More complex formatting can be done with DateTimeFormatterBuilder.
Get a particular day 🎣
The TemporalAdjusters class contains a standard set of adjusters, which are available as static methods. These include:
- finding the first or last day of the month
- finding the first day of next month
- finding the first or last day of the year
- finding the first day of next year
- finding the first or last day-of-week within a month, such as “first Wednesday in June”
- finding the next or previous day-of-week, such as “next Thursday”
Java 7 and below 📅️👴👵
In short, don’t waste your time doing it this way! But you may need to maintain code that uses these classes.
Some of the issues are:
- Some of the classes have poor API design. For example, years in
java.util.Date
start at 1900, months start at 1, and days start at 0. This is not very intuitive! - Classes such as
java.util.Date
and
SimpleDateFormatter
aren’t thread-safe, leading to potential concurrency issues for users, not something you would expect to deal with when writing date-handling code.
But for completeness, lets show a little of the way it was done.
The Date class
is the raw form of a date, it is the number of milliseconds
since January 1, 1970. Most of it’s methods are deprecated, we are encouraged to
use Calendar GregorianCalendar for creating a specific date.
You can create a Date using java.util.Date
in 2 forms:
You would probably not use the second version, and you may want a date other
than now! So…
Really, Date is used as the type to transfer a date between Calendar
and SimpleDateFormat:
- Calendar is an abstract class for creating and manipulating a specific date. There is a static method for creating an instance:
- GregorianCalendar is a concrete class for creating and manipulating a specific date, in the Gregorian standard that is followed by most countries in the world.
- SimpleDateFormat is to format and parse a dates e.g. dd.mm.yyyy for German dates.
Example application 💾
For a loan of a book, we want to store when the book is loaned, and when it is returned. It is a better fit to use an Instant
for these fields, and stick with UTC timezone implicitly. The user can decide how to record the value (timezone), and display it to the user.
To see if the book was returned late, in getDaysLate()
, we want to use the beginning of the loan date (midnight) as the basis of the calculation, to include the full day. The easiest way (I could find) to do this, is to convert the Instant
to a ZonedDateTime
using the “UTC” ZoneId
, and use truncatedTo()
to only take the date portion, therefore resetting the time portion to 00:00:00. 😅
This is a good example of how to use temporal classes for storing dates, and making calculations based on them.