Intuitive, Robust Date and Time Handling, Finally Comes to Java
The concepts of date and time are fundamental to many applications. Things as diverse as birth dates, rental periods, event timestamps and shop opening hours are all based on date and time, yet Java SE had no good API to handle them. With Java SE 8, there is a new set of packages - java.time - that provides a well-structured API to cover date and time.
When Java was first launched, in version 1.0, the only support for dates and times was the java.util.Date class. The first thing most developers note about the class is that it doesn’t represent a “date”. What it does represent is actually quite simple - an instantaneous point in time based on millisecond precision, measured from the epoch of 1970-01-01Z. However, because the standard toString() form prints the date and time in the JVM’s default time-zone, some developers wrongly assume that it is a time-zone aware class.
The Date class was deemed impossible to fix when the time came for improvements in version 1.1. As a result a new API was added - java.util.Calendar. Sadly, the Calendar class isn’t really much better than java.util.Date. Some of the issues faced with these classes are:
- Mutable. Classes like dates and time simply should be immutable.
- Offset. Years in Date start from 1900, Months in both classes start from zero.
- Naming. Date is not a “date”. Calendar is not a “calendar”.
- Formatting. The formatter only works with Date, not Calendar, and is not thread-safe
Around 2001, the Joda-Time project was started. The purpose of Joda-Time was simple - to provide a quality date and time library for Java. It took a while, but eventually version 1.0 of Joda-Time was launched and it became a very widely used and popular library. Over time the demand grew to provide a library like Joda-Time in JDK. With the help of Michael Nascimento Santos from Brazil JSR-310 was started, the official process to create and integrate a new date and time API for the JDK.
The new java.time API consists of 5 packages:
- java.time - the base package containing the value objects
- java.time.chrono -provides access to different calendar systems
- java.time.format - allows date and time to be formatted and parsed
- java.time.temporal - the low level framework and extended features
- java.time.zone - support classes for time-zones
Most developers will primarily use the base and format packages, and perhaps the temporal package. Thus, although there are 68 new public types, most developers will only actively use around a third of them.
The class LocalDate is one of the most important in the new API. It is an immutable value type that represents a date. There is no representation of time-of-day or time-zone.
The “local” terminology is familiar from Joda-Time and comes originally from the ISO-8601 date and time standard. It relates specifically to the absence of a time-zone. In effect, a local date is a description of a date, such as the “5th April 2014”. That particular local date will start at different points on the time-line depending on where on the Earth you are. Thus the local date will start in Australia 10 hours before it starts in London and 18 hours before it starts in San Francisco.
The LocalDate class is designed to have all the methods that are commonly needed:
LocalDate date = LocalDate.of(2014, Month.JUNE, 10); int year = date.getYear(); // 2014 Month month = date.getMonth(); // JUNE int dom = date.getDayOfMonth(); // 10 DayOfWeek dow = date.getDayOfWeek(); // Tuesday int len = date.lengthOfMonth(); // 30 (days in June) boolean leap = date.isLeapYear(); // false (not a leap year)
In the example, we see a date being created using a factory method (all constructors are private). It is then queried for some essential information. Note the Month and DayOfWeek enums designed to make code more readable and reliable.
In the next example, we see how an instance is manipulated. As the class is immutable, each manipulation results in a new instance, with the original unaffected.
LocalDate date = LocalDate.of(2014, Month.JUNE, 10); date = date.withYear(2015); // 2015-06-10 date = date.plusMonths(2); // 2015-08-10 date = date.minusDays(1); // 2015-08-09
These changes are relatively simple, but often there is a need to make more complex alterations to a date. The java.time API includes a mechanism to handle this - TemporalAdjuster. The idea behind a temporal adjuster is to provide a pre-packaged utility capable of manipulating a date, such as obtaining the instance corresponding to the last day of the month. Common ones are supplied in the API, but you can add your own. Using an adjuster is very easy, but does benefit from static imports:
import static java.time.DayOfWeek.* import static java.time.temporal.TemporalAdjusters.* LocalDate date = LocalDate.of(2014, Month.JUNE, 10); date = date.with(lastDayOfMonth()); date = date.with(nextOrSame(WEDNESDAY));
The immediate reaction to seeing an adjuster in use is typically an appreciation of how close the code is to the intended business logic. Achieving that is very important with date and time business logic. The last thing that we want to see is large amounts of manual manipulation of dates. If you have a common manipulation that you are going to perform many times in your codebase, consider writing your own adjuster once and getting your team to pull it in as a pre-written, pre-tested component.
Dates and Times as Values
It is worth spending a brief moment considering what makes the LocalDate class into a value. Values are simple data types where two instances that are equal are entirely substitutable - object identity has no real meaning. The String class is the canonical example of a value - we care whether two strings are true by equals(), we don’t care if they are identical by ==.
Most date and time classes should also be values, and the java.time API fulfills this expectation. Thus, there is never a good reason to compare two LocalDate instances using ==, and in fact the Javadoc advises against it.
For those wanting to know more, see my recent definition of VALJOs, which defines a strict set of rules for value objects in Java, including immutability, factory methods and good definitions of equals, hashCode, toString and compareTo.
Alternate calendar systems
The LocalDate class, like all the main date and time classes in java.time, is fixed to a single calendar system - the calendar system defined in the ISO-8601 standard.
The ISO-8601 calendar system is the de facto world civil calendar system, also described as the proleptic Gregorian calendar. Standard years are 365 days long, and leap years are 366 days long. Leap years occur every 4 years, but not every 100, unless divisible by 400. The year before year one is considered to be year zero for consistent calculations.
The first impact of using this calendar system as the default is that dates are not necessarily compatible with the results from GregorianCalendar. In GregorianCalendar, there is a cutover from the Julian calendar system to the Gregorian one which occurs by default on 15th October 1582. Before that date, the Julian calendar is used, which has a leap year every 4 years without fail. After that date, the Gregorian calendar is used, which has the more complicated leap year system we use today.
Given that this change in calendar system is a historical fact, why does the new java.time API not model it? The reason is that most Java applications that make use of such historic dates are incorrect today, and it would be a mistake to continue that. The reason is that while the Vatican City in Rome changed calendar system on 15th October 1582, most of the rest of the world did not. In particular the British Empire, including the early USA, did not change until 14th September 1752, nearly 200 years later. Russia didn’t change until 14th February 1918, and Sweden’s change was particularly messy. Thus, the meaning of a date prior to 1918 is in fact quite open to interpretation, and belief in the single cutover of GregorianCalendar is misplaced. The choice of LocalDate to have no cutover is thus a very rational choice. An application requires additional contextual information to accurately interpret a specific historical date between the Julian and Gregorian calendars.
The second impact of the focus on using the ISO-8601 de facto world calendar system in all the main classes, is a need for an additional set of classes to handle other calendar systems. The Chronology interface is the main entry point to alternate calendar systems, allowing them to be looked up by name of locale. Four other calendar systems are provided in Java SE 8 - the Thai Buddhist, the Minguo, the Japanese and the Hirah. Other calendar systems can be supplied by applications.
Each calendar system has a dedicated date class, thus there is a ThaiBuddhistDate, MinguoDate, JapaneseDate and HijrahDate. These are used if building a highly localized application, such as one for the Japanese government. An additional interface, ChronoLocalDate, is used as the base abstraction of these four plus LocalDate allowing code to be written without knowing what calendar system it is operating on. Despite the existence of this abstraction, the intention is that it is rarely used.
Understanding why the abstraction is to be rarely used is critical to correct use of the whole java.time API. The truth is that when today’s applications are examined, most code that tries to operate in a calendar system neutral manner is broken. For example, you cannot assume that there are 12 months in a year, yet developers do and add 12 months assuming that they have added a whole year. You cannot assume that all months are roughly the same length - the Coptic calendar system has 12 months of 30 days and one month of five or six days. Nor can you assume that the next year has a number one larger than the current year, as calendars like the Japanese restart year numbering when the Emperor changes, which is typically mid-year (you can’t even assume that two days in the same month have the same year!).
The only way to write code across a large application in a calendar system neutral way is to have a heavy regime of code review where every line of date and time code is double checked for bias towards the ISO calendar system. As such, the recommended use of java.time is to use LocalDate throughout your application, including all storage, manipulation and interpretation of business rules. The only time that ChronoLocalDate should be used is when localizing for input or output, typically achieved by storing the user’s preferred calendar system in their user profile, and even then most applications do not really need that level of localization.
For the full rationale in this area, see the Javadoc of ChronoLocalDate.
Time of day
Moving beyond dates, the next concept to consider is local time-of-day, represented by LocalTime. Classic examples of this might be to represent the time that a convenience store opens, say from 07:00 to 23:00 (7am to 11pm). Such stores might open at those hours across the whole of the USA, but the times are local ignoring time-zone.
LocalTime is a value type with no associated date or time-zone. When adding or subtracting an amount of time, it will wrap around midnight. Thus, 20:00 plus 6 hours results in 02:00.
Using a LocalTime is similar to using LocalDate:
LocalTime time = LocalTime.of(20, 30); int hour = date.getHour(); // 20 int minute = date.getMinute(); // 30 time = time.withSecond(6); // 20:30:06 time = time.plusMinutes(3); // 20:33:06
The adjuster mechanism also works with LocalTime, however there are fewer complicated manipulations of times that call for it.
Combined date and time
The next class to consider is LocalDateTime. This value type is a simple combination of LocalDate and LocalTime. It represents both a date and a time without a time-zone.
A LocalDateTime is created either directly, or by combining a date and time:
LocalDateTime dt1 = LocalDateTime.of(2014, Month.JUNE, 10, 20, 30); LocalDateTime dt2 = LocalDateTime.of(date, time); LocalDateTime dt3 = date.atTime(20, 30); LocalDateTime dt4 = date.atTime(time);
The third and fourth options use atTime() which provides a fluent way to build up a date-time. Most of the supplied date and time classes have “at” methods which can be used in this way to combine the object you have with another object to form a more complex one.
The other methods on LocalDateTime are similar to those of LocalDate and LocalTime. This familiar pattern of methods is very useful to help learn the API. This table summarises the method prefixes used:
Static factory methods that create an instance from constituent parts.
Static factory methods that try to extract an instance from a similar object. A from() method is less type-safe than an of() method.
Static factory method that obtains an instance at the current time.
Static factory method that allows a string to be parsed into an instance of the object.
Gets part of the state of the date-time object.
Checks if something is true or false about the date-time object.
Returns a copy of the date-time object with part of the state changed.
Returns a copy of the date-time object with an amount of time added.
Returns a copy of the date-time object with an amount of time subtracted.
Converts this date-time object to another, which may represent part or all of the state of the original object.
Combines this date-time object with additional data to create a larger or more complex date-time object.
Provides the ability to format this date-time object.
When dealing with dates and times we usually think in terms of years, months, days, hours minutes and seconds. However, this is only one model of time, one I refer to as “human”. The second common model is “machine” or “continuous” time. In this model, a point on the time-line is represented by a single large number. This approach is easy for computers to deal with, and is seen in the UNIX count of seconds from 1970, matched in Java by the millisecond count from 1970.
The java.time API provides a machine view of time via the Instant value type. It provides the ability to represent a point on the time-line without any other contextual information, such as a time-zone. Conceptually, it simply represents the number of seconds since the epoch of 1970 (Midnight at the start of the 1st of January 1970 UTC). Since the API is based on nanoseconds, the Instant class provides the ability to store the instant to nanosecond precision.
Instant start = Instant.now(); // perform some calculation Instant end = Instant.now(); assert end.isAfter(start);
The Instant class will typically be used for storing and comparing timestamps, where you need to record when an event occurred but do not need to record any information about the time-zone it occurred in.
In many ways, the interesting aspect of Instant is what you cannot do with it, rather than what you can. For example, these lines of code will throw exceptions:
instant.get(ChronoField.MONTH_OF_YEAR); instant.plus(6, ChronoUnit.YEARS);
They throw exceptions because Instant only consists of a number of seconds and nanoseconds and provides no ability to handle units meaningful to humans. If you need that ability, you need to provide time-zone information.
The concept of time-zones was introduced by the UK where the invention of the railway, and other improvements in communication, suddenly meant that people could cover distances where the change in solar time was important. Up until that point, every village and town had its own definition of time based on the sun and typically reckoned by sundial.
An example of the confusion this brought initially is shown in this photo of the clock on the Exchange building in Bristol, UK. The red hands show Greenwich Mean Time while the black hand shows Bristol Time, 10 minutes different:
A standard system of time-zones evolved, driven by technology, replacing the older local solar time. But the key fact is that time-zones are political creations. They are often used to demonstrate political control over an area, such as the recent change in Crimea to Moscow time. As with anything political, the associated rules frequently defy logic. The rules can and do change with very little notice.
The rules of time-zones are collected and gathered by an international group who publish the IANA time-zone database. This set of data contains an identifier for each region on the Earth and a history of time-zone changes there. The identifiers are of the form “Europe/London” or “America/New_York”.
Before the java.time API, you used the TimeZone class to represent time-zones. Now you use the ZoneId class. There are two key differences. Firstly, ZoneId is immutable, which provides that ability to store instances in static variables amongst other things. Secondly, the actual rules themselves are held in ZoneRules, not in ZoneId itself, simply call getRules() on ZoneId to obtain the rules.
One common case of time-zone is a fixed offset from UTC/Greenwich. You commonly encounter this when you talk about time differences, such as New York being “5 hours behind London”. The class ZoneOffset , a subclass of ZoneId, represents the offset of a time from the zero-meridian of Greenwich, London.
As a developer, it would be nice to not have to deal with time-zones and their complexities. The java.time API allows you to do that so far as it is possible. Wherever you can, use the LocalDate, LocalTime, LocalDateTime and Instant classes. When you cannot avoid time-zones, the ZonedDateTime class handles the requirement.
The ZonedDateTime class manages the conversion from the human time-line, seen on desktop calendars and wall clocks, to the machine time-line, the ever incrementing count of seconds. As such, you can create a ZonedDateTime from either a local class or an instant:
ZoneId zone = ZoneId.of("Europe/Paris"); LocalDate date = LocalDate.of(2014, Month.JUNE, 10); ZonedDateTime zdt1 = date.atStartOfDay(zone); Instant instant = Instant.now(); ZonedDateTime zdt2 = instant.atZone(zone);
One of the most annoying parts of time-zones is Daylight Saving Time (DST). With DST, the offset from Greenwich is changed two (or more) times a year, typically moving forward in Spring and back in Autumn/Fall. When these adjustments happen, we all have to change the wall clocks dotted around the house. These changes are referred to by java.time as offset transitions. In Spring there is a "gap" in the local time-line where some local times do not occur. By contrast, in Autumn/Fall there is an "overlap" when some local times occur twice.
The ZonedDateTime class handles this in its factory methods and manipulation methods. For example, adding one day will add a logical day, which may be more or less than 24 hours if the DST boundary is crossed. Similarly, the method atStartOfDay() is so named because you cannot assume that the resulting time will be midnight - there might be a DST gap from midnight to 1am.
One final tip on DST. If you want to demonstrate that you have thought about what should happen in a DST overlap (where the same local time occurs twice), you can use one of the two special methods dedicated to handling overlaps:
zdt = zdt.withEarlierOffsetAtOverlap(); zdt = zdt.withLaterOffsetAtOverlap();
Use of one of these two methods will select the earlier or later time if the object is in a DST overlap. In all other circumstances, the methods will have no effect.
Amounts of time
The date and time classes discussed so far represent points on the time-line in various ways. Two additional value types are provided for amounts of time.
The Duration class represents an amount of time measured in seconds and nanoseconds. For example, "23.6 seconds".
The Period class represents an amount of time measured in years, months and days. For example, "3 years, 2 months and 6 days".
These can be added to, and subtracted from, the main date and time classes:
Period sixMonths = Period.ofMonths(6); LocalDate date = LocalDate.now(); LocalDate future = date.plus(sixMonths);
Formatting and Parsing
An entire package is devoted to formatting and printing dates and times - java.time.format. The package revolves around DateTimeFormatter and its associated builder DateTimeFormatterBuilder.
The most common ways to create a formatter are static methods and constants on DateTimeFormatter. These include:
- Constants for commons ISO formats, such as ISO_LOCAL_DATE.
- Pattern letters, such as ofPattern("dd/MM/uuuu").
- Localized styles, such as ofLocalizedDate(FormatStyle.MEDIUM).
Once you have a formatter, you typically use it by passing it to the relevant method on the main date and time classes:
DateTimeFormatter f = DateTimeFormatter.ofPattern("dd/MM/uuuu"); LocalDate date = LocalDate.parse("24/06/2014", f); String str = date.format(f);
In this way, you are insulated from the format and parse methods on the formatter itself.
If you need to control the locale of formatting, use the withLocale(Locale) method on the formatter. Similar methods allow the calendar system, time-zone, decimal numbering system and resolution of parsing to be controlled.
If you need even more control, see the DateTimeFormatterBuilder class, which allows complex formats to be built up step by step. It also provides the ability to have case insensitive parsing, lenient parsing, padding and optional sections of the formatter.
The java.time API is a new comprehensive model for date and time in Java SE 8. It takes the ideas and implementation started in Joda-Time to the next level and finally allows developers to leave java.util.Date and Calendar behind. Its definitely time to enjoy date and time programming again!
- Official tutorial at Oracle
- Unofficial project home page
- ThreeTen-Extra project, with additional classes that supplement core Java SE
About the Author
Stephen Colebourne is a Java Champion and JavaOne Rock Star speaker. He has been working with Java since version 1.0 and contributing to open source software since 2000. He has made major contributions to Apache Commons and created the Joda open source projects including Joda-Time. He blogs on Java topics, is a frequent conference speaker and contributor to discussions on language change such as Project Lambda. He was co-spec lead on JSR-310, which created the new java.time API in Java SE 8.