Principais Lições
1. Fábricas Estáticas em vez de Construtores: Nomeação, Controle e Flexibilidade
Oferecer um método de fábrica estático em vez de um construtor público traz vantagens e desvantagens.
Vantagens da Nomeação. Métodos de fábrica estáticos permitem nomes descritivos, ao contrário dos construtores, o que melhora a legibilidade e usabilidade do código. Isso é especialmente útil quando os parâmetros do construtor não indicam claramente o objeto que está sendo criado. Por exemplo, um método chamado BigInteger.probablePrime() é mais descritivo do que um construtor BigInteger(int, int, Random).
Controle da Instância. Fábricas estáticas não são obrigadas a criar novos objetos a cada invocação, permitindo que classes imutáveis armazenem em cache instâncias e evitem a criação desnecessária de objetos. Isso pode melhorar significativamente o desempenho, especialmente para objetos frequentemente solicitados. O método Boolean.valueOf(boolean) exemplifica isso, pois sempre retorna as mesmas instâncias Boolean.TRUE ou Boolean.FALSE.
Flexibilidade de Subtipos. Fábricas estáticas podem retornar objetos de qualquer subtipo do seu tipo de retorno, permitindo que APIs retornem objetos sem tornar suas classes públicas. Isso promove uma API compacta e permite variar a classe retornada com base nos parâmetros de entrada ou até mesmo entre diferentes versões, aumentando a manutenção e flexibilidade. Essa é a base de frameworks de provedores de serviço como a Java Cryptography Extension (JCE).
2. Implementação de Singleton: Construtores Privados e Acesso Público
Um singleton é simplesmente uma classe que é instanciada exatamente uma vez [Gamma98, p. 127].
Garantindo a Unicidade. Singletons, que representam componentes únicos do sistema, são garantidos tornando o construtor privado e fornecendo um membro estático público para acesso. Isso assegura que exista apenas uma instância. Existem duas abordagens comuns: usar um campo final ou um método de fábrica estático.
Abordagem do Campo Final. O membro estático público é um campo final inicializado com a instância singleton. Essa abordagem é direta e deixa claro que a classe é um singleton. Por exemplo:
public static final Elvis INSTANCE = new Elvis();
Abordagem da Fábrica Estática. Um método de fábrica estático público retorna a instância singleton. Isso oferece flexibilidade para alterar a implementação sem modificar a API. Por exemplo:
public static Elvis getInstance() { return INSTANCE; }
Consideração sobre Serialização. Para manter a propriedade singleton durante a serialização, deve-se fornecer um método readResolve que retorne a instância existente, evitando a criação de novas instâncias na desserialização. Isso garante que o singleton permaneça único mesmo após serialização e desserialização.
3. Classes Utilitárias: Impedindo a Instanciação
Essas classes utilitárias não foram projetadas para serem instanciadas: uma instância seria sem sentido.
Propósito das Classes Utilitárias. Classes utilitárias, que agrupam métodos e campos estáticos, servem para organizar funcionalidades relacionadas a valores primitivos, arrays ou objetos que implementam interfaces específicas. Elas não foram feitas para serem instanciadas, pois instâncias seriam inúteis. Exemplos incluem java.lang.Math e java.util.Arrays.
Evitando a Instanciação. Para garantir que não sejam instanciadas, um idiom simples consiste em incluir um único construtor privado explícito. Isso impede que o compilador gere um construtor público padrão sem parâmetros, assegurando que a classe não possa ser instanciada externamente.
Efeitos Colaterais. Esse idiom também impede a herança, pois subclasses não teriam um construtor acessível para invocar o construtor da superclasse. É recomendável incluir um comentário explicando o propósito do construtor privado, pois pode parecer contraintuitivo.
4. Reutilização de Objetos: Eficiência e Imutabilidade
Muitas vezes é apropriado reutilizar um único objeto em vez de criar um novo objeto funcionalmente equivalente toda vez que ele é necessário.
Benefícios da Reutilização. Reutilizar objetos, especialmente imutáveis, é mais eficiente e elegante do que criar novos repetidamente. Isso porque a criação de objetos e a coleta de lixo podem ser custosas, especialmente para objetos pesados. Por exemplo, usar String s = "silly"; é melhor do que String s = new String("silly");.
Objetos Imutáveis. Objetos imutáveis podem sempre ser reutilizados com segurança. Por exemplo, Boolean.valueOf(String) é preferível a Boolean(String) porque o primeiro pode reutilizar instâncias existentes.
Objetos Mutáveis. Objetos mutáveis podem ser reutilizados se for garantido que não serão modificados. Por exemplo, usar inicialização estática para criar instâncias de Calendar e Date uma única vez, em vez de criá-las a cada invocação de método, pode melhorar significativamente o desempenho.
5. Gerenciamento de Memória: Eliminando Referências Obsoletas
Uma referência obsoleta é simplesmente uma referência que nunca mais será desreferenciada.
Vazamentos de Memória. Vazamentos de memória em linguagens com coleta de lixo ocorrem quando referências a objetos são mantidas inadvertidamente, impedindo a coleta desses objetos. Isso pode levar a desempenho reduzido, aumento do uso de memória e até OutOfMemoryError.
Identificando Vazamentos. Uma fonte comum de vazamentos são classes que gerenciam sua própria memória, como uma classe Stack. Quando um elemento é liberado, quaisquer referências a objetos contidas nele devem ser anuladas.
Soluções. Para evitar vazamentos, anule referências assim que se tornem obsoletas. Também é importante cuidar de caches, que podem manter referências a objetos muito tempo após sua relevância. Use WeakHashMap para caches onde as entradas são relevantes apenas enquanto existirem referências às suas chaves fora do cache.
6. Contrato do Equals: Reflexividade, Simetria e Transitividade
O método equals implementa uma relação de equivalência.
Relação de Equivalência. Ao sobrescrever o método equals, é fundamental respeitar seu contrato geral, que define uma relação de equivalência. Esse contrato inclui reflexividade (x.equals(x) deve retornar true), simetria (x.equals(y) deve retornar true se e somente se y.equals(x) retornar true) e transitividade (se x.equals(y) e y.equals(z) são verdadeiros, então x.equals(z) deve ser verdadeiro).
Violação da Simetria. Quebrar a simetria pode causar comportamentos imprevisíveis quando objetos são usados em coleções. Por exemplo, uma classe CaseInsensitiveString que tenta interoperar com strings comuns pode violar a simetria.
Violação da Transitividade. Quebrar a transitividade pode ocorrer ao estender uma classe instanciável e adicionar um aspecto que afeta comparações equals. Para evitar isso, prefira composição em vez de herança.
Não Nulidade. O método equals deve retornar false para qualquer valor de referência não nulo x quando x.equals(null) for invocado. O operador instanceof trata isso automaticamente.
7. Sobrescrita do HashCode: Consistência com Equals
Você deve sobrescrever hashCode em toda classe que sobrescreve equals.
Importância do HashCode. Sobrescrever hashCode é essencial ao sobrescrever equals para garantir que objetos iguais tenham códigos hash iguais. Não fazê-lo viola o contrato geral de Object.hashCode e impede que a classe funcione corretamente em coleções baseadas em hash, como HashMap e HashSet.
Função Hash Legal, mas Ruim. Um método trivial de hashCode que sempre retorna o mesmo valor é legal, mas resulta em desempenho ruim para tabelas hash, pois todos os objetos caem no mesmo balde.
Receita para uma Boa Função Hash. Uma boa função hash tende a produzir códigos hash diferentes para objetos diferentes. Uma receita simples envolve:
- Inicializar uma variável
intchamadaresultcom um valor constante não nulo (por exemplo, 17). - Para cada campo significativo
fdo objeto, calcular um código hashce combiná-lo emresultusandoresult = 37 * result + c;. - Retornar
result.
8. Sobrescrita do ToString: Representação Concisa e Informativa
O método toString é automaticamente invocado quando seu objeto é passado para println, o operador de concatenação de strings (+) ou, a partir da versão 1.4, assert.
Importância do ToString. Sobrescrever o método toString fornece uma representação em string concisa e informativa de um objeto, tornando a classe mais agradável de usar. Isso é especialmente útil para depuração e registro.
Conteúdo do ToString. Quando possível, o método toString deve retornar todas as informações interessantes contidas no objeto. Se o objeto for grande ou contiver estado que não se presta a uma representação em string, deve-se retornar um resumo.
Especificando o Formato. Ao implementar toString, decida se deve especificar o formato do valor retornado na documentação. Especificar o formato fornece uma representação padrão e inequívoca, mas limita a flexibilidade para mudanças futuras. Se especificar o formato, forneça um construtor String correspondente ou uma fábrica estática.
9. Interface Cloneable: Implementação Cuidadosa
A interface Cloneable foi concebida como uma interface mixin (Item 16) para objetos anunciarem que permitem clonagem.
Deficiências do Cloneable. A interface Cloneable não possui um método clone, e o método clone de Object é protegido. Isso dificulta invocar o método clone em um objeto apenas porque ele implementa Cloneable.
Contrato do Clone. O contrato geral para o método clone é fraco, afirmando que ele cria e retorna uma cópia do objeto. No entanto, não especifica como essa cópia deve ser feita ou se os construtores devem ser chamados.
Implementando Cloneable. Para implementar Cloneable corretamente, uma classe deve sobrescrever o método clone com um método público que primeiro chama super.clone e depois corrige quaisquer campos que precisem ser ajustados. Isso normalmente significa copiar quaisquer objetos mutáveis que compõem a "estrutura profunda" interna do objeto.
Alternativas ao Cloneable. Uma abordagem recomendada para cópia de objetos é fornecer um construtor de cópia ou uma fábrica estática. Essas abordagens não dependem de um mecanismo de criação de objetos extralinguístico e arriscado, e não conflitam com o uso correto de campos finais.
10. Interface Comparable: Ordenação Natural
Ao implementar Comparable, uma classe indica que suas instâncias possuem uma ordenação natural.
Benefícios do Comparable. Implementar a interface Comparable permite que uma classe interaja com algoritmos genéricos e implementações de coleções que dependem de ordenação, como Arrays.sort e TreeSet.
Contrato do CompareTo. O contrato geral para o método compareTo é semelhante ao do método equals, exigindo reflexividade, transitividade e simetria. Recomenda-se fortemente que (x.compareTo(y)==0) == (x.equals(y)).
Escrevendo um Método CompareTo. Ao escrever um método compareTo, compare campos de referência de objetos invocando recursivamente o método compareTo. Compare campos primitivos usando os operadores relacionais < e >. Se a classe tiver múltiplos campos significativos, compare-os na ordem de importância.
11. Minimizar a Acessibilidade: Ocultação de Informação
O fator mais importante que distingue um módulo bem projetado de um mal projetado é o grau em que o módulo oculta seus dados internos e outros detalhes de implementação de outros módulos.
Benefícios da Ocultação de Informação. A ocultação de informação, ou encapsulamento, desacopla módulos, permitindo que sejam desenvolvidos, testados, otimizados e modificados independentemente. Isso acelera o desenvolvimento do sistema, facilita a manutenção, aumenta o reuso de software e diminui riscos.
Níveis de Acesso. A linguagem Java oferece modificadores de acesso (private, pacote-padrão, protected e public) para auxiliar na ocultação de informação. A regra prática é tornar cada classe ou membro o mais inacessível possível.
Campos Públicos. Classes públicas devem raramente, se é que devem, ter campos públicos (em oposição a métodos públicos). A exceção é para campos públicos estáticos finais contendo valores primitivos ou referências a objetos imutáveis.
12. Favorecer a Imutabilidade: Simplicidade e Segurança em Threads
Uma classe imutável é simplesmente uma classe cujas instâncias não podem ser modificadas.
Vantagens da Imutabilidade. Classes imutáveis são mais fáceis de projetar, implementar e usar do que classes mutáveis. São menos propensas a erros e mais seguras.
Regras para Imutabilidade. Para tornar uma classe imutável:
- Não forneça métodos que modifiquem o objeto (mutadores).
- Assegure que nenhum método possa ser sobrescrito (torne a classe final ou use construtores privados e fábricas estáticas).
- Torne todos os campos finais.
- Torne todos os campos privados.
- Garanta acesso exclusivo a quaisquer componentes mutáveis (faça cópias defensivas).
Desvantagens da Imutabilidade. A única desvantagem real das classes imutáveis é que elas exigem um objeto separado para cada valor distinto. Para lidar com isso, forneça constantes públicas estáticas finais para valores usados com frequência e considere oferecer uma classe pública mutável acompanhante.
Resumo das Resenhas
Effective Java é amplamente reconhecido como uma leitura indispensável para programadores Java, elogiado pelas suas explicações claras e conselhos práticos. Os leitores valorizam a experiência de Bloch e o enfoque do livro em escrever código de alta qualidade e fácil manutenção. Muitos consideram-no uma obra obrigatória, tanto para iniciantes como para programadores experientes. O livro aborda diversas funcionalidades do Java, melhores práticas e armadilhas comuns. Embora alguns considerem certas partes desatualizadas ou demasiado avançadas, a maioria concorda que ele melhora significativamente as competências de programação e a compreensão das complexidades do Java.
Outros Também Leram
Perguntas Frequentes
What's Effective Java about?
- Java best practices: Effective Java by Joshua Bloch is a comprehensive guide focused on best practices for Java programming. It aims to help developers write clear, correct, robust, and reusable code.
- Practical advice: The book is tailored for developers familiar with Java who wish to enhance their coding skills. It covers a wide range of topics, including object creation, methods, classes, and interfaces.
- Structured format: The content is organized into distinct items, each addressing a specific aspect of Java programming. This structure allows readers to easily navigate and apply the advice.
Why should I read Effective Java?
- Improve coding skills: Reading Effective Java can significantly enhance your Java programming abilities by providing insights not commonly found in other resources.
- Expert insights: Joshua Bloch, a key contributor to the Java platform, shares his extensive experience through practical examples and rules of thumb.
- Code quality focus: The book emphasizes writing code that is not only functional but also maintainable and efficient, leading to fewer bugs and easier code management.
What are the key takeaways of Effective Java?
- Clarity and simplicity: The book stresses the importance of writing clear and simple code, ensuring understandability and maintainability over time.
- Composition over inheritance: Bloch advises using composition instead of inheritance to create more flexible and robust code structures, reducing dependencies and potential fragility.
- Static factory methods: The book recommends using static factory methods for object creation, offering better readability and flexibility.
What are some specific rules from Effective Java?
- Static Factory Methods: Consider providing static factory methods instead of constructors for better naming and performance.
- Singleton Property: Enforce the singleton property with a private constructor to ensure a class has only one instance.
- Override Equals and HashCode: Always override
hashCodewhen you overrideequalsto ensure correct functioning of hash-based collections.
How does Effective Java address object creation?
- Static Factory Methods: Suggests using static factory methods for object creation instead of constructors for better naming and performance.
- Avoiding Duplicate Objects: Advises against creating duplicate objects, especially for immutable types, to improve memory management.
- Enforcing Noninstantiability: Discusses enforcing noninstantiability with a private constructor for utility classes to prevent unnecessary instances.
What are the benefits of immutability discussed in Effective Java?
- Thread Safety: Immutable objects are inherently thread-safe, eliminating the need for synchronization and simplifying concurrent programming.
- Simplicity and Clarity: Immutability leads to simpler code, making reasoning about the code easier and reducing the likelihood of bugs.
- Reuse and Performance: Immutable objects can be shared freely, allowing for efficient memory usage and reduced overhead of object creation.
How does Effective Java suggest handling exceptions?
- Use Exceptions for Exceptional Conditions: Advises using exceptions only for exceptional conditions, not for regular control flow, to maintain clarity and avoid performance penalties.
- Document Exceptions: Emphasizes documenting exceptions that a method can throw to improve code readability and help users handle potential errors.
- Favor Standard Exceptions: Recommends using standard exceptions provided by the Java platform for consistency and familiarity.
What is the significance of the Effective Java item on interfaces?
- Define Types with Interfaces: Emphasizes using interfaces to define types, not to export constants, keeping the API clean and focused on behavior.
- Favor Interfaces Over Abstract Classes: Advises using interfaces for flexibility, allowing multiple implementations without single inheritance constraints.
- Avoid Constant Interfaces: Warns against using constant interfaces, suggesting utility classes instead to group constants.
How does Effective Java recommend designing for inheritance?
- Design and Document for Inheritance: If a class is designed for inheritance, it must document the effects of overriding methods to ensure subclass understanding.
- Prohibit Unnecessary Inheritance: Suggests making classes final or using private constructors to prevent unintended inheritance, reducing fragility.
- Provide Protected Methods: Expose protected methods judiciously to allow subclass extension while avoiding excessive implementation detail exposure.
How does Effective Java address concurrency?
- Synchronize Access to Shared Data: Emphasizes synchronizing access to shared mutable data to prevent data corruption and ensure thread safety.
- Avoid Excessive Synchronization: Advises minimizing work within synchronized blocks to enhance performance and avoid deadlocks.
- Document Thread Safety: Stresses the importance of documenting the thread safety of classes and methods for safe multithreaded use.
What is the double-checked locking idiom, and why is it problematic?
- Lazy Initialization Purpose: Used to reduce synchronization overhead when lazily initializing a resource by checking if it's initialized before locking.
- Inconsistent States Risk: Without proper synchronization, threads may see a partially constructed object, leading to inconsistent states and bugs.
- Avoidance Recommendation: Advises against using double-checked locking due to its complexity and potential pitfalls, suggesting simpler alternatives.
What are the best quotes from Effective Java and what do they mean?
- "Programs must be written for people to read, and only incidentally for machines to execute." Emphasizes writing code understandable to humans, not just optimized for machines.
- "The single most important factor that distinguishes a well-designed module from a poorly designed one is the degree to which the module hides its internal data." Highlights the principle of encapsulation for maintainable and flexible code.
- "Don't create a new object when you should reuse an existing one." Encourages object reuse, especially for immutable objects, to enhance performance and reduce memory usage.