Still using java.util.Date? Don’t!

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 a DateTime, but in order to defer to those in SQL land, there’s another subclass java.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 2017 September 2017) and will add even more features to java.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 Optionals 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?

4 Comments

  1. Pingback: Bluesoft News #37 - Melhores de Outubro - Bluesoft Labs

  2. Pingback: A BRIEF HISTORY OF TIME … in Java – My joy of coding

  3. Pingback: New Date and Time API in Java 8 – My joy of coding

  4. Pingback: Czas w Javie ⏱️. Dobrze używasz? - bdabek.pl

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.