Java 8 was released 3 years ago (March 2014) and brought a lot of language improvements. One of those is new Date and Time API for Java, also known as JSR-310. It represents a very rich API for working with dates and times. Yet, I see many developers still using good old java.util.Date
and java.util.Calendar
classes in the code they write today.
Sure, we still have to interact with legacy applications and old APIs, using mentioned classes. But this does not mean we can not use new java.time
API when writing new code or refactoring the old one. Why we would like to do so? Well, using the new API is simpler, more straightforward, flexible, easier to understand, classes are immutable and hence thread safe… just to mention a few.
What’s wrong with the old Java Date API?
java.util.Date
has some serious design flows, from the day it was introduced. Many of its methods were deprecated since Java 1.1 and ported to (abstract) java.util.Calendar
and java.util.GregorianCalendar
.
java.util.Date
is poorly understood by developers. It’s been badly abused by library authors, adding further to the confusion. A Date
instance represents an instant in time, not a date. Importantly, that means:
- It doesn’t have a time zone.
- It doesn’t have a format.
- It doesn’t have a calendar system.
Some other problems are:
- It rates years as two digits since 1900. There are many workarounds in the Java world around this banal design decision, like handling years before 1900.
- Months are zero indexed (0 – January, 11 – December). Not very intuitive and led to many off-by-one errors.
- All classes in this old API are mutable. As a result, any time you want to give a date back (say, as an instance structure) you need to return a clone of that date instead of the date object itself (since otherwise, people can mutate your structure). Date formatting classes are also not thread safe. You have to always take care about that or it could lead to some unexpected behaviors.
Date
represents aDateTime
, but in order to defer to those in SQL land, there’s another subclassjava.sql.Date
, which represents a single day (though without a timezone associated with it).- It implicitly uses the system-local time zone in many places – including toString() – which confuses many developers.
Read more about this topic on this blog post.
New Date and Time API for Java
In order to address these problems and provide better support in the JDK core, a new date and time API, which is free of these problems, has been designed for Java SE 8.
The project has been led jointly by the author of Joda-Time (Stephen Colebourne) and Oracle, under JSR 310, and appeared in the new Java SE 8 package java.time
.
Core Ideas
The new API is driven by three core ideas:
- Immutable-value classes. One of the serious weaknesses of the existing formatters (like
java.util.SimpleDateFormat
) in Java is that they aren’t thread-safe. - Domain-driven design. The new API models its domain very precisely with classes that represent different use cases for Date and Time closely. This emphasis on domain-driven design offers long-term benefits around clarity and understandability, but you might need to think through your application’s domain model of dates when porting from previous APIs to Java SE 8.
- Separation of chronologies. The new API allows people to work with different “non-ISO-8601” calendaring systems, like one used in Japan or Thailand.
Real life examle: We’ve recently encounter a problem in one of the projects. Someone reused an instance of
SimpleDateFormat
in the XML exporting logic. In development and test environments we did not notice any problem. But in production environment, on a bit increased load, each 10th or so execution suffered from this issue, mixing up printed dates.
New API in a nutshell
The new Date and Time API is moved to java.time
package and Joda time format is followed. Classes are immutable and hence thread-safe. There are many static methods you can use directly. For every date-time manipulation, there is probably already implemented method to use. The new java.time
package contains all the classes for date, time, date/time, time zones, instants, duration, and clocks manipulation. Example classes:
Clock
provides access to the current instant, date and time using a time-zone.LocaleDate
holds only the date part without a time-zone in the ISO-8601 calendar system.LocaleTime
holds only the time part without time-zone in the ISO-8601 calendar system.- The
LocalDateTime
combines together LocaleDate and LocalTime and holds a date with time but without a time-zone in the ISO-8601 calendar system. ZonedDateTime
holds a date with time and with a time-zone in the ISO-8601 calendar system.Duration
class represents an amount of time in terms of seconds and nanoseconds. It makes very easy to compute the different between two time values. Period, on the other hand, performs a date based comparison between two dates.
Examples:
// Get the local date final LocalDate date = LocalDate.now(); // Get the local time final LocalTime time = LocalTime.now(); // Get the local date/time final LocalDateTime datetime = LocalDateTime.now(); // Get duration between two dates final LocalDateTime from = LocalDateTime.of( 2014, Month.APRIL, 16, 0, 0, 0 ); final LocalDateTime to = LocalDateTime.of( 2015, Month.APRIL, 16, 23, 59, 59 ); final Duration duration = Duration.between( from, to );
Get more info from official documentation. See more code examples here.
Java version 9 is just around the corner (to be released end of
July 2017September 2017) and will add even more features tojava.time
API.
Parsing date and time
To create a LocalDateTime
object from a string you can use the static LocalDateTime.parse()
method. It takes a string and a DateTimeFormatter
as parameter. The DateTimeFormatter
is used to specify the date/time pattern.
String str = "2017-04-08 12:30"; DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); LocalDateTime dateTime = LocalDateTime.parse(str, formatter);
Formatting date and time
To create a formatted string out a LocalDateTime
object you can use the format()
method.
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); LocalDateTime dateTime = LocalDateTime.of(2017, Month.APRIL, 8, 12, 30); String formattedDateTime = dateTime.format(formatter); // "2017-04-08 12:30"
Note that there are some commonly used date/time formats predefined as constants in DateTimeFormatter
. For example: Using DateTimeFormatter.ISO_DATE_TIME
to format the LocalDateTime
instance from above would result in the string "2017-04-08T12:30:00"
.
The parse()
and format()
methods are available for all date/time related objects (e.g. LocalDate
or ZonedDateTime
).
Note that
DateTimeFormatter
is immutable and thread-safe, so you can freely reuse its instance between different threads.
Converting between types of old and new Date and Time APIs
A java.util.Date
object basically represents a moment on the timeline in UTC, a combination of a date and a time-of-day. We can translate that to any of several types in java.time
.
Java 8 has added toInstant()
method which helps to convert existing java.util.Date
and java.util.Calendar
instance to new Date Time API, as in the following code snippet:
java.util.Date date = ...; java.util.Calendar calendar = ...; LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()); LocalDateTime.ofInstant(calendar.toInstant(), ZoneId.systemDefault());
There is, though, one issue, if you try to use this approach to convert and instance of java.sql.Date
(extends java.util.Date
). Method toInstant()
of java.sql.Date
always throws an UnsupportedOperationException
and should not be used because SQL Date values do not have a time component. But there is a method toLocalDate()
, which converts java.sql.Date
object to a LocalDate
. So, the best approach is to have a small utility class to be used for converting back and forth between old and new java Date and Time APIs. Here is an example from Stackoverflow:
/** * Utilities for conversion between the old and new JDK date types * (between {@code java.util.Date} and {@code java.time.*}). * * <p> * All methods are null-safe. */ public class DateConvertUtils { /** * Calls {@link #asLocalDate(Date, ZoneId)} with the system default time zone. */ public static LocalDate asLocalDate(java.util.Date date) { return asLocalDate(date, ZoneId.systemDefault()); } /** * Creates {@link LocalDate} from {@code java.util.Date} or it's subclasses. Null-safe. */ public static LocalDate asLocalDate(java.util.Date date, ZoneId zone) { if (date == null) return null; if (date instanceof java.sql.Date) return ((java.sql.Date) date).toLocalDate(); else return Instant.ofEpochMilli(date.getTime()).atZone(zone).toLocalDate(); } /** * Calls {@link #asLocalDateTime(Date, ZoneId)} with the system default time zone. */ public static LocalDateTime asLocalDateTime(java.util.Date date) { return asLocalDateTime(date, ZoneId.systemDefault()); } /** * Creates {@link LocalDateTime} from {@code java.util.Date} or it's subclasses. Null-safe. */ public static LocalDateTime asLocalDateTime(java.util.Date date, ZoneId zone) { if (date == null) return null; if (date instanceof java.sql.Timestamp) return ((java.sql.Timestamp) date).toLocalDateTime(); else return Instant.ofEpochMilli(date.getTime()).atZone(zone).toLocalDateTime(); } /** * Calls {@link #asUtilDate(Object, ZoneId)} with the system default time zone. */ public static java.util.Date asUtilDate(Object date) { return asUtilDate(date, ZoneId.systemDefault()); } /** * Creates a {@link java.util.Date} from various date objects. Is null-safe. Currently supports:<ul> * <li>{@link java.util.Date} * <li>{@link java.sql.Date} * <li>{@link java.sql.Timestamp} * <li>{@link java.time.LocalDate} * <li>{@link java.time.LocalDateTime} * <li>{@link java.time.ZonedDateTime} * <li>{@link java.time.Instant} * </ul> * * @param zone Time zone, used only if the input object is LocalDate or LocalDateTime. * * @return {@link java.util.Date} (exactly this class, not a subclass, such as java.sql.Date) */ public static java.util.Date asUtilDate(Object date, ZoneId zone) { if (date == null) return null; if (date instanceof java.sql.Date || date instanceof java.sql.Timestamp) return new java.util.Date(((java.util.Date) date).getTime()); if (date instanceof java.util.Date) return (java.util.Date) date; if (date instanceof LocalDate) return java.util.Date.from(((LocalDate) date).atStartOfDay(zone).toInstant()); if (date instanceof LocalDateTime) return java.util.Date.from(((LocalDateTime) date).atZone(zone).toInstant()); if (date instanceof ZonedDateTime) return java.util.Date.from(((ZonedDateTime) date).toInstant()); if (date instanceof Instant) return java.util.Date.from((Instant) date); throw new UnsupportedOperationException("Don't know hot to convert " + date.getClass().getName() + " to java.util.Date"); } /** * Creates an {@link Instant} from {@code java.util.Date} or it's subclasses. Null-safe. */ public static Instant asInstant(Date date) { if (date == null) return null; else return Instant.ofEpochMilli(date.getTime()); } /** * Calls {@link #asZonedDateTime(Date, ZoneId)} with the system default time zone. */ public static ZonedDateTime asZonedDateTime(Date date) { return asZonedDateTime(date, ZoneId.systemDefault()); } /** * Creates {@link ZonedDateTime} from {@code java.util.Date} or it's subclasses. Null-safe. */ public static ZonedDateTime asZonedDateTime(Date date, ZoneId zone) { if (date == null) return null; else return asInstant(date).atZone(zone); } }
Java 8 Date and Time API external libraries support
New Date and Time API and JPA
JPA 2.1 was released before Java 8 and therefore doesn’t support the new Date and Time API. If you want to use the new classes (in the right way), you need to define the conversion to java.sql.Date
and java.sql.Timestamp
yourself. This can be easily done by implementing the AttributeConverter<EntityType, DatabaseType>
interface and annotating the class with @Converter(autoApply=true)
. By setting autoApply=true
, the converter will be applied to all attributes of the EntityType
and no changes on the entity are required. See more details here.
New Date and Time API and Hibernate
Since Hibernate version 5.0.0, there is an additional library which gives support for Java 8 Date and Time API. You just need to add this dependency to your project:
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-java8</artifactId> <version>${hibernate.version}</version> </dependency>
Starting with Hibernate 5.2.x this addition is not necessary anymore since these Java 8 features were merged into Hibernate Core.
New Date and Time API with XML/JSON libraries
Most of the libraries used for generating XML or JSON data out of Java objects, still do not support java.time
API out of the box. But there are already different adapters implementations, to support marshalling/unmarshalling of new java.time
classes. It does require some additional configuration into the code but it is straightforward and worth of efforts.
Java Architecture for XML Binding (JAXB) does not support new java.time
API. JAXB adapters for Java 8 Date and Time API (JSR-310) types aims to address the issue by providing a collection of type adapters to enable use of Java 8 Date and Time API types in JAXB bindings.
If you are using GSON, there is gson-javatime-serialisers. A set of GSON serialiser/deserialisers for dealing with Java 8 java.time
entities. Wherever possible, ISO 8601 string representations are used. You just need to register appropriate class adapter, when building Gson
object with GsonBuilder
.
In the similar way you can add register modules to Jackson, to add support for Java 8 java.time
entities (as well as support for Optional
s and parameter names). Check Jackson Modules for Java 8 library.
Not yet on Java 8?
If your legacy project is still building on Java 6 or Java 7, you have an option to use ThreeTen-Backport. It provides a backport of the Java SE 8 date-time classes to Java SE 6 and 7. Although this backport is NOT an implementation of JSR-310, it is a simple backport intended to allow users to quickly use the JSR-310 API on Java SE 6 and 7.
Backporting is the action of taking parts from a newer version of a software system or software component and porting them to an older version of the same software (from Wikipedia).
Another option is to use Joda Time. Joda-Time provides a quality replacement for the Java date and time classes. Joda-Time is the de facto standard date and time library for Java prior to Java SE 8. Note that from Java SE 8 onwards, users are asked to migrate to java.time
(JSR-310) – a core part of the JDK which replaces this project.
References
JSR 310: Date and Time API
A deeper look into the Java 8 Date and Time API
Stackoverflow: What’s wrong with Java Date & Time API?
All about java.util.Date
Java SE 8 Date and Time
How to persist LocalDate and LocalDateTime with JPA
Stackoverflow: Convert java.util.Date to what “java.time” type?
Pingback: Bluesoft News #37 - Melhores de Outubro - Bluesoft Labs
Pingback: A BRIEF HISTORY OF TIME … in Java – My joy of coding
Pingback: New Date and Time API in Java 8 – My joy of coding
Pingback: Czas w Javie ⏱️. Dobrze używasz? - bdabek.pl