Criando e Destruindo Objetos
Este artigo trata sobre criação e
destruição de objetos: quando
e como criá-los ou evitar criá-los, como garantir que
são destruídos no tempo correto e como gerenciar qualquer
ação de cleanup que
precisa preceder a destruição de um objeto.
Item 1: Considere a
utilização de static factory methods ao invés de
construtores
A forma mais comum de permitir que clientes obtenham uma
instância de sua classe é através de construtores
públicos. Uma outra maneira possível é
através de um static factory
method público, que nada mais é do que um
método estático que retorna uma instância da
classe. Ex.:
public static Boolean valueOf(boolean b) {
return (b ? Boolean.TRUE : Boolean.FALSE);
}
Uma das vantagens de um static factory method é que
eles possuem nomes. Em casos em que temos construtores para diversas
categorias diferentes de objetos, às vezes recebendo até
os mesmos tipos em seus parâmetros (diferenciando apenas pela
ordem já que a assinatura do construtor deve ser diferente),
é aconselhável substituir um ou mais desses construtores
por métodos factory com nomes mais sugestivos para indicar as
diferenças.
Uma segunda vantagem dos static factory methods sobre os
construtores é eles não precisarem criar um novo objeto
cada vez que são chamados, permitindo um maior controle das
instâncias criadas ou até mesmo fazer um cache de objetos.
Assim é possível, por exemplo, garantir que duas
instâncias são iguais - a.equals(b) - se e
somente se a == b, e
então os clientes podem utilizar esta segunda forma para testar
igualdade, ganhando performance.
Uma terceira vantagem dos static factory methods é
eles poderem retornar um objeto de qualquer subtipo do seu tipo de
retorno, aumentando a flexibilidade. Desta forma, estas subclasses
podem
não constar na API, tornando-a mais compacta e simples de
entender. Esta técnica é utilizada em frameworks baseadas
em interfaces, que são tipos de retorno naturais para estes
métodos, e ainda garante que o cliente também
estará
utilizando as interfaces, o que é outra boa prática.
É até possível criar soluções mais
elaboradas, como a encontrada na JCE (Java Cryptography Extension), em
que as classes são instanciadas de acordo com o parâmetro
passado para o método.
A principal desvantagem do uso de static factory methods substituindo
os construtores é que
não é possível herdar uma classe sem que ela tenha
um
construtor visível externamente. Porém isto pode ser
visto como uma certa vantagem, uma vez que favorece
composição ao invés de herança.
Uma segunda desvantagem é que eles não são
facilmente distinguidos de outros métodos estáticos,
ficando mais difícil entender na documentação da
API como instanciar uma classe que utiliza static factory methods ao
invés de construtores. Algumas convenções
estão sendo criadas para endereçar esse problema, sendo
que dois nomes são comuns para esses métodos: valueOf (utilizado
normalmente para conversões de tipo) e getInstance.
Portanto, o importante é analisar o código para verificar
se seria apropriado o uso de static factory methods. Caso seja
indiferente, ainda é melhor utilizar construtores, por eles
serem o padrão.
Item 2: Garanta uma propriedade singleton
com um construtor privado
Um singleton é uma classe que pode ser instanciada uma
única vez, como explicado no livro de Design Patterns da GoF.
Normalmente representa um componente que é
intrinsicamente
único, como uma placa de vídeo ou o sistema de arquivos.
A forma de implementar um singleton é mantendo o construtor
privado e provendo um membro estático público para
retornar a instância, que pode ser diretamente um campo final da
classe ou então um método que retorne essa
instância. Ex.:
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() {
// construtor da classe
}
public static Elvis getInstance() {
return INSTANCE;
}
}
Para que
o singleton seja serializável,
também é necessário incluir um método
readResolve que descarte
a nova instância criada na
desserialização, retornando aquela já existente.
Item 3: Garanta a
não-instancialização com um construtor privado
Às vezes é prático criar uma classe apenas com
métodos estáticos que podem ser utilizados para executar
tarefas que ajudam outras classes, tal como acontece na java.lang.Math. Essas
classes não foram criadas para serem instanciadas, mas se
não for colocado nenhum construtor o compilador automaticamente
criará um, permitindo que sejam criados os objetos. Para evitar
isso, inclua um construtor privado. Note que simplesmente declarar a
classe abstrata não funcionaria, pois daria a impressão
de que ela foi criada para ser estendida e as subclasses seriam
instanciáveis. Como dito anteriormente, se a classe possuir
apenas um construtor privado, não será
possível estendê-la.
Item 4: Evite criar objetos duplicados
Reutilizar um objeto é mais rápido e elegante do que
criar um novo objeto toda vez. Um objeto poderá sempre ser
reutilizado caso seja imutável. Um exemplo do que evitar:
String s = new String("evite isso");
O código acima cria uma nova instância a cada vez que
é executado, sendo que seu argumento já é em si
mesmo uma
instância de String
funcionalmente idêntica a qualquer objeto criado pelo construtor.
Portanto, o correto seria fazer simplesmente o seguinte:
String s = "faça assim";
Desta
forma, só é criado um novo objeto se ainda não
existir um equivalente.
Para outros casos, podem ser utilizados static factory methods ao
invés de construtores para atingir este objetivo, como no
exemplo da classe Boolean acima, cujo
método valueOf retorna um
objeto já existente.
Outra situação seria quando dentro de um método
que é chamado várias vezes você cria objetos como o
Calendar ou TimeZone, mas na verdade seus valores são fixos
durante o ciclo de vida do programa e bastaria que fossem criados
quando é inicializada a classe (em um bloco static) ou quando
são usados pela primeira vez.
Porém, essa dica não deve ser levada ao extremo. Objetos
pequenos cujo construtor possui pouco processamento não
representam muito overhead de performance, então não
há problema em criá-los se isso irá melhorar a
clareza ou simplicidade do código. Já para casos como a
conexão em um banco de dados, cuja criação tem um
custo elevado, compensa até utilizar mecanismos como um pool de
objetos que
são reutilizados.
É importante ressaltar também que é melhor criar
um novo objeto, mesmo sem necessidade, do que reutilizar um objeto
quando
deveria ser criado um novo, pois isso poderá levar a
sérios bugs no programa.
Item 5: Elimine referências a
objetos obsoletas
Apesar do Java cuidar da maior parte do gerenciamento de memória
através do garbage collector, não podemos esquecer
totalmente do assunto enquanto programamos.
É importante estar sempre atento para verificar se uma
referência a um objeto que não é mais utilizado
está sendo retida, impedindo que ele (e aqueles referenciados
por ele) sejam recolhidos pelo garbage collector. Isso é mais
freqüente em casos em que a classe propõe-se a gerenciar
sua
própria memória, como no exemplo abaixo:
public class Stack {
private Object[] elements;
private int size = 0;
public Stack(int initialCapacity) {
this.elements = new
Object[initialCapacity];
}
public void push(Object e) {
elements[size++] = e;
}
public Object pop() {
return elements[--size];
}
}
No código acima, são inseridos e retornados elementos de
uma array. Pela implementação é possível
observar que uma vez que um elemento é retornado da array, ele
não é mais utilizado, porém sua referência
continua lá e o Java não sabe que ele não
será mais utilizado. Uma maneira melhor de implementar o
método pop seria:
public Object pop() {
Object result = elements[--size];
elements[size] = null;
return result;
}
Essa
técnica também ajuda na detecção de erros,
uma vez que se tentarem reutilizar a referência por engano
acontecerá logo uma NullPointerException ao
invés de um comportamento imprevisível.
Porém, novamente, não é necessário
radicalizar e colocar todas as referências como null assim que
terminar de utilizar o objeto. O melhor é reutilizar a
variável ou deixá-la sair de escopo. Isso costuma ocorrer
naturalmente quando você a declara no menor escopo
possível. Note que não basta sair do bloco em que a
variável está definida, e sim do método que o
contém.
São em casos como o acima que é mais comum precisar estar
atento em relação à necessidade de colocar o null
explicitamente. Outro caso comum de vazamento de memória
é o de caches. Em algumas situações é
possível controlar o ciclo de vida do cache de acordo com o
escopo de variáveis que referenciam a chave, e então o
problema é solucionável com um WeakHashMap. Mas na
maioria dos casos é necessário recorrer a uma thread que
faça a limpeza dos objetos já não utilizados
regularmente ou então remover os registros mais antigos conforme
são adicionados novos, por exemplo utilizando o método removeEldestEntry da classe java.util.LinkedHashMap.
Os vazamentos de memória não costumam ser facilmente
identificados, sendo necessário verificar detalhadamente o
código ou então utilizar ferramentas conhecidas como heap profilers. Por isso é
preferível um bom planejamento antecipadamente da classe do que
deixar para diagnosticar os problemas depois.
Item 6: Evite finalizadores
Considerando que a especificação da linguagem Java
não garante a execução do método finalize, e muito
menos o tempo em que ela irá ocorrer, é importante
não depender da execução deste método.
É melhor criar um método que deva ser explicitamente
chamado pelo cliente quando o objeto não estiver mais sendo
usado, como o close() da classe InputStream. O finalize deve ser
implementado apenas como uma garantia a mais para a
liberação de recursos, não como a principal
solução.
Outro motivo para evitar este método é que se ocorrer
qualquer exceção dentro do mesmo ela é
simplesmente ignorada, podendo deixar outros objetos em estado
corrompido sem nem ao menos avisar.
Um uso legítimo de finalizadores é no caso de
métodos nativos delegando tarefas a objetos nativos, pois estes
estão fora do escopo do garbage collector e por isso não
são recolhidos automaticamente por ele. Porém, se o
objeto retém recursos críticos, é
necessário
ter um método explícito para liberá-los.
Outro ponto importante é que, diferentemente dos construtores,
não existe uma chamada automática do método finalize da superclasse
de uma classe. Portanto, ao fazer override deste método, o
correto é finalizar a subclasse em um bloco try e chamar o super.finalize() no bloco finally, para
garantindo
que ambos serão chamados.
Uma classe pode evitar que seu finalizador não seja chamado em
decorrência de uma incorreta implementação do
finalizador da subclasse. Isso é conseguido ao custo de criar um
objeto a mais para cada
instância. Ao invés de colocar o código no
finalizador da própria classe, é criada uma classe
anônima com um
finalizador contendo o código necessário. Ex.:
public class Foo {
private final Object
finalizerGuardian = new Object() {
protected void
finalize() throws Throwable {
// Finaliza o objeto Foo
}
};
... // restante da classe Foo
}
Bibliografia:
Bloch, Joshua. Effective Java.
Resumo por: Vanessa Sabino