Trabalhar com datas é muito comum

Figura 1: Trabalhar com datas é muito comum

Introdução

Para realizar operações de formatação ou parse de datas, muitos programadores Java usam a conhecida classe SimpleDateFormat, do pacote java.text. Ela é uma alternativa natural, pois faz parte da API padrão há muito tempo e é relativamente simples de usar. Porém, o que poucos sabem é que ela pode não ser a melhor opção em termos de performance para certos cenários.

Como alternativa ao SimpleDateFormat, iremos analisar o FastDateFormat da Apache e o DateTimeFormatter do JodaTime.

SimpleDateFormat:

  • Faz parte da API padrão. (Será utilizado o Java 6).

FastDateFormat:

DateTimeFormatter:

Os testes serão realizados sobre as seguintes configurações:

  • Windows XP;
  • Intel Core 2 Duo CPU - 2.80 Ghz;
  • 3,49 G Ram;
  • Netbeans 7.0.1;
  • Java 1.6.
  • Listagem 1: Classe de Teste para formatação com criação de objeto no loop

    
    package br.com.devmedia.benchmark;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import org.apache.commons.lang3.time.FastDateFormat;
    import org.joda.time.DateTime;
    import org.joda.time.format.DateTimeFormat;
    
    public class TestePerformanceFormat1 {
    
        private static final int OPERACOES = 1000;
        private static final int EXECUCOES = 1000;
        private static final String PATTERN = "yyyy/MM/dd";
    
        public static void main(String... args) {
            double t1, t2, t3;
            int i;
    
            final Date now = new Date();
            final DateTime nowJoda = new DateTime();
    
            t1 = t2 = t3 = 0.0;
            for (i = 0; i < EXECUCOES; i++) {
                t1 += testSimpleDateFormat(now);
                t2 += testFastDateFormat(now);
                t3 += testDateTimeFormat(nowJoda);
            }
    
            System.out.println("SimpleDateFormat: Tempo médio decorrido = " + (t1 / EXECUCOES) + " milisegundos ");
            System.out.println("FastDateFormat Tempo médio decorrido = " + (t2 / EXECUCOES) + " milisegundos ");
            System.out.println("DateTimeFormatter Tempo médio decorrido = " + (t3 / EXECUCOES) + " milisegundos ");
        }
    
        public static long testSimpleDateFormat(final Date now) {
            String result = null;
            int i;
            long t1 = System.currentTimeMillis();
            for (i = 0; i < OPERACOES; i++) {
                result = new SimpleDateFormat(PATTERN).format(now);
            }
            long t2 = System.currentTimeMillis();
            // Print nao entra no contagem de tempo
            System.out.println("result testSimpleDateFormat = " + result);
            return t2 - t1;
        }
    
        public static long testFastDateFormat(final Date now) {
            String result = null;
            int i;
            long t1 = System.currentTimeMillis();
            for (i = 0; i < OPERACOES; i++) {
                result = FastDateFormat.getInstance(PATTERN).format(now);
            }
            long t2 = System.currentTimeMillis();
            // Print nao entra no contagem de tempo
            System.out.println("result testFastDateFormat = " + result);
            return t2 - t1;
        }
    
        public static long testDateTimeFormat(final DateTime now) {
            String result = null;
            int i;
            long t1 = System.currentTimeMillis();
            for (i = 0; i < OPERACOES; i++) {
                result = DateTimeFormat.forPattern(PATTERN).print(now);
            }
            long t2 = System.currentTimeMillis();
            // Print nao entra no contagem de tempo
            System.out.println("result testDateTimeFormat = " + result);
            return t2 - t1;
        }
    }
    

    Ao rodar o teste, temos os seguintes tempos médios:

    Saída:

    • result testSimpleDateFormat = 2012/11/25
    • result testFastDateFormat = 2012/11/25
    • result testDateTimeFormat = 2012/11/25

    1ª Execução:

    • SimpleDateFormat: Tempo médio decorrido = 6.63 milisegundos
    • FastDateFormat Tempo médio decorrido = 2.386 milisegundos
    • DateTimeFormatter Tempo médio decorrido = 0.765 milisegundos

    2ª Execução:

    • SimpleDateFormat: Tempo médio decorrido = 6.623 milisegundos
    • FastDateFormat Tempo médio decorrido = 2.165 milisegundos
    • DateTimeFormatter Tempo médio decorrido = 0.884 milisegundos

    No código-fonte da listagem 1, esta sendo cronometrado o tempo de criação dos objetos e a operação de formatação, que transforma o objeto Date numa String. (Observe que o JodaTime não trabalha com o objeto Date, e sim com DateTime).

    Nota-se que o tempo médio de processamento nesse cenário é favorável ao DateTimeFormatter. Utilizando o JProfile, verificou também que o consumo de memória é bem menor no método que utiliza DateTimeFormatter do que nas outras duas abordagens. (Ou seja, a API do Joda Time cria menos objetos internos do que as outras alternativas).

    Liistagem 2: Classe de Teste para formatação sem criação de objeto no loop

    
    package br.com.devmedia.benchmark;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import org.apache.commons.lang3.time.FastDateFormat;
    import org.joda.time.DateTime;
    import org.joda.time.format.DateTimeFormat;
    import org.joda.time.format.DateTimeFormatter;
    
    public class TestePerformanceFormat2 {
        
        private static final int OPERACOES = 1000;
        private static final int EXECUCOES = 1000;    
        private static final String PATTERN = "yyyy/MM/dd";
        
        public static void main(String... args) {        
            double t1, t2, t3;
            int i;
            
            final Date now = new Date();
            final DateTime nowJoda = new DateTime();        
            
            final SimpleDateFormat sdf = new SimpleDateFormat(PATTERN);
            final FastDateFormat fdf = FastDateFormat.getInstance(PATTERN);
            final DateTimeFormatter dtf = DateTimeFormat.forPattern(PATTERN);
            
            t1 = t2 = t3 = 0;
            for(i = 0; i < EXECUCOES; i++) {        
                t1 += testSimpleDateFormat(sdf, now);        
                t2 += testFastDateFormat(fdf, now);
                t3 += testDateTimeFormat(dtf, nowJoda);            
            }
            
            System.out.println("SimpleDateFormat: Tempo médio decorrido = " + (t1 / EXECUCOES) + " milisegundos ");
            System.out.println("FastDateFormat Tempo médio decorrido = " + (t2 / EXECUCOES) + " milisegundos ");
            System.out.println("DateTimeFormatter Tempo médio decorrido = " + (t3 / EXECUCOES) + " milisegundos ");
        }    
        
        public static long testSimpleDateFormat(final SimpleDateFormat sdf, final Date now) {
            String result = null;
            long t1 = System.currentTimeMillis();        
            for (int i = 0; i < OPERACOES; i++) {
                result = sdf.format(now);
            }
            long t2 = System.currentTimeMillis();  
            // Print nao entra no contagem de tempo
            System.out.println("result testSimpleDateFormat = " + result);
            return t2 - t1;
        }
        
        public static long testFastDateFormat(final FastDateFormat fdf, final Date now) {
            String result = null;
            long t1 = System.currentTimeMillis();        
            for (int i = 0; i < OPERACOES; i++) {
                result = fdf.format(now);
            }
            long t2 = System.currentTimeMillis();
            // Print nao entra no contagem de tempo
            System.out.println("result testFastDateFormat = " + result);
            return t2 - t1;
        }    
        
        public static long testDateTimeFormat(final DateTimeFormatter dtf, final DateTime now) {
            String result = null;
            long t1 = System.currentTimeMillis();        
            for (int i = 0; i < OPERACOES; i++) {
                result = dtf.print(now);
            }
            long t2 = System.currentTimeMillis();
            // Print nao entra no contagem de tempo
            System.out.println("result testDateTimeFormat = " + result);
            return t2 - t1;
        }        
    }

    Saída:

    • result testSimpleDateFormat = 2012/11/25
    • result testFastDateFormat = 2012/11/25
    • result testDateTimeFormat = 2012/11/25

    1ª Execução:

    • SimpleDateFormat: Tempo médio decorrido = 0.731 milisegundos
    • FastDateFormat Tempo médio decorrido = 2.064 milisegundos
    • DateTimeFormatter Tempo médio decorrido = 0.878 milisegundos

    2ª Execução:

    • SimpleDateFormat: Tempo médio decorrido = 0.871 milisegundos
    • FastDateFormat Tempo médio decorrido = 1.673 milisegundos
    • DateTimeFormatter Tempo médio decorrido = 1.143 milisegundos

    Quando os objetos são criados fora dos loops, verifica-se que o SimpleDateFormat teve um desempenho melhor. Houve também um menor consumo de memória no método que usa SimpleDateFormat do que o DateTimeFormatter (JProfile). Já o FastDateFormat, além de ser o mais lento foi o que consumiu maior quantidade de memória.

    Listagem 3: Classe de Teste para operações de parse com criação de objeto no loop

    
    package br.com.devmedia.benchmark;
    
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import org.joda.time.DateTime;
    import org.joda.time.format.DateTimeFormat;
    
    public class TestePerformanceParse1 {
    
        private static final int OPERACOES = 1000;
        private static final int EXECUCOES = 1000;
        private static final String PATTERN = "yyyy/MM/dd";
        private static String date = "2010/10/10";
    
        public static void main(String... args) throws ParseException {
            double t1, t2;
            int i;
    
            t1 = t2 = 0;
            for (i = 0; i < EXECUCOES; i++) {
                t1 += testSimpleDateFormat(date);
                t2 += testDateTimeFormat(date);
            }
    
            System.out.println("SimpleDateFormat: Tempo médio decorrido = " + (t1 / EXECUCOES) + " milisegundos ");
            System.out.println("DateTimeFormatter Tempo médio decorrido = " + (t2 / EXECUCOES) + " milisegundos ");
        }
    
        public static long testSimpleDateFormat(final String strDate) throws ParseException {
            Date result = null;
            int i;
            long t1 = System.currentTimeMillis();
            for (i = 0; i < OPERACOES; i++) {
                result = new SimpleDateFormat(PATTERN).parse(strDate);
            }
            long t2 = System.currentTimeMillis();
            // Print nao entra no contagem de tempo
            System.out.println("result testSimpleDateFormat = " + result);
            return t2 - t1;
        }
    
        public static long testDateTimeFormat(final String strDate) {
            DateTime result = null;
            int i;
            long t1 = System.currentTimeMillis();
            for (i = 0; i < OPERACOES; i++) {
                result = DateTimeFormat.forPattern(PATTERN).parseDateTime(strDate);
            }
            long t2 = System.currentTimeMillis();
            // Print nao entra no contagem de tempo
            System.out.println("result testDateTimeFormat = " + result);
            return t2 - t1;
        }
    }
    

    Saída:

    • result testSimpleDateFormat = Sun Oct 10 00:00:00 BRT 2010
    • result testDateTimeFormat = 2010-10-10T00:00:00.000-03:00

    1ª Execução:

    • SimpleDateFormat: Tempo médio decorrido = 9.107 milisegundos
    • DateTimeFormatter Tempo médio decorrido = 1.939 milisegundos

    2ª Execução:

    • SimpleDateFormat: Tempo médio decorrido = 8.867 milisegundos
    • DateTimeFormatter Tempo médio decorrido = 1.993 milisegundos

    Assim como na listagem 1, quando os objetos são criados dentro do loop, a API do JodaTime é novamente superior em termos de desempenho (e uso de memória).

    Nota: O FastDateFormat não foi utilizado, pois ele não possue funções de parse semelhantes ao SimpleDateFormat e DateTimeFormatter. FastDateFormat só pode ser utilizado para formatação.

    Listagem 4: Classe de Teste para operações de parse sem criação de objeto no loop

    
    package br.com.devmedia.benchmark;
    
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import org.joda.time.DateTime;
    import org.joda.time.format.DateTimeFormat;
    import org.joda.time.format.DateTimeFormatter;
    
    
    public class TestePerformanceParse2 {
    
        private static final int OPERACOES = 1000;
        private static final int EXECUCOES = 1000;
        private static final String PATTERN = "yyyy/MM/dd";
        private static String date = "2010/10/10";
    
        public static void main(String... args) throws ParseException {
            double t1, t2;
            int i;
    
            final SimpleDateFormat sdf = new SimpleDateFormat(PATTERN);
            final DateTimeFormatter dtf = DateTimeFormat.forPattern(PATTERN);
    
            t1 = t2 = 0;
            for (i = 0; i < EXECUCOES; i++) {
                t1 += testSimpleDateFormat(sdf, date);
                t2 += testDateTimeFormat(dtf, date);
            }
    
            System.out.println("SimpleDateFormat: Tempo médio decorrido = " + (t1 / EXECUCOES) + " milisegundos ");
            System.out.println("DateTimeFormatter Tempo médio decorrido = " + (t2 / EXECUCOES) + " milisegundos ");
        }
    
        public static long testSimpleDateFormat(final SimpleDateFormat sdf, final String strDate) throws ParseException {
            Date result = null;
            int i;
            long t1 = System.currentTimeMillis();
            for (i = 0; i < OPERACOES; i++) {
                result = sdf.parse(strDate);
            }
            long t2 = System.currentTimeMillis();
            // Print nao entra no contagem de tempo
            System.out.println("result testSimpleDateFormat = " + result);
            return t2 - t1;
        }
    
        public static long testDateTimeFormat(final DateTimeFormatter dtf, final String strDate) {
            DateTime result = null;
            int i;
            long t1 = System.currentTimeMillis();
            for (i = 0; i < OPERACOES; i++) {
                result = dtf.parseDateTime(strDate);
            }
            long t2 = System.currentTimeMillis();
            // Print nao entra no contagem de tempo
            System.out.println("result testDateTimeFormat = " + result);
            return t2 - t1;
        }
    }
    

    Saída:

    • result testSimpleDateFormat = Sun Oct 10 00:00:00 BRT 2010
    • result testDateTimeFormat = 2010-10-10T00:00:00.000-03:00

    1ª Execução:

    • SimpleDateFormat: Tempo médio decorrido = 2.486 milisegundos
    • DateTimeFormatter Tempo médio decorrido = 1.95 milisegundos

    2ª Execução:

    • SimpleDateFormat: Tempo médio decorrido = 2.477 milisegundos
    • DateTimeFormatter Tempo médio decorrido = 2.148 milisegundos

    3ª Execução:

    • SimpleDateFormat: Tempo médio decorrido = 2.702 milisegundos
    • DateTimeFormatter Tempo médio decorrido = 1.922 milisegundos

    4ª Execução:

    • SimpleDateFormat: Tempo médio decorrido = 2.544 milisegundos
    • DateTimeFormatter Tempo médio decorrido = 2.002 milisegundos

    Esse foi o cenário mais equilibrado. Pode-se dizer que as duas abordagens são semelhantes em desempenho. Já em relação ao consumo de memória medida pelo JProfile, o DateTimeFormatter é mais eficiente do que o SimpleDateFormat.

    SimpleDateFormat não é Thread-Safe!

    O grande calcanhar de aquiles do SimpleDateFormat é que ele não é thread-safe. Isso significa que ele não pode ser compartilhado por múltiplas threads, o que força os desenvolvedores a instanciá-lo de forma local sempre que for necessário utilizá-lo (as listagens 1 e 3 deixam claro o custo de instanciação). Devemos, portanto, ficar atentos ao usar o SimpleDateFormat em ambiente web, ou aplicações que façam uso de mais de uma thread.

    A grande vantagem do DateTimeFormatter e do FastDateFormat é que ambos foram concebidos para serem classes imutáveis, e portanto, thread-safe. Isso garante que eles possam ser compartilhados em ambientes de múltiplas threads e podem inclusive serem injetados via Spring no modo Singleton, onde uma única instância pode ser compartilhada por vários outros beans do sistema, reduzindo o uso de memória.

    De fato, em ambientes concorrentes, pode ser mais interessante usar o DateTimeFormatter do Joda Time, pois ele tem bom desempenho e consumo de memória. O FastDateFormat é limitado a situações onde apenas seja necessário operações de formatação.

    A abordagem da imutabilidade é importante, pois mesmo que as classes que você desenvolva agora não sejam compartilhadas por várias threads, ninguém pode garantir que no futuro elas não sejam. Então, utilizar classes imutáveis no seu desenvolvimento pode ser uma questão interessante a ser levada em consideração.

    Com isso finaliza-se a comparação entre essas três alternativas. Espero que isso tenha auxiliado o leitor a considerar outras opções ao SimpleDateFormat.

    Obrigado e até a próxima!

    Referências: