Boas práticas de “Effective Java (3rd Edition)”

Trechos de “Effective Java (3rd Edition) ”, de Joshua Bloch

1. Retorne coleções ou arrays vazios, não null

Se o método retorna um Set, não faça return null; para um resultado vazio – use return Set.of();.

2. Considere métodos de fábrica estáticos em vez de construtores

public static Boolean valueOf(boolean b) {
    return b ? Boolean.TRUE : Boolean.FALSE;
}

Nem sempre são melhores, mas em alguns casos de uso oferecem inúmeras vantagens:

Desvantagens:

3. Considere um builder quando houver muitos parâmetros no construtor

Fábricas estáticas e construtores têm uma limitação em comum: não escalam bem para um grande número de parâmetros opcionais. Isso pegou em cheio alguns projetos em que trabalhei.

4. Garanta a propriedade de singleton com um construtor privado ou um tipo enum

Tudo o que não precisa de várias instâncias deve ser instanciado uma única vez – seja com @ApplicationScoped ou com outro padrão de singleton.

5. Impeça a instanciação com um construtor privado

De vez em quando, você vai querer escrever uma classe que é só um agrupamento de métodos e campos estáticos. Essas classes ganharam má fama porque algumas pessoas abusam delas para não ter de pensar em termos de objetos, mas elas têm usos legítimos.

private MyStaticClass() {
    // private constructor to prevent instantiation
}

6. Evite criar objetos desnecessários

String s = new String("bikini");  // DON'T DO THIS!

Ou:

// Hideously slow! Can you spot the object creation?
private static long sum() {
    Long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++)
        sum += i;
    return sum;
}

Solução: este programa chega à resposta certa, mas é muito mais lento do que deveria, por causa de um erro de digitação de um único caractere. A variável sum é declarada como Long em vez de long, o que significa que o programa cria cerca de 2^31 instâncias desnecessárias de Long (aproximadamente uma a cada vez que o long i é somado ao Long sum).

7. Evite finalizers e cleaners

Finalizers são imprevisíveis, muitas vezes perigosos e, em geral, desnecessários. Usá-los pode causar comportamento errático, baixa performance e problemas de portabilidade.

8. Prefira try-with-resources a try-finally

// try-with-resources - the best way to close resources!
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
    return br.readLine();
}

9. Considere implementar Comparable

Ao implementar Comparable, você permite que sua classe funcione com todos os muitos algoritmos genéricos e implementações de coleções que dependem dessa interface.

10. Minimize a acessibilidade de classes e membros

O fator mais importante que diferencia um componente bem projetado de um mal projetado é o quanto o componente esconde seus dados internos e outros detalhes de implementação dos demais componentes. Um componente bem projetado esconde todos os seus detalhes de implementação, separando de forma limpa sua API da sua implementação. Os componentes então se comunicam apenas por meio de suas APIs e ignoram o funcionamento interno uns dos outros. Esse conceito, conhecido como ocultação de informação ou encapsulamento, é um princípio fundamental do design de software.

11. Em classes públicas, use métodos de acesso, não campos públicos

De vez em quando, você pode ficar tentado a escrever classes degeneradas, que não servem para nada além de agrupar campos de instância:

// Degenerate classes like this should not be public!
class Point {
    public double x;
    public double y;
}

Como os campos de dados dessas classes são acessados diretamente, elas não oferecem os benefícios do encapsulamento. Você não consegue mudar a representação sem mudar a API, não consegue garantir invariantes e não consegue executar ações auxiliares quando um campo é acessado. Programadores orientados a objetos mais radicais acham essas classes abomináveis e defendem que elas sejam sempre substituídas por classes com campos privados e métodos de acesso públicos (getters) e, no caso de classes mutáveis, métodos modificadores (setters):

// Encapsulation of data by accessor methods and mutators
class Point {

    private double x;
    private double y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() { return x; }
    public double getY() { return y; }

    public void setX(double x) { this.x = x; }
    public void setY(double y) { this.y = y; }
}

Sem dúvida, os radicais têm razão quando se trata de classes públicas: se uma classe é acessível fora do seu pacote, ofereça métodos de acesso para preservar a flexibilidade de mudar a representação interna da classe. Se uma classe pública expõe seus campos de dados, toda esperança de mudar sua representação está perdida, porque o código cliente pode estar espalhado por toda parte.

