Puntos clave
1. Fábricas Estáticas en Lugar de Constructores: Nombres, Control y Flexibilidad
Ofrecer un método de fábrica estático en lugar de un constructor público tiene tanto ventajas como desventajas.
Ventajas Nombradas. Los métodos de fábrica estáticos permiten usar nombres descriptivos, a diferencia de los constructores, lo que mejora la legibilidad y usabilidad del código. Esto es especialmente útil cuando los parámetros del constructor no indican claramente el objeto que se crea. Por ejemplo, un método llamado BigInteger.probablePrime()
es más explícito que un constructor BigInteger(int, int, Random)
.
Control de Instancias. Las fábricas estáticas no están obligadas a crear un nuevo objeto en cada llamada, lo que permite que las clases inmutables almacenen en caché instancias y eviten crear objetos innecesarios. Esto puede mejorar significativamente el rendimiento, sobre todo para objetos solicitados con frecuencia. El método Boolean.valueOf(boolean)
ejemplifica esto, ya que siempre devuelve las mismas instancias Boolean.TRUE
o Boolean.FALSE
.
Flexibilidad de Subtipos. Las fábricas estáticas pueden devolver objetos de cualquier subtipo de su tipo de retorno, lo que permite que las APIs devuelvan objetos sin hacer públicas sus clases. Esto promueve una API compacta y permite variar la clase devuelta según los parámetros de entrada o incluso entre diferentes versiones, mejorando la mantenibilidad y flexibilidad. Esta es la base de frameworks proveedores de servicios como la Extensión de Criptografía de Java (JCE).
2. Implementación de Singleton: Constructores Privados y Acceso Público
Un singleton es simplemente una clase que se instancia exactamente una vez [Gamma98, p. 127].
Garantizando la Unicidad. Los singletons, que representan componentes únicos del sistema, se aseguran haciendo el constructor privado y proporcionando un miembro estático público para el acceso. Esto garantiza que solo exista una instancia. Hay dos enfoques comunes: usar un campo final o un método de fábrica estático.
Enfoque de Campo Final. El miembro estático público es un campo final inicializado con la instancia singleton. Este enfoque es sencillo y deja claro que la clase es un singleton. Por ejemplo:
public static final Elvis INSTANCE = new Elvis();
Enfoque de Fábrica Estática. Un método de fábrica estático público devuelve la instancia singleton. Esto ofrece flexibilidad para cambiar la implementación sin alterar la API. Por ejemplo:
public static Elvis getInstance() { return INSTANCE; }
Consideración sobre Serialización. Para mantener la propiedad singleton durante la serialización, debe proporcionarse un método readResolve
que devuelva la instancia existente, evitando la creación de nuevas instancias al deserializar. Esto asegura que el singleton siga siendo único incluso después de la serialización y deserialización.
3. Clases Utilitarias: Imposibilidad de Instanciación
Estas clases utilitarias no fueron diseñadas para ser instanciadas: una instancia carecería de sentido.
Propósito de las Clases Utilitarias. Las clases utilitarias, que agrupan métodos y campos estáticos, sirven para organizar funcionalidades relacionadas con valores primitivos, arreglos u objetos que implementan interfaces específicas. No están diseñadas para ser instanciadas, ya que una instancia sería absurda. Ejemplos incluyen java.lang.Math
y java.util.Arrays
.
Prevención de Instanciación. Para asegurar que no se puedan instanciar, se emplea un patrón sencillo que consiste en incluir un único constructor privado explícito. Esto impide que el compilador genere un constructor público sin parámetros por defecto, garantizando que la clase no pueda ser instanciada desde fuera.
Efectos Secundarios. Este patrón también impide la herencia, ya que las subclases carecerían de un constructor accesible para invocar al constructor de la superclase. Es recomendable incluir un comentario que explique el propósito del constructor privado, pues puede parecer contraintuitivo.
4. Reutilización de Objetos: Eficiencia e Inmutabilidad
A menudo es apropiado reutilizar un único objeto en lugar de crear uno nuevo funcionalmente equivalente cada vez que se necesita.
Beneficios de la Reutilización. Reutilizar objetos, especialmente inmutables, es más eficiente y elegante que crear nuevos repetidamente. Esto se debe a que la creación de objetos y la recolección de basura pueden ser costosas, sobre todo para objetos pesados. Por ejemplo, usar String s = "silly";
es mejor que String s = new String("silly");
.
Objetos Inmutables. Los objetos inmutables pueden reutilizarse siempre de forma segura. Por ejemplo, Boolean.valueOf(String)
es preferible a Boolean(String)
porque el primero puede reutilizar instancias existentes.
Objetos Mutables. Los objetos mutables pueden reutilizarse si se sabe que no serán modificados. Por ejemplo, usar inicialización estática para crear instancias de Calendar
y Date
una sola vez, en lugar de crearlas cada vez que se invoca un método, puede mejorar significativamente el rendimiento.
5. Gestión de Memoria: Eliminando Referencias Obsoletas
Una referencia obsoleta es simplemente una referencia que nunca volverá a ser desreferenciada.
Fugas de Memoria. Las fugas de memoria en lenguajes con recolección de basura ocurren cuando se retienen referencias a objetos de forma involuntaria, impidiendo su recolección. Esto puede causar reducción del rendimiento, aumento del uso de memoria e incluso OutOfMemoryError
.
Identificación de Fugas. Una fuente común de fugas son las clases que gestionan su propia memoria, como una clase Stack
. Cuando se libera un elemento, las referencias a objetos contenidas en él deben ser anuladas.
Soluciones. Para evitar fugas, se deben anular las referencias una vez que se vuelven obsoletas. También hay que tener cuidado con las cachés, que pueden retener referencias mucho después de que sean irrelevantes. Para cachés donde las entradas son relevantes solo mientras existan referencias a sus claves fuera de la caché, se recomienda usar WeakHashMap
.
6. Contrato de Equals: Reflexividad, Simetría y Transitividad
El método equals implementa una relación de equivalencia.
Relación de Equivalencia. Al sobrescribir el método equals
, es crucial respetar su contrato general, que define una relación de equivalencia. Este contrato incluye reflexividad (x.equals(x) debe devolver true), simetría (x.equals(y) debe devolver true si y solo si y.equals(x) devuelve true) y transitividad (si x.equals(y) y y.equals(z) son true, entonces x.equals(z) debe ser true).
Violación de Simetría. Romper la simetría puede causar comportamientos impredecibles cuando los objetos se usan en colecciones. Por ejemplo, una clase CaseInsensitiveString
que intenta interoperar con cadenas ordinarias puede violar la simetría.
Violación de Transitividad. La violación de la transitividad puede ocurrir al extender una clase instanciable y añadir un aspecto que afecta las comparaciones equals
. Para evitarlo, es preferible usar composición en lugar de herencia.
No Nulidad. El método equals
debe devolver false para cualquier referencia no nula x cuando se invoque x.equals(null). El operador instanceof
maneja esto automáticamente.
7. Sobrescribir HashCode: Consistencia con Equals
Debe sobrescribirse hashCode en toda clase que sobrescriba equals.
Importancia de HashCode. Sobrescribir hashCode
es esencial al sobrescribir equals
para asegurar que objetos iguales tengan códigos hash iguales. No hacerlo viola el contrato general de Object.hashCode
y evita que la clase funcione correctamente en colecciones basadas en hash como HashMap
y HashSet
.
Función Hash Legal pero Deficiente. Un método hashCode
trivial que siempre devuelve el mismo valor es legal pero produce un rendimiento pobre en tablas hash, ya que todos los objetos se asignan al mismo cubo.
Receta para una Buena Función Hash. Una buena función hash tiende a producir códigos hash diferentes para objetos diferentes. Una receta sencilla consiste en:
- Inicializar una variable
int
llamadaresult
con un valor constante distinto de cero (por ejemplo, 17). - Para cada campo significativo
f
del objeto, calcular un código hashc
y combinarlo enresult
usandoresult = 37 * result + c;
. - Devolver
result
.
8. Sobrescribir ToString: Representación Concisa e Informativa
El método toString se invoca automáticamente cuando su objeto se pasa a println, al operador de concatenación de cadenas (+) o, desde la versión 1.4, a assert.
Importancia de ToString. Sobrescribir el método toString
proporciona una representación en cadena concisa e informativa de un objeto, haciendo que la clase sea más agradable de usar. Esto es especialmente útil para depuración y registro.
Contenido de ToString. Cuando sea práctico, el método toString
debe devolver toda la información interesante contenida en el objeto. Si el objeto es grande o contiene estado que no se presta a una representación en cadena, debe devolverse un resumen.
Especificar Formato. Al implementar toString
, decida si especificar el formato del valor devuelto en la documentación. Especificar el formato proporciona una representación estándar y sin ambigüedades, pero limita la flexibilidad para cambios futuros. Si se especifica el formato, proporcione un constructor String o una fábrica estática que coincida.
9. Interfaz Cloneable: Implementar con Prudencia
La interfaz Cloneable fue concebida como una interfaz mixin (Ítem 16) para que los objetos anuncien que permiten clonarse.
Defectos de Cloneable. La interfaz Cloneable
carece de un método clone
, y el método clone
de Object
es protegido. Esto dificulta invocar el método clone
en un objeto solo porque implemente Cloneable
.
Contrato de Clone. El contrato general para el método clone
es débil, indicando que crea y devuelve una copia del objeto. Sin embargo, no especifica cómo debe hacerse esta copia ni si deben llamarse constructores.
Implementar Cloneable. Para implementar Cloneable
correctamente, una clase debe sobrescribir el método clone
con un método público que primero llame a super.clone
y luego corrija los campos que necesiten ajuste. Esto suele implicar copiar objetos mutables que forman la "estructura profunda" interna del objeto.
Alternativas a Cloneable. Una buena alternativa para copiar objetos es proporcionar un constructor de copia o una fábrica estática. Estos enfoques no dependen de un mecanismo de creación de objetos extralingüístico y arriesgado, y no entran en conflicto con el uso adecuado de campos finales.
10. Interfaz Comparable: Orden Natural
Al implementar Comparable, una clase indica que sus instancias tienen un orden natural.
Beneficios de Comparable. Implementar la interfaz Comparable
permite que una clase interopere con algoritmos genéricos e implementaciones de colecciones que dependen del orden, como Arrays.sort
y TreeSet
.
Contrato de CompareTo. El contrato general para el método compareTo
es similar al de equals
, requiriendo reflexividad, transitividad y simetría. Se recomienda encarecidamente que (x.compareTo(y)==0) == (x.equals(y))
.
Escribir un Método CompareTo. Al escribir un método compareTo
, compare campos de referencia de objetos invocando recursivamente compareTo
. Compare campos primitivos usando los operadores relacionales <
y >
. Si la clase tiene varios campos significativos, compárelos en orden de importancia.
11. Minimizar la Accesibilidad: Ocultamiento de Información
El factor más importante que distingue un módulo bien diseñado de uno mal diseñado es el grado en que el módulo oculta sus datos internos y otros detalles de implementación a otros módulos.
Beneficios del Ocultamiento de Información. El ocultamiento de información, o encapsulación, desacopla módulos, permitiendo que se desarrollen, prueben, optimicen y modifiquen de forma independiente. Esto acelera el desarrollo del sistema, facilita el mantenimiento, aumenta la reutilización de software y disminuye riesgos.
Niveles de Acceso. El lenguaje Java proporciona modificadores de acceso (private
, paquete, protected
y public
) para ayudar al ocultamiento de información. La regla general es hacer que cada clase o miembro sea lo menos accesible posible.
Campos Públicos. Las clases públicas rara vez, si acaso, deben tener campos públicos (a diferencia de métodos públicos). La excepción son campos públicos estáticos finales que contienen valores primitivos o referencias a objetos inmutables.
12. Favorecer la Inmutabilidad: Simplicidad y Seguridad en Hilos
Una clase inmutable es simplemente una clase cuyas instancias no pueden modificarse.
Ventajas de la Inmutabilidad. Las clases inmutables son más fáciles de diseñar, implementar y usar que las mutables. Son menos propensas a errores y más seguras.
Reglas para la Inmutabilidad. Para hacer una clase inmutable:
- No proporcione métodos que modifiquen el objeto (mutadores).
- Asegúrese de que ningún método pueda ser sobrescrito (haga la clase final o use constructores privados y fábricas estáticas).
- Declare todos los campos como finales.
- Declare todos los campos como privados.
- Asegure acceso exclusivo a cualquier componente mutable (haga copias defensivas).
Desventajas de la Inmutabilidad. La única desventaja real de las clases inmutables es que requieren un objeto separado para cada valor distinto. Para manejar esto, proporcione constantes públicas estáticas finales para valores usados frecuentemente y considere ofrecer una clase compañera mutable pública.
Última actualización:
FAQ
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
hashCode
when you overrideequals
to 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.
Reseñas
Effective Java es considerado una lectura esencial para los desarrolladores de Java, reconocido por sus explicaciones claras y consejos prácticos. Los lectores valoran la experiencia de Bloch y el enfoque del libro en cómo escribir código de alta calidad y fácil mantenimiento. Muchos lo consideran una obra imprescindible tanto para principiantes como para programadores experimentados. El libro aborda diversas características de Java, mejores prácticas y errores comunes. Aunque algunos encuentran ciertas secciones desactualizadas o demasiado avanzadas, la mayoría coincide en que mejora significativamente las habilidades de programación y la comprensión de las complejidades de Java.
Similar Books









