java.util.Date, java.util.Calendar and java.text.SimpleDateFormat performance

by Mikhail Vorontsov

In this post we will discuss memory footprint and speed of various classes used to represent and parse dates and times in core Java:

  • java.util.Date – Java 1.0 style datetime storage class, now absolutely useless
  • java.util.Calendar – Java 1.1+ style datetime storage class, supports time zones and locales, useful for date calculations
  • java.text.SimpleDateFormat – most commonly used datetime parser

Datetime storage classes

There are 2 core Java classes designed to store dates and times: java.util.Date and java.util.Calendar. Despite the fact the former is used in a lot of APIs, it is actually just an wrapper over a long timestamp field. It is also important to note that java.util.Date is not immutable, what further limits its use.

The latter one, java.util.Calendar, was added in Java 1.1 in order to support internationalization. Besides, it has a good support of datetime arithmetic operations, like adding 30 days to a given date, supporting all daylight savings issues as well. So, this class should be used for most of modern software datetime operations.

Still, when it comes to storing millions of datetimes, both these classes are a rather bad choice. Simply remember the footprint of instances of these classes in most common conditions:

java.util.Date java.util.GregorianCalendar
24 bytes 448 bytes

Yes, 448 bytes to keep a timestamp, locale and time zone. Take a look at your IDE debugger to see how much information is stored inside a java.util.GregorianCalendar – the most commonly used subclass of java.util.Calendar.

Most current systems deal with this problem by keeping just a timestamp, which could be stored in long field in general case, or even in int, if all timestamps belong to some predefined time span (like current date). In most cases a program is processing timestamps from one time zone at a time, which also helps storing long timestamps and keeping current time zone in a single separate variable. As a result, an array keeping a million of timestamps, will consume 8 Mb for long[], 28 Mb for Date[] an stunning 452 Mb for Calendar[]. Besides other benefits, such replacement will allow you to use Trove primitive collections to keep dates as longs.