Porém, se uma classe é package-private ou é uma classe aninhada privada, não há nada de intrinsecamente errado em expor seus campos de dados – desde que eles descrevam adequadamente a abstração oferecida pela classe.

12. Minimize a mutabilidade

Uma classe imutável é simplesmente uma classe cujas instâncias não podem ser modificadas. Todas as informações contidas em cada instância são fixas durante toda a vida do objeto, de modo que nenhuma mudança jamais pode ser observada. As bibliotecas da plataforma Java contêm muitas classes imutáveis, entre elas String, as classes de primitivos encapsulados (boxed), BigInteger e BigDecimal. Há muitos bons motivos para isso: classes imutáveis são mais fáceis de projetar, implementar e usar do que classes mutáveis. Elas são menos propensas a erros e mais seguras. Para tornar uma classe imutável, siga estas cinco regras:

  1. Não ofereça métodos que modifiquem o estado do objeto (conhecidos como mutators, ou modificadores).
  2. Garanta que a classe não possa ser estendida. Isso impede que subclasses descuidadas ou mal-intencionadas comprometam o comportamento imutável da classe agindo como se o estado do objeto tivesse mudado. Em geral, impede-se a criação de subclasses tornando a classe final, por exemplo.
  3. Torne todos os campos final. Isso expressa claramente a sua intenção de uma forma que o próprio sistema garante. Além disso, é necessário para assegurar o comportamento correto caso a referência a uma instância recém-criada seja passada de uma thread para outra sem sincronização, como descrito no modelo de memória.
  4. Torne todos os campos privados. Isso impede que os clientes obtenham acesso a objetos mutáveis referenciados pelos campos e os modifiquem diretamente. Embora seja tecnicamente permitido que classes imutáveis tenham campos public final contendo valores primitivos ou referências a objetos imutáveis, isso não é recomendado, porque impede mudar a representação interna em uma versão futura.
  5. Garanta acesso exclusivo a quaisquer componentes mutáveis. Se a sua classe tiver campos que referenciam objetos mutáveis, garanta que os clientes da classe não consigam obter referências a esses objetos. Nunca inicialize um campo desses com uma referência a objeto fornecida pelo cliente, nem o retorne a partir de um método de acesso. Faça cópias defensivas nos construtores, nos métodos de acesso e nos métodos readObject.

13. Prefira composição a herança

A herança é uma forma poderosa de reutilizar código, mas nem sempre é a melhor ferramenta para o trabalho. Usada de forma inadequada, ela leva a software frágil. É seguro usar herança dentro de um pacote, onde as implementações da subclasse e da superclasse estão sob o controle dos mesmos programadores. Também é seguro usar herança ao estender classes projetadas e documentadas especificamente para extensão. Já herdar de classes concretas comuns além dos limites do pacote é perigoso.

Ao contrário da invocação de métodos, a herança viola o encapsulamento. Você pode achar que é seguro estender uma classe se apenas adicionar novos métodos e evitar sobrescrever os existentes. Embora esse tipo de extensão seja bem mais seguro, ele não está livre de riscos. Se a superclasse ganhar um novo método em uma versão posterior e você tiver o azar de ter dado à subclasse um método com a mesma assinatura e um tipo de retorno diferente, sua subclasse deixará de compilar. Se você deu à subclasse um método com a mesma assinatura e o mesmo tipo de retorno do novo método da superclasse, agora você o está sobrescrevendo e fica sujeito aos problemas descritos antes. Além disso, é duvidoso que o seu método cumpra o contrato do novo método da superclasse, já que esse contrato ainda não tinha sido escrito quando você escreveu o método da subclasse.

Felizmente, existe uma forma de evitar todos os problemas descritos acima. Em vez de estender uma classe existente, dê à sua nova classe um campo privado que referencia uma instância da classe existente. Esse design se chama composição, porque a classe existente se torna um componente da nova. Cada método de instância da nova classe invoca o método correspondente na instância contida da classe existente e retorna os resultados. Isso se chama encaminhamento (forwarding), e os métodos da nova classe são chamados de métodos de encaminhamento. A classe resultante será sólida como uma rocha, sem nenhuma dependência dos detalhes de implementação da classe existente. Nem mesmo adicionar novos métodos à classe existente terá impacto na nova classe.

14. Prefira interfaces a classes abstratas

O Java tem dois mecanismos para definir um tipo que permite várias implementações: interfaces e classes abstratas. Desde a introdução dos métodos default em interfaces no Java 8, os dois mecanismos permitem fornecer implementações para alguns métodos de instância.

Uma diferença importante é que, para implementar o tipo definido por uma classe abstrata, uma classe precisa ser subclasse dessa classe abstrata. Como o Java só permite herança simples, essa restrição limita bastante o uso de classes abstratas como definições de tipo. Qualquer classe que defina todos os métodos exigidos e obedeça ao contrato geral pode implementar uma interface, não importa onde a classe esteja na hierarquia de classes. Classes existentes podem ser facilmente adaptadas para implementar uma nova interface.

15. Generics: não use raw types

Você só descobre o erro em tempo de execução, muito depois de ele ter acontecido, e em um código que pode estar longe do código que contém o erro. Quando você vê a ClassCastException, precisa vasculhar a base de código atrás da invocação de método que colocou a moeda na coleção de selos. O compilador não pode ajudar você.

16. Generics: elimine os warnings unchecked

Quando você programa com generics, vê muitos warnings do compilador:

Quanto mais experiência você adquire com generics, menos warnings recebe, mas não espere que um código recém-escrito compile sem nenhum. Muitos warnings unchecked são fáceis de eliminar. Por exemplo, suponha que você escreva sem querer esta declaração:

Set<Lark> exaltation = new HashSet();

Mude para:

Set<Lark> exaltation = new HashSet<>();

Alguns warnings serão muito mais difíceis de eliminar. Quando aparecerem warnings que exigem alguma reflexão, persista! Elimine todos os warnings unchecked que puder.

Se você eliminar todos os warnings, terá a garantia de que seu código é typesafe, o que é ótimo. Isso significa que você não vai receber uma ClassCastException em tempo de execução e aumenta sua confiança de que o programa vai se comportar como você pretendia. Se você não consegue eliminar um warning, mas consegue provar que o código que o gerou é typesafe, então (e só então) suprima o warning com uma anotação @SuppressWarnings("unchecked"). Se você suprimir warnings sem antes provar que o código é typesafe, estará se dando uma falsa sensação de segurança. O código pode compilar sem emitir nenhum warning, mas ainda assim lançar uma ClassCastException em tempo de execução.

Toda vez que usar uma anotação @SuppressWarnings("unchecked"), adicione um comentário explicando por que é seguro fazer isso.

17. Generics: quando fazem sentido

Ai – uma classe que precisa urgentemente de generics:

Exemplo de uma classe que precisa de generics

Algo assim deveria ser barrado no merge request, no mais tardar!

18. Generics: como não fazer

Desculpe, não precisa dizer nada. Por favor, NUNCA faça merge disto:

public abstract class JpaLearningCardBaseContentService<LC extends LearningCardBaseContent<L, LC, AC>, J extends JpaLearningCardBaseContent<L, LC, AC>, L extends
        LearningCardBase<LC, A>, A extends AnswerOptionBase<L, AC>, AC extends AnswerOptionBaseContent<LC, AC, A>, AS extends AnswerOptionBaseService<A, AC, L, LC>, ACS extends
        AnswerOptionBaseContentService<AC, A, L, LC>, LS extends LearningCardBaseService<L>>
    extends JpaLightPersistenceService<LC, J, Long> implements LearningCardBaseContentService<LC, L, A, AC> {
    // ...
}

Uma simples declaração de classe nunca deveria levar 5 minutos para ser entendida.

19. Prefira listas a arrays

Arrays diferem dos tipos genéricos em dois aspectos importantes. Primeiro, arrays são covariantes. Essa palavra assustadora significa simplesmente que, se Sub é um subtipo de Super, então o tipo array Sub[] é um subtipo do tipo array Super[]. Os generics, por outro lado, são invariantes: para quaisquer dois tipos distintos Type1 e Type2, List<Type1> não é nem subtipo nem supertipo de List<Type2>.

20. Prefira lambdas a classes anônimas

// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return Integer.compare(s1.length(), s2.length());
    }
});

Em vez disso, faça assim:

// Lambda expression as function object (replaces anonymous class)
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));

21. Minimize o escopo das variáveis locais

A técnica mais poderosa para minimizar o escopo de uma variável local é declará-la onde ela é usada pela primeira vez...

22. Prefira loops for-each a loops for tradicionais

// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
    ... // Do something with e
}

23. Conheça e use as bibliotecas

Suponha que você queira gerar inteiros aleatórios entre zero e algum limite superior. Diante dessa tarefa comum, muitos programadores escreveriam um pequeno método parecido com este:

// Common but deeply flawed!
static Random rnd = new Random();

static int random(int n) {
    return Math.abs(rnd.nextInt()) % n;
}

Esse método pode parecer bom, mas tem três falhas. A primeira é que, se n for uma pequena potência de dois, a sequência de números aleatórios vai se repetir depois de um período bem curto. A segunda falha é que, se n não for uma potência de dois, alguns números serão, em média, retornados com mais frequência do que outros. Se n for grande, esse efeito pode ser bem acentuado.

A terceira falha do método random é que ele pode, em raras ocasiões, falhar de forma catastrófica, retornando um número fora do intervalo especificado. Isso acontece porque o método tenta mapear o valor retornado por rnd.nextInt() para um int não negativo chamando Math.abs. Se nextInt() retornar Integer.MIN_VALUE, Math.abs também vai retornar Integer.MIN_VALUE, e o operador de resto (%) vai retornar um número negativo, supondo que n não seja uma potência de dois. Isso quase certamente fará seu programa falhar, e a falha pode ser difícil de reproduzir.

24. Evite float e double se forem necessárias respostas exatas

Use BigDecimal para valores monetários. float e double geram erros de arredondamento.

Por exemplo, suponha que você tenha US$ 1,03 no bolso e gaste 42 centavos. Quanto dinheiro sobra? Eis um fragmento de programa ingênuo que tenta responder a essa pergunta:

System.out.println(1.03 - 0.42);

Infelizmente, ele imprime 0.6100000000000001. Não é um caso isolado. Suponha que você tenha um dólar no bolso e compre nove arruelas a dez centavos cada. Quanto você recebe de troco?

System.out.println(1.00 - 9 * 0.10);

Segundo esse fragmento de programa, você recebe $0.09999999999999998.

Você pode achar que o problema se resolve simplesmente arredondando os resultados antes de imprimi-los, mas, infelizmente, isso nem sempre funciona. Por exemplo, suponha que você tenha um dólar no bolso e veja uma prateleira com uma fileira de balas deliciosas que custam 10, 20, 30 centavos e assim por diante, até um dólar. Você compra uma bala de cada, começando pela de 10 centavos, até não ter mais dinheiro para comprar a próxima bala da prateleira.

25. Prefira tipos primitivos a primitivos encapsulados

// Broken comparator - can you spot the flaw?
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);

26. Refira-se aos objetos por suas interfaces

Bom exemplo:

Set<Son> sonSet = new LinkedHashSet<>();

Mau exemplo:

LinkedHashSet<Son> sonSet = new LinkedHashSet<>();

27. Prefira interfaces a reflexão

Caso contrário:

28. Siga as convenções de nomenclatura geralmente aceitas

A plataforma Java tem um conjunto bem estabelecido de convenções de nomenclatura, muitas das quais estão na The Java Language Specification [JLS, 6.1].

29. Use exceções apenas para condições excepcionais

// Horrible abuse of exceptions. Don't ever do this!
try {
    int i = 0;
    while (true) {
        range[i++].climb();
    }
} catch (ArrayIndexOutOfBoundsException e) {
    // catch it
}

O que esse código faz? Não fica nada óbvio só de olhar, e esse já é motivo suficiente para não usá-lo. Acontece que ele é um idioma terrivelmente mal concebido para percorrer os elementos de um array. O loop infinito termina lançando, capturando e ignorando uma ArrayIndexOutOfBoundsException quando tenta acessar o primeiro elemento fora dos limites do array. A ideia é que ele seja equivalente ao idioma padrão para percorrer um array, que qualquer programador Java reconhece na hora:

for (Mountain m : range) {
    m.climb();
}

Exceções, como o nome indica, devem ser usadas apenas para condições excepcionais; nunca devem ser usadas no fluxo de controle normal.

30. Use exceções checked para condições recuperáveis e exceções de runtime para erros de programação

Use exceções checked para condições das quais se pode razoavelmente esperar que o chamador se recupere. Ao confrontar o usuário com uma exceção checked, o designer da API impõe a obrigação de se recuperar da condição. O usuário pode ignorar essa obrigação capturando a exceção e não fazendo nada, mas em geral isso é má ideia.

Use exceções de runtime para indicar erros de programação. A grande maioria das exceções de runtime indica violações de pré-condições. Uma violação de pré-condição é simplesmente o cliente de uma API deixar de cumprir o contrato estabelecido pela especificação da API. Por exemplo, o contrato de acesso a arrays especifica que o índice do array deve estar entre zero e o tamanho do array menos um, inclusive. ArrayIndexOutOfBoundsException indica que essa pré-condição foi violada.

31. Evite o uso desnecessário de exceções checked

Em resumo, quando usadas com moderação, as exceções checked podem aumentar a confiabilidade dos programas; quando usadas em excesso, tornam as APIs penosas de usar.

Pair<Boolean, LearningCardGroup> answerLearningCard(@NotNull LearningCardGroup learningCardGroup,
                                                      LearningCardAssignment learningCardAssignment,
                                                      @NotNull Collection<AnswerOption> answers)
      throws InvalidTrainingStateException, PersistenceException, LearningCardGroupAlreadyCompletedException,
             ConsecutiveCorrectCountExceededException,
             PreviousLearningCardGroupUncompletedException, LearningCardGroupCooldownException,
             LearningCardGroupHasUncompletedLearningCardsException, InvalidParticipantException, TrainingNotSignedException; // but why ???
     // this is a mistake seen a lot with junior devs
     // exceptions are exceptionally slow and should be avoided for mere validation
     // the performance gets even worse if a stacktrace is printed, since logging and printing is one of the slowest possible actions

32. Prefira usar as exceções padrão

Uma característica que diferencia programadores experientes dos menos experientes é que os experientes buscam e geralmente alcançam um alto grau de reutilização de código.

As exceções não são exceção à regra de que reutilizar código é bom. As bibliotecas do Java oferecem um conjunto de exceções que cobre a maior parte das necessidades da maioria das APIs. Reutilizar exceções padrão traz vários benefícios. O principal é que isso torna sua API mais fácil de aprender e usar, porque segue convenções estabelecidas com as quais os programadores já estão familiarizados. Logo em seguida vem o fato de que programas que usam sua API ficam mais fáceis de ler, porque não estão atulhados de exceções desconhecidas. Por último (e menos importante), menos classes de exceção significam menor consumo de memória e menos tempo gasto carregando classes.

O tipo de exceção mais reutilizado é IllegalArgumentException. Em geral, é a exceção a ser lançada quando o chamador passa um argumento cujo valor é inadequado. Por exemplo, seria a exceção a lançar se o chamador passasse um número negativo em um parâmetro que representa quantas vezes uma ação deve ser repetida. Outra exceção muito reutilizada é IllegalStateException. Em geral, é a exceção a ser lançada quando a invocação é ilegal por causa do estado do objeto receptor. Por exemplo, seria a exceção a lançar se o chamador tentasse usar um objeto antes de ele ter sido devidamente inicializado.

Pode-se argumentar que toda invocação de método incorreta se resume a um argumento ou estado ilegal, mas outras exceções são usadas por padrão para certos tipos de argumentos e estados ilegais. Se um chamador passar null em algum parâmetro para o qual valores null são proibidos, a convenção determina que seja lançada uma NullPointerException, e não uma IllegalArgumentException. Da mesma forma, se um chamador passar um valor fora do intervalo em um parâmetro que representa um índice de uma sequência, deve-se lançar IndexOutOfBoundsException, e não IllegalArgumentException.

Não reutilize Exception, RuntimeException, Throwable ou Error diretamente. Trate essas classes como se fossem abstratas. Ainda assim, RuntimeException pode ser usada se você quiser converter uma exceção checked em uma exceção de runtime e souber muito bem o que está fazendo.

33. Não ignore exceções

// Empty catch block ignores exception - Highly suspect!
try {
    ...
} catch (SomeException e) {
}

Um bloco catch vazio anula o propósito das exceções, que é obrigar você a tratar condições excepcionais. Ignorar uma exceção é como ignorar um alarme de incêndio – e ainda desligá-lo para que ninguém mais tenha a chance de ver se há fogo de verdade.