29 de abr. de 2010

Instanciar ou não instanciar coleções

Intro

Em um papo de almoço, conversávamos sobre instanciar ou não instanciar uma coleção membro de alguma classe do domínio, exemplo:


public class Cliente {
    List enderecos; //null por default
}

ou

public class Cliente {
    List enderecos = new ArrayList();
}

E a questão fica entre performance e complexidade.


Performance

Performance por que quando um Cliente é instanciado, o seu atributo, enderecos, não é instanciado junto. No segundo caso, cada vez que em algum lugar em der um new Cliente() automaticamente está acontecendo um new ArrayList(), que constrói um Array interno de 10 posições (referências nulas claro).


Complexidade

Imagine que a classe Cliente seja um JavaBean, e a partir de uma classe cliente eu deseje iterar os endereços:

public class MinhaClasseDeNegocio {

    public Endereco enderecoDefault(Integer idCliente) {
        Cliente cliente = meuDataSource.find(idCliente);

        for (Endereco e : cliente.getEnderecos()) {
            if (e.isDefault()) return e;
        }

        return null;
    }

}

O que acontecerá se o Cliente não tiver endereços? Tchananam... NullPointerException

Então, se há a possibilidade de a lista vir vazia, eu tenho que lidar com isso, garantir, como abaixo:

public class MinhaClasseDeNegocio {

    public Endereco enderecoDefault(Integer idCliente) {
        Cliente cliente = meuDataSource.find(idCliente);

        if (cliente.getEndereco() != null) {
            for (Endereco e : cliente.getEnderecos()) {
                if (e.isDefault()) return e;
            }
        }
        return null;
    }

}


Obs.: Existe o modo Domain-Driven (o qual prefiro) de lidar com isso, mas fica para outro tópico.



Profiling

Para ilustrar a performance, fiz um pequeno benchmark, que pode ser visto abaixo:

import java.util.*;

public class ArrayProfile {
static final int ITENS = 1000000;

public static void main (String[] args) throws Exception {
long memoryBefore = Runtime.getRuntime().totalMemory();
long startTime = System.currentTimeMillis();
List lista = new ArrayList();
for (int i = 0; i < ITENS; i++) {
lista.add(new FaixaRepository());
}
long endTime = System.currentTimeMillis();

Thread.sleep(1000);
long memoryAfter = Runtime.getRuntime().totalMemory();
System.out.format("%nItens instanciados: %,d %n", ITENS);
System.out.format("%nMemória usada: %,d bytes %n", (memoryAfter - memoryBefore));
System.out.format("%nTempo necessário: %d ms %n%n",  (endTime - startTime));
}

}

class FaixaRepository {
List lista = new ArrayList();
}

class Faixa {
Integer codigo;
Double  remuneracao;
}


Neste primeiro caso, eu instancio o ArrayList, membro de FaixaRepository, e o resultado foi o seguinte:

mtorres@independiente:~/temp/sparring/collections$ java ArrayProfile 

Itens instanciados: 1.000.000 

Memória usada: 101.974.016 bytes 

Tempo necessário: 1359 ms 



Fazendo uma alteração, deixando o membro lista nulo, como abaixo:

...
class FaixaRepository {
List lista;
}
...

Obtive o seguinte resultado:

mtorres@independiente:~/temp/sparring/collections$ java ArrayProfile 

Itens instanciados: 1.000.000 

Memória usada: 9.437.184 bytes 

Tempo necessário: 111 ms 



Conclusão

Este é outro caso em que não se está definido o que é certo ou errado, cada caso é um caso, faz parte do design decidir qual estratégia será utilizada.



Obs: O tempo que eu uso para fazer estes micro artigos é a espera do build e do start dos servidores, eu instrumentei, hehehe, abaixo:

mtorres@independiente:~/desenvolvimento/bla/bla/bla$ time mvn install -Plocal
======= Construindo ... bla bla bla =======
... muitas linhas depois ....

real 0m48.702s
user 0m35.430s
sys 0m2.904s

mtorres@independiente:~/desenvolvimento/bla/bla/bla$ $ time mvn jetty:run -Plocal
... bla bla bla ...
real 0m28.685s
user 0m29.782s
sys 0m0.844s

mtorres@independiente:~/desenvolvimento/bla/bla/bla$ $ time mvn jetty:run -Plocal
... bla bla bla ...
real 0m12.671s
user 0m12.369s
sys 0m0.300s

Nenhum comentário: