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:
- Uma vantagem dos métodos de fábrica estáticos é que, ao contrário dos construtores, eles têm nomes.
- Eles não precisam criar um novo objeto toda vez que são chamados.
- Eles podem retornar um objeto de qualquer subtipo do seu tipo de retorno.
- A classe do objeto retornado pode variar de chamada para chamada, em função dos parâmetros de entrada.
- A classe do objeto retornado não precisa existir quando a classe que contém o método é escrita.
Desvantagens:
- A principal limitação de oferecer apenas métodos de fábrica estáticos é que classes sem construtores públicos ou protegidos não podem ter subclasses.
- Uma segunda desvantagem dos métodos de fábrica estáticos é que eles são difíceis de encontrar para os programadores.
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:
- Não ofereça métodos que modifiquem o estado do objeto (conhecidos como mutators, ou modificadores).
-
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. - 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.
- 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.
-
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.
- Interfaces são ideais para definir mixins.
- Interfaces permitem construir estruturas de tipos não hierárquicas.
- Interfaces possibilitam melhorias de funcionalidade seguras e poderosas por meio do idioma de classe wrapper. Se você usar classes abstratas para definir tipos, não deixa ao programador que quer adicionar funcionalidade nenhuma alternativa além da herança.
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:
- warnings de cast unchecked
- warnings de invocação de método unchecked
- warnings de tipo vararg parametrizado unchecked
- warnings de conversão unchecked.
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:
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:
- Você perde todos os benefícios da verificação de tipos em tempo de compilação, inclusive a verificação de exceções. Se um programa tentar invocar via reflexão um método inexistente ou inacessível, ele vai falhar em tempo de execução, a menos que você tenha tomado precauções especiais.
- O código necessário para o acesso via reflexão é desajeitado e verboso. É tedioso de escrever e difícil de ler.
- A performance sofre. A invocação de métodos via reflexão é muito mais lenta do que a invocação normal.
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.