How fast you can create Date and Calendar objects? Date is just a wrapper over long timestamp field (actually, there is one more deprecated Date field inside, which, nevertheless, consumes 8 more bytes per java.util.Date instance.
java.util.Calendar is actually normalizing all dates it is being created with (and which are changed via various set methods), so you can’t end up with 32.13.2012. Instead it will return 01/Feb/2013. All this normalization is an enormous logic (take a look at it JDK sources), so, not surprisingly, it consumes some time. Let’s see how long it will take to create 10 million objects:

java.util.Date java.util.GregorianCalendar.setTimeInMillis java.util.GregorianCalendar(6 arg constructor)
0.177 sec 6.624 sec 9.041 sec

As you can see, Date is created 35-50 times faster than GregorianCalendar. This is a difference worth to avoid in the high-performance code.

Date parsing

Many applications have to parse datetime arriving in string form. Generally, this is done by java.text.SimpleDateFormat class. This class greatly simplifies parsing, but, unfortunately, at a performance cost. Interpreting a format string, parsing an input date and checking its validity takes some time. But how much time does it take to parse a correct date? Two tests were created. Both of them parse "2012.01.01 13:34:47" date using the following SimpleDateFormat object:

1
2
3
4
5
6
7
private static DateFormat getFormat()
{
    final SimpleDateFormat sdf = new SimpleDateFormat( "yyyy'.'MM'.'dd' 'HH':'mm':'ss" );
    sdf.setTimeZone( TimeZone.getTimeZone( "UTC" ) ); //UTC is rather commonly used
    sdf.setLenient( false ); //strict parsing
    return sdf;
}
private static DateFormat getFormat()
{
    final SimpleDateFormat sdf = new SimpleDateFormat( "yyyy'.'MM'.'dd' 'HH':'mm':'ss" );
    sdf.setTimeZone( TimeZone.getTimeZone( "UTC" ) ); //UTC is rather commonly used
    sdf.setLenient( false ); //strict parsing
    return sdf;
}

The first test creates a SimpleDateFormat object once and reuses it. Second test creates such object on each iteration.

Create once and parse 10M times Create each time and parse 10M times
16.7 sec 34.8 sec

The first scenario is more common in batch processing, the second one – in event processing. SimpleDateFormat class is not thread safe, so you either need a newly created local object to parse your data or you can store it in a ThreadLocal:

1
2
3
4
5
6
7
private static final ThreadLocal<DateFormat> THREAD_SAFE_FORMAT = new ThreadLocal<DateFormat>()
{
    @Override
    protected DateFormat initialValue() {
        return getFormat();
    }
};
private static final ThreadLocal<DateFormat> THREAD_SAFE_FORMAT = new ThreadLocal<DateFormat>()
{
    @Override
    protected DateFormat initialValue() {
        return getFormat();
    }
};

ThreadLocal field access speed is about the same as the one of local variable in your method, so you should always keep DateFormat class instances in some long-term storage, like ThreadLocal.

But are 600,000 parsed dates per second the best we can achieve? Let’s implement strict parsing of the same pattern manually. Of course, all validity checks should be implemented. In the example implementation, IllegalArgumentException will be thrown in case of incorrect input, but you can update it to return 0 from parse method as well as to return boolean from checking methods.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
private static class FastDateParser
{
    private static final int[] DATE_DIGITS = { 0, 1, 2, 3, 5, 6, 8, 9 };
    private static final int[] DATETIME_DIGITS = { 0, 1, 2, 3, 5, 6, 8, 9, 11, 12, 14, 15, 17, 18 };
    private static final int[] MAX_DAY = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
 
    private final Map<String, long[]> m_cache = new HashMap<String, long[]>( 20 );
    private final TimeZone m_timeZone;
 
    public FastDateParser(TimeZone m_timeZone) {
        this.m_timeZone = m_timeZone;
    }
 
    private static class DateParts
    {
        public final int year;
        public final int month;
        public final int day;
 
        public DateParts(int year, int month, int day) {
            this.year = year;
            this.month = month;
            this.day = day;
        }
    }
 
    private static boolean allDigits( final String s, final int[] positions )
    {
        for ( final int pos : positions )
        {
            final char c = s.charAt( pos );
            if ( c > '9' || c < '0' )
                return false;
        }
        return true;
    }
 
    private static final int TWO_SUBTRACT = '0' * 11;
 
    private static int two( final String s, final int from )
    {
        return s.charAt( from ) * 10 + s.charAt( from + 1 ) - TWO_SUBTRACT;
    }
 
    private static int four( final String s )
    {
        return 100 * two( s, 0 ) + two( s, 2 );
    }
 
    private static DateParts parseDate( final String s )
    {
        return new DateParts( four( s ), two( s, 5 ) - 1, two( s, 8 ) );
    }
 
    public void fastDate( final String date )
    {
        checkDate(date);
        final DateParts dp = parseDate( date );
        checkDateParts( dp.year, dp.month, dp.day );
        m_cache.put( date, prepareDate(dp) );
    }
 
    private static void checkDateParts( final int year, final int month, final int day )
    {
        if ( year < 1800 || year > 2200 )
            throw new IllegalArgumentException( "Year must be between 1800 and 2200!" );
        if ( month < 0 || month > 11 )
            throw new IllegalArgumentException( "Month must be between 1 and 12!" );
        if ( month != 1 )
        {
            if ( day < 1 || day > MAX_DAY[ month ] )
                throw new IllegalArgumentException( "Day must be between 1 and " + MAX_DAY[ month ] + '!' );
        }
        else {
            int maxFeb = 28;
            if ( ( year & 3 ) == 0 )
                maxFeb = (year % 100 == 0) && (year % 400 != 0) ? 28 : 29;
            if ( day < 1 || day > maxFeb )
                throw new IllegalArgumentException( "Day must be between 1 and " + maxFeb + '!' );
        }
    }
 
    private static void checkTimeParts( final int hour, final int min, final int sec )
    {
        if ( hour < 0 || hour > 23 )
            throw new IllegalArgumentException( "Hours must be between 0 and 23!" );
        if ( min < 0 || min > 59 )
            throw new IllegalArgumentException( "Minutes must be between 0 and 59!" );
        if ( sec < 0 || sec > 59 )
            throw new IllegalArgumentException( "Seconds must be between 0 and 59!" );
    }
 
    private void checkDate( final String date )
    {
        if ( date == null || date.length() != 10 || !allDigits( date, DATE_DIGITS ) ||
                date.charAt( 4 ) != '.' || date.charAt( 7 ) != '.' )
            throw new IllegalArgumentException( "Date should be in yyyy.MM.dd format!" );
    }
 
    private void checkDateTime( final String dateTime )
    {
        if ( dateTime == null || dateTime.length() != 19 || !allDigits( dateTime, DATETIME_DIGITS ) ||
                dateTime.charAt( 4 ) != '.' || dateTime.charAt( 7 ) != '.' || dateTime.charAt( 10 ) != ' ' ||
                dateTime.charAt( 13 ) != ':' || dateTime.charAt( 16 ) != ':' )
            throw new IllegalArgumentException( "DateTime should be in yyyy.MM.dd HH:mm:ss format!" );
    }
 
    private long[] prepareDate( final DateParts dp )
    {
        final long[] res = new long[ 24 ];
        for ( int i = 0; i < 23; ++i )
        {
            final Calendar cl = new GregorianCalendar( dp.year, dp.month, dp.day, i, 0, 0 );
            cl.setTimeZone( m_timeZone );
            res[ i ] = cl.getTimeInMillis();
        }
        return res;
    }
 
    public long parse( final String dateTime )
    {
        checkDateTime( dateTime );
        final long[] cache = m_cache.get( dateTime.substring( 0, 10 ) );
        if ( cache != null )
        {
            final int hour = two( dateTime, 11 );
            final int min = two( dateTime, 14 );
            final int sec = two( dateTime, 17 );
            checkTimeParts( hour, min, sec );
            final long hourTime = cache[ hour ];
            return hourTime + min * 60000 + sec * 1000;
        }
        else {
            final int year = four( dateTime );
            final int month = two( dateTime, 5 ) - 1;
            final int day = two( dateTime, 8 );
            final int hour = two( dateTime, 11 );
            final int min = two( dateTime, 14 );
            final int sec = two( dateTime, 17 );
            checkDateParts( year, month, day );
            checkTimeParts( hour, min, sec );
            final Calendar cl = new GregorianCalendar( year, month, day, hour, min, sec );
            cl.setTimeZone( m_timeZone );
            return cl.getTimeInMillis();
        }
    }
}
private static class FastDateParser
{
    private static final int[] DATE_DIGITS = { 0, 1, 2, 3, 5, 6, 8, 9 };
    private static final int[] DATETIME_DIGITS = { 0, 1, 2, 3, 5, 6, 8, 9, 11, 12, 14, 15, 17, 18 };
    private static final int[] MAX_DAY = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

    private final Map<String, long[]> m_cache = new HashMap<String, long[]>( 20 );
    private final TimeZone m_timeZone;

    public FastDateParser(TimeZone m_timeZone) {
        this.m_timeZone = m_timeZone;
    }

    private static class DateParts
    {
        public final int year;
        public final int month;
        public final int day;

        public DateParts(int year, int month, int day) {
            this.year = year;
            this.month = month;
            this.day = day;
        }
    }

    private static boolean allDigits( final String s, final int[] positions )
    {
        for ( final int pos : positions )
        {
            final char c = s.charAt( pos );
            if ( c > '9' || c < '0' )
                return false;
        }
        return true;
    }

    private static final int TWO_SUBTRACT = '0' * 11;

    private static int two( final String s, final int from )
    {
        return s.charAt( from ) * 10 + s.charAt( from + 1 ) - TWO_SUBTRACT;
    }

    private static int four( final String s )
    {
        return 100 * two( s, 0 ) + two( s, 2 );
    }

    private static DateParts parseDate( final String s )
    {
        return new DateParts( four( s ), two( s, 5 ) - 1, two( s, 8 ) );
    }

    public void fastDate( final String date )
    {
        checkDate(date);
        final DateParts dp = parseDate( date );
        checkDateParts( dp.year, dp.month, dp.day );
        m_cache.put( date, prepareDate(dp) );
    }

    private static void checkDateParts( final int year, final int month, final int day )
    {
        if ( year < 1800 || year > 2200 )
            throw new IllegalArgumentException( "Year must be between 1800 and 2200!" );
        if ( month < 0 || month > 11 )
            throw new IllegalArgumentException( "Month must be between 1 and 12!" );
        if ( month != 1 )
        {
            if ( day < 1 || day > MAX_DAY[ month ] )
                throw new IllegalArgumentException( "Day must be between 1 and " + MAX_DAY[ month ] + '!' );
        }
        else {
            int maxFeb = 28;
            if ( ( year & 3 ) == 0 )
                maxFeb = (year % 100 == 0) && (year % 400 != 0) ? 28 : 29;
            if ( day < 1 || day > maxFeb )
                throw new IllegalArgumentException( "Day must be between 1 and " + maxFeb + '!' );
        }
    }

    private static void checkTimeParts( final int hour, final int min, final int sec )
    {
        if ( hour < 0 || hour > 23 )
            throw new IllegalArgumentException( "Hours must be between 0 and 23!" );
        if ( min < 0 || min > 59 )
            throw new IllegalArgumentException( "Minutes must be between 0 and 59!" );
        if ( sec < 0 || sec > 59 )
            throw new IllegalArgumentException( "Seconds must be between 0 and 59!" );
    }

    private void checkDate( final String date )
    {
        if ( date == null || date.length() != 10 || !allDigits( date, DATE_DIGITS ) ||
                date.charAt( 4 ) != '.' || date.charAt( 7 ) != '.' )
            throw new IllegalArgumentException( "Date should be in yyyy.MM.dd format!" );
    }

    private void checkDateTime( final String dateTime )
    {
        if ( dateTime == null || dateTime.length() != 19 || !allDigits( dateTime, DATETIME_DIGITS ) ||
                dateTime.charAt( 4 ) != '.' || dateTime.charAt( 7 ) != '.' || dateTime.charAt( 10 ) != ' ' ||
                dateTime.charAt( 13 ) != ':' || dateTime.charAt( 16 ) != ':' )
            throw new IllegalArgumentException( "DateTime should be in yyyy.MM.dd HH:mm:ss format!" );
    }

    private long[] prepareDate( final DateParts dp )
    {
        final long[] res = new long[ 24 ];
        for ( int i = 0; i < 23; ++i )
        {
            final Calendar cl = new GregorianCalendar( dp.year, dp.month, dp.day, i, 0, 0 );
            cl.setTimeZone( m_timeZone );
            res[ i ] = cl.getTimeInMillis();
        }
        return res;
    }

    public long parse( final String dateTime )
    {
        checkDateTime( dateTime );
        final long[] cache = m_cache.get( dateTime.substring( 0, 10 ) );
        if ( cache != null )
        {
            final int hour = two( dateTime, 11 );
            final int min = two( dateTime, 14 );
            final int sec = two( dateTime, 17 );
            checkTimeParts( hour, min, sec );
            final long hourTime = cache[ hour ];
            return hourTime + min * 60000 + sec * 1000;
        }
        else {
            final int year = four( dateTime );
            final int month = two( dateTime, 5 ) - 1;
            final int day = two( dateTime, 8 );
            final int hour = two( dateTime, 11 );
            final int min = two( dateTime, 14 );
            final int sec = two( dateTime, 17 );
            checkDateParts( year, month, day );
            checkTimeParts( hour, min, sec );
            final Calendar cl = new GregorianCalendar( year, month, day, hour, min, sec );
            cl.setTimeZone( m_timeZone );
            return cl.getTimeInMillis();
        }
    }
}

FastDateParser class works with one time zone at a time. In order to further improve parsing speed, it provides fastDate(String) method, which accepts a date in yyyy.MM.dd format. All dates provided to this method will be preprocessed: timestamp for zero minutes zero seconds of each hour will be calculated and stored. We need each hour because daylight savings time change happens on such times. After that we may use these timestamps for fast table lookups not involving creating a java.util.GregorianCalendar instances.

So, how long does it take to parse 10M dates with our parser? Again, two cases have to be tested: parsing a preprocessed date and parsing not a preprocessed date.

Preprocessed date, 10M calls Not a preprocessed date, 10M calls
0.78 sec 3.5 sec

Five times improvement over SimpleDateFormat for a not preprocessed date is a good improvement, but 20 times for a preprocessed date with the same amount of checks is even better. You can preprocess a few most probable dates (in many scenarios they are yesterday, today and tomorrow) in order to get improved performance in the average case.

See also

Joda Time library performance - an article comparing the performance of JDK date/time
related classes and matching Joda Time library classes.

Summary

Do not use java.util.Date unless you have to use it. Use an ordinary long instead.

java.util.Calendar is useful for all sorts of date calculations and i18n, but avoid either storing a lot of such objects or extensively creating them - they consume a lot of memory and expensive to create.

java.text.SimpleDateFormat is useful for general case datetime parsing, but it is better to avoid it if you have to parse a lot of similar dates. Implement a parser manually instead.


2 thoughts on “java.util.Date, java.util.Calendar and java.text.SimpleDateFormat performance

  1. Pingback: Joda Time library performance - Java Performance Tuning Guide

  2. Pingback: JSR 310 - Java 8 Date/Time library performance (as well as Joda Time 2.3 and j.u.Calendar)   | Java Performance Tuning Guide

Leave a Reply

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