Handling dates in Java
Inspired by a dzone article I decided to take a stab on this old issue which I’ve had to tackle quite a few times over the years.
The date/time API in Java is notoriously painful to work with. Not to make it any easier, there are also some things that are generally not very well understood about it. Perhaps the biggest misconception is that Date objects contain time zone and DST information. They do not. Date in Java is only a glorified long. Ie. it is a wrapper for milliseconds elapsed since 00:00:00 UTC on 1 January 1970, aka the epoch. Understanding this makes reasoning about time in Java a lot easier.
Okay, so Date is only a bunch of milliseconds. What if I need the representation of some arbitrary date? Do I calculate it from the epoch? How about printing a date? No worries, DateFormat is here to help. The API documentation states that “DateFormat is an abstract class for date/time formatting subclasses which formats and parses dates or time”. DateFormat needs a date pattern which it uses for parsing and formatting. Once provided, using DateFormat is pretty straight-forward.
//parsing a date from string DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"); Date dateOne = df.parse("2011-02-08 10:00:00 +0300"); //formatting a string representation given a date and a pattern String dateStr = new SimpleDateFormat("EE, dd MMMM yyyy z").format(dateOne); System.out.println(dateStr); //Tue, 08 February 2011 EET
Since DateFormat is an abstract class, we need an implementation for instantiating it. That’s what we get with SimpleDateFormat. The cool thing about DateFormat is that it isn’t overly complex to use and given enough information it handles time zones as well.
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"); Date dateOne = df.parse("2011-02-08 10:00:00 +0300"); Date dateTwo = df.parse("2011-02-08 08:00:00 +0100"); //compare the millisecond representations long timeDiff = dateOne.getTime() - dateTwo.getTime(); System.out.println(String.format("difference %s milliseconds.", timeDiff)); //difference 0 milliseconds.
Here we parsed two dates from strings and compared them. The (wall clock) time in the dates is different but since they are in separate time zones, they actually represent the same point in time.
DateFormat looks pretty simple. However, there is more than meets the eye. One could think that besides the date pattern, DateFormat is essentially a stateless parser/formatter. Wrong! This is hinted by the methods getCalendar() and setCalendar(Calendar newCalendar). Ie. a Calendar object plays an essential role in the implementation of DateFormat.
This is where things start to go downhill. First of all, DateFormat is not thread safe. The API document states this clearly: “Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.” You might be tempted to create a set of static convenience methods using DateFormat and place them in a util class which can easily be used from all over the codebase. Don’t do that! The chances are, you will run into hairy concurrency issues.
What if we need to add or subtract seconds or minutes or some other time unit to/from the date? There are at least a couple of ways of doing this, none of which is particularly elegant and the current Date API itself doesn’t provide a method for doing so. First of all you can expose the implementation details and manipulate the milliseconds directly. Date has a method getTime() for that purpose.
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"); Date dateOne = df.parse("2011-02-08 10:00:00 +0200"); //2011-02-08 10:00:00 +0200 //an hour worth of milliseconds long hour = 1000 * 60 * 60; //Create a new Date from milliseconds Date dateTwo = new Date(dateOne.getTime() + (2 * hour)); System.out.println(df.format(dateTwo)); //2011-02-08 12:00:00 +0200 //Change the milliseconds of an existing date dateTwo.setTime(dateOne.getTime() + (3 * hour)); System.out.println(df.format(dateTwo)); //2011-02-08 13:00:00 +0200
When you need to convert time units to other time units, a utility class called TimeUnit can save you time and make the code more readable.
//Instead of... long hour = 1000 * 60 * 60; dateTwo.setTime(dateOne.getTime() + (3 * hour)); //...you could write dateTwo.setTime(dateOne.getTime() + TimeUnit.HOURS.toMillis(3));
You may have noticed that the dates were printed in the same timezone as dateOne was parsed. This is only a coincidence, since unless explicitly specified, DateFormat uses the system time zone which in this case just happens to be +0200 (EET in normal time). Consider the following:
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"); Date dateOne = df.parse("2011-02-08 10:00:00 +0100"); System.out.println(df.format(dateOne)); //2011-02-08 11:00:00 +0200 df.setTimeZone(TimeZone.getTimeZone("CET")); System.out.println(df.format(dateOne)); //2011-02-08 10:00:00 +0100
As you can see, the first day is not formatted with the time zone offset +0100 even though it was parsed from such a string. If you want to get the String representation of a Date in a certain time zone, you have to explicitly set a TimeZone object to the DateFormat. Here the TimeZone object was created by using a static factory method getTimeZone(String timeZoneId). An array of available time zone ids can be accessed by the method getAvailableIDs(). The time zone offset can also be set using the method setRawOffset(int offsetMillis). For public constructors, consult the TimeZone API.
I mentioned there are at least couple of ways for changing a date. In addition to manipulating the milliseconds directly, a Calendar object can be used. Calendar is an abstract base class for calendar implementations. For instantiating it, you need its implementation, GregorianCalendar.
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"); Date date = df.parse("2010-11-08 10:00:00 +0200"); Calendar cal = new GregorianCalendar(); cal.setTime(date); TimeZone helsinkiTz = TimeZone.getTimeZone("Europe/Helsinki"); cal.setTimeZone(helsinkiTz); cal.add(Calendar.HOUR_OF_DAY, 2); Date newDate = cal.getTime(); df.setTimeZone(helsinkiTz); System.out.println(df.format(newDate)); //2010-11-08 12:00:00 +0200
For me, Calendar is the single biggest reason why date/time manipulation in Java is unpleasant to work with. The API is not very well designed. It is outdated and it also has several misleading characteristics. By outdated, I refer to its heavy use of int constants. Calendar was added to the Java standard library before we had enums in the language, but even though enums have been around since Java 1.5, the Calendar API makes no use of them.
If the code is written using the static final ints in the Calendar class, the code can be semi-readable. However, because of the way the API was designed, it easily leads to code, where magic numbers or custom constants are used in place of the named constants in the Calendar class. This is confusing for the reader and can cause some tricky bugs.
//the calendar object from the previous example holding the date //2010-11-08 12:00:00 +0200 cal.set(Calendar.MONTH, 1);
What do you think the new Date is? If you thought the month was changed to January, you guessed wrong. The new month is February. Ie. Calendar.FEBRUARY = 1. Not so easy to spot.
One common case where the date/time API doesn’t shine is when you need to find out some property of a date. Eg. the day or the month or the year. I’m not too happy about the options provided for this. You can use DateFormat to format the date to a string and then parse the needed information, but that’s error-prone and not very elegant. Another way is to create a calendar, set the time zone and use the method getField() with the correct constant, but this is not very straight-forward either for such a simple task.
Yet another poorly supported feature is time periods. There are no classes manipulating intervals between dates, doing calculation for them etc.
Is the JDK standard date/time API the only option? Fortunately not. Perhaps the best-known alternative is an open source library called Joda-Time. Joda-Time was born out of frustration with the standard API. Not everyone is happy with Joda-Time either. Another open source library, Date4j is a light-weight alternative, that may appeal to those who don’t need n+1 classes for handling their dates. Then there is JSR 310, which is the proposal for the new and improved Date/Time API coming for Java SE 7. JSR 310 is largely based on Joda-Time. JDK 7 is supposed to be released later this year. We’ll see…