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
1