Overview

It’s About Time

No Comments

Everyone who has been working with Java for a while knows that the it lacks a decent API for working with dates, times and the like. In this blog post I want to briefly summarize what the problems with the existing APIs are. Then I’m going to discuss the new Java 8 Date-Time API.

It all began with java.util.Date. Although relatively simple and strait forward to use, this class has a series of flaws. At first, java.util.Date is not a date, but “a specific instant in time, with millisecond precision”. You might also be surprised by the output of

System.out.println("Current year: " + new Date().getYear());

which in fact writes the current year minus 1900 to your terminal. Generally, the representations used by the various getters and setters of the date class are quite irregular. Also, being a lightweight value type, java.util.Date should clearly be immutable, which it is not. But the most serious flaw of the java.util.Date class is that is doesn’t properly support time zones. This is the
reason why java.util.Calendar was born. Unfortunately java.util.Calendar was never very popular among Java developers, as the related APIs are quite cumbersome. Also, as with java.util.Date, there is no proper way to deal with time intervals. Try to compute the number of days since you where born to see what I mean. To cut a long story short:

java.util.Date is a testament to the fact that even brilliant programmers can screw up. java.util.Calendar, which Sun licensed to rectify the Date mess, is a testament to the fact that average programmers can screw up, too.
http://stackoverflow.com/questions/1571265/why-is-the-java-date-api-java-util-date-calendar-such-a-mess

This is the reason why a lot of Java developers, including me, avoid the JDK date and time APIs wherever possible and use Joda Time instead.

With the upcoming release of Java 8, another date and time API is entering the picture. Before diving into details I want to give you a rough overview of the new API and discuss how it compares to its predecessors. Since JSR-310 was mostly driven by the creator of Joda Time, you will indeed notice a lot of similarities to the aforementioned library. Don’t expect Java 8 to bundle a copy of Joda Time under a different package though. There is a very interesting blog post where Stephen Colebourne explains the rationale behind some of the differences between Joda Time and the new Java time API. These include

  • A different approach for supporting alternative calendar systems.
  • Factory methods are strongly favoured over constructors (which are mostly private).
  • A clear separation between human and machine timelines.
  • Null arguments are treated as errors.

However, apart from the points mentioned above, the new Java 8 time API feels very much like Joda Time:

  • All date time classes are immutable.
  • Class and method names are often very similar or identical.
  • The library uses unchecked exceptions only.
  • The API is powerful and easy to use.

Notice the sharp contrast to java.util.Calendar, that is neither powerful nor easy to use, and heavily relies on mutable state. Another key difference of the new API to java.util.Calendar and the broken java.util.Date is that concepts like a date without a time, or a time without a date, are properly supported. The same applies for date time arithmetic and durations.

Let’s take a look at some simple examples. We start with a small program that deals with birthdays:

package de.codecentric.java.time;
 
import java.time.LocalDate;
import java.time.MonthDay;
import java.time.temporal.ChronoUnit;
 
public class Birthdays {
    public static void main(String[] args) {
        LocalDate dateOfBirth = LocalDate.of(1981, 5, 1);
        System.out.println("You are " + getDaysAlive(dateOfBirth) + " days alive;"
            + " your next birthday is in " 
            + getDaysToNextBirthday(MonthDay.from(dateOfBirth)) + " day(s).");
    }
 
    private static long getDaysAlive(LocalDate dateOfBirth) {
        return ChronoUnit.DAYS.between(dateOfBirth, LocalDate.now());
    }
 
    private static long getDaysToNextBirthday(MonthDay birthday) {
        LocalDate nextBirthday = getNextBirthday(birthday);
        return ChronoUnit.DAYS.between(LocalDate.now(), nextBirthday);
 
    }
 
    private static LocalDate getNextBirthday(MonthDay birthday) {
        LocalDate today = LocalDate.now();
        LocalDate birthdayThisYear = birthday.atYear(today.getYear());
        if(birthdayThisYear.isAfter(today) || birthdayThisYear.equals(today))
            return birthdayThisYear;
        return birthdayThisYear.plusYears(1);
    }
}

The code should be pretty self explanatory, so I won’t elaborate on it in detail, but you should notice the use of LocalDate, which is a date without a time or a timezone, as well as the MonthDay class, that just represents a month with a day.

In the next example we obtain the current time in Vladivostok:

package de.codecentric.java.time;
 
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
 
public class TimeInVladivostok {
    public static void main(String[] args) {
        System.out.println("Time in Vladivostok: " + getTimeInVladivostok());
    }
 
    private static LocalTime getTimeInVladivostok() {
        return ZonedDateTime.now(ZoneId.of("Asia/Vladivostok")).toLocalTime();
    }
}

The code is completely straight forward as it should be. ZonedDateTime is a date and a time with time zone information. LocalTime, which is the return type of ZonedDateTime#toLocalTime() is a time without a date and without a time zone. The next example is about DateTimeFormatters:

package de.codecentric.java.time;
 
import static org.junit.Assert.assertEquals;
 
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
 
import org.junit.Test;
 
public class TestDateTimeFormatters {
    private static final DateTimeFormatter 
        FMT_LOCAL_DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd"),
        FMT_LOCAL_TIME = DateTimeFormatter.ofPattern("HH:mm");
 
    @Test
    public void testParse() {
        assertEquals(LocalDate.of(1999, 12, 31), LocalDate.parse("1999-12-31"));
        assertEquals(LocalTime.of(20, 15), LocalTime.parse("20:15", FMT_LOCAL_TIME));
    }
 
    @Test
    public void testFormat() {
        assertEquals("2007-11-12", LocalDate.of(2007, 11, 12).format(FMT_LOCAL_DATE));
        assertEquals("12:31", LocalTime.of(12, 31).format(FMT_LOCAL_TIME));
    }
 
    @Test(expected = DateTimeException.class)
    public void testIllegalParsing() {
        LocalDate.parse("23:59", FMT_LOCAL_TIME);
    }
 
    @Test(expected = DateTimeException.class)
    public void testIllegalFormatting() {
        LocalTime.of(23, 32).format(FMT_LOCAL_DATE);
    }
}

As you can see, the format string syntax is similar to SimpleDateFormat. Unlike SimpleDateFormat however, DateFormatters are thread safe. Also note that we can use them together with different types. When doing so, some care has to be taken to only use combinations that make sense. Constructing a LocalDate from a formatter that only extracts the time of day can’t possibly work. The same is true for extracting date related information from a LocalTime. These cases are illustrated in TestDateTimeFormatters#testIllegalParsing and TestDateTimeFormatters#testIllegalFormatting in the example above.

Another important class you should be aware of is Instant. It represents a single point on the time line, without any timezone information, in other words a timestamp. Executing

package de.codecentric.java.time;
 
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
 
public class OneInstantMultipleTimes {
    public static void main(String[] args) {
        Instant zero = Instant.EPOCH;
 
        System.out.println("Start of the Epoch in Moscow    : " + toLocalDateTime(zero, "Europe/Moscow"));
        System.out.println("Start of the Epoch in Washington: " + toLocalDateTime(zero, "America/New_York"));
    }
 
    private static LocalDateTime toLocalDateTime(Instant instant, String zoneId) {
        return instant.atZone(ZoneId.of(zoneId)).toLocalDateTime();
    }
}

demonstrates how one instant can be tied to different local dates and times:

Start of the Epoch in Moscow    : 1970-01-01T03:00
Start of the Epoch in Washington: 1969-12-31T19:00

It is therefore illegal to extract things like the year, moth, weekdays and so forth from an Instant, although the API might tempt you to do so. The following statement for example compiles flawlessly

Year year = Year.from(Instant.now()));

but fails with an exception at runtime. Luckily there is Year.now(), which should normally to what you want without any nasty surprises.

In this connection I should also mention Duration and Period. A Duration models a time based amount, like 42 seconds, while a Period stands for a date based amount like 1 year, 3 month and 20 days. There is another, subtle difference between Period and Duration, as they might behave differently when added to a ZonedDateTime:

package de.codecentric.java.time;
 
import java.time.Duration;
import java.time.Period;
import java.time.ZoneId;
import java.time.ZonedDateTime;
 
public class DurationVsPeriod {
    private static final ZoneId ZONE_ID = ZoneId.of("Europe/Berlin");
 
    public static void main(String[] args) {
        ZonedDateTime beforeDstOverlap = ZonedDateTime.of(2013, 10, 26, 12, 0, 0, 0, ZONE_ID);
        Duration dayDuration = Duration.ofDays(1);
        Period dayPeriod = Period.ofDays(1);
 
        System.out.println("beforeDstOverlap.plus(dayDuration): " + beforeDstOverlap.plus(dayDuration));
        System.out.println("beforeDstOverlap.plus(dayPeriod)  : " + beforeDstOverlap.plus(dayPeriod));
    }
}

Bear in mind that beforeDstOverlap is actually on the last day of daylight saving time when interpreting the output of the above program:

beforeDstOverlap.plus(dayDuration): 2013-10-27T11:00+01:00[Europe/Berlin]
beforeDstOverlap.plus(dayPeriod)  : 2013-10-27T12:00+01:00[Europe/Berlin]

As you can see, adding a Duration of a day is like adding the related number of seconds, while adding a Period maintains the local time.

The last part of the new date time API that I want to explicitly mention here is java.time.Clock. It is an abstract class that provides acess to the current instant and time zone and has been designed with dependency injection and test driven development in mind. Take a look at the following example to see how you can take advantage of it:

package de.codecentric.java.time;
 
import java.time.Clock;
import java.time.DayOfWeek;
import java.time.LocalDateTime;
import java.time.LocalTime;
 
public class Pub {
    private final Clock clock;
 
    public Pub(Clock clock) {
        this.clock = clock;
    }
 
    public boolean isOpen() {
        LocalDateTime now = LocalDateTime.now(clock);
        if(now.getDayOfWeek() == DayOfWeek.SUNDAY)
            return false;
 
        LocalTime time = now.toLocalTime();
        return time.isAfter(LocalTime.of(19, 0)) && time.isBefore(LocalTime.of(23, 0));
    }
}

In production, you could use Clock#systemDefaultZone(), but for testing you might find Clock#fixed() helpful.

Summarizing it seems that the JDK has a proper date time API at last. If you understand the basic concepts, the API is very pleasant to use and leads to self documenting code. The only downside is that the library could catch a few more errors statically (that is at compile time), than by throwing exceptions at runtime. Note that I’ve only scratched to surface of what you can do. A much more detailed discussion of this topic can be found here.

Comment

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