Key Takeaways
1. Static Factories Over Constructors: Naming, Control, and Flexibility
Providing a static factory method instead of a public constructor has both advantages and disadvantages.
Named Advantages. Static factory methods offer descriptive names, unlike constructors, enhancing code readability and usability. This is especially useful when constructor parameters don't clearly indicate the object being created. For example, a method named BigInteger.probablePrime()
is more descriptive than a constructor BigInteger(int, int, Random)
.
Instance Control. Static factories aren't obligated to create new objects on each invocation, allowing immutable classes to cache instances and avoid unnecessary object creation. This can significantly improve performance, especially for frequently requested objects. The Boolean.valueOf(boolean)
method exemplifies this, as it always returns the same Boolean.TRUE
or Boolean.FALSE
instances.
Subtype Flexibility. Static factories can return objects of any subtype of their return type, enabling APIs to return objects without making their classes public. This promotes a compact API and allows for varying the returned class based on input parameters or even across different releases, enhancing maintainability and flexibility. This is the basis of service provider frameworks like the Java Cryptography Extension (JCE).
2. Singleton Implementation: Private Constructors and Public Access
A singleton is simply a class that is instantiated exactly once [Gamma98, p. 127].
Ensuring Uniqueness. Singletons, representing unique system components, are enforced by making the constructor private and providing a public static member for access. This guarantees only one instance exists. There are two common approaches: using a final field or a static factory method.
Final Field Approach. The public static member is a final field initialized with the singleton instance. This approach is straightforward and makes it clear that the class is a singleton. For example:
public static final Elvis INSTANCE = new Elvis();
Static Factory Approach. A public static factory method returns the singleton instance. This offers flexibility to change the implementation without altering the API. For example:
public static Elvis getInstance() { return INSTANCE; }
Serialization Consideration. To maintain the singleton property during serialization, a readResolve
method must be provided to return the existing instance, preventing the creation of new instances upon deserialization. This ensures that the singleton remains unique even after serialization and deserialization.
3. Utility Classes: Enforcing Non-Instantiability
Such utility classes were not designed to be instantiated: An instance would be nonsensical.
Purpose of Utility Classes. Utility classes, grouping static methods and fields, serve to organize related functionalities on primitive values, arrays, or objects implementing specific interfaces. They are not designed for instantiation, as instances would be meaningless. Examples include java.lang.Math
and java.util.Arrays
.
Preventing Instantiation. To enforce non-instantiability, a simple idiom involves including a single, explicit private constructor. This prevents the compiler from generating a public, parameterless default constructor, ensuring that the class cannot be instantiated from outside.
Side Effects. This idiom also prevents subclassing, as subclasses would lack an accessible constructor to invoke the superclass constructor. It's wise to include a comment explaining the purpose of the private constructor, as it might seem counterintuitive.
4. Object Reuse: Efficiency and Immutability
It is often appropriate to reuse a single object instead of creating a new functionally equivalent object each time it is needed.
Benefits of Reuse. Reusing objects, especially immutable ones, is more efficient and stylish than creating new ones repeatedly. This is because object creation and garbage collection can be expensive, especially for heavyweight objects. For example, using String s = "silly";
is better than String s = new String("silly");
Immutable Objects. Immutable objects can always be reused safely. For example, Boolean.valueOf(String)
is preferable to Boolean(String)
because the former may reuse existing instances.
Mutable Objects. Mutable objects can be reused if they are known not to be modified. For example, using static initialization to create Calendar
and Date
instances once, rather than creating them every time a method is invoked, can significantly improve performance.
5. Memory Management: Eliminating Obsolete References
An obsolete reference is simply a reference that will never be dereferenced again.
Memory Leaks. Memory leaks in garbage-collected languages occur when object references are unintentionally retained, preventing garbage collection. This can lead to reduced performance, increased memory footprint, and even OutOfMemoryError
.
Identifying Memory Leaks. A common source of memory leaks is classes that manage their own memory, such as a Stack
class. When an element is freed, any object references contained in the element should be nulled out.
Solutions. To prevent memory leaks, null out references once they become obsolete. Also, be mindful of caches, which can retain object references long after they become irrelevant. Use WeakHashMap
for caches where entries are relevant only as long as there are references to their keys outside the cache.
6. Equals Contract: Reflexivity, Symmetry, and Transitivity
The equals method implements an equivalence relation.
Equivalence Relation. When overriding the equals
method, it's crucial to adhere to its general contract, which defines an equivalence relation. This contract includes reflexivity (x.equals(x) must return true), symmetry (x.equals(y) must return true if and only if y.equals(x) returns true), and transitivity (if x.equals(y) and y.equals(z) are true, then x.equals(z) must be true).
Symmetry Violation. Violating symmetry can lead to unpredictable behavior when objects are used in collections. For example, a CaseInsensitiveString
class that attempts to interoperate with ordinary strings can violate symmetry.
Transitivity Violation. Violating transitivity can occur when extending an instantiable class and adding an aspect that affects equals
comparisons. To avoid this, favor composition over inheritance.
Non-nullity. The equals
method must return false for any non-null reference value x when x.equals(null) is invoked. The instanceof
operator handles this automatically.
7. HashCode Override: Consistency with Equals
You must override hashCode in every class that overrides equals.
Importance of HashCode. Overriding hashCode
is essential when overriding equals
to ensure that equal objects have equal hash codes. Failure to do so violates the general contract for Object.hashCode
and prevents the class from functioning properly in hash-based collections like HashMap
and HashSet
.
Legal but Poor Hash Function. A trivial hashCode
method that always returns the same value is legal but results in poor performance for hash tables, as all objects hash to the same bucket.
Recipe for a Good Hash Function. A good hash function tends to produce unequal hash codes for unequal objects. A simple recipe involves:
- Initializing an
int
variableresult
to a constant nonzero value (e.g., 17). - For each significant field
f
in the object, compute a hash codec
and combine it intoresult
usingresult = 37 * result + c;
. - Return
result
.
8. ToString Override: Concise and Informative Representation
The toString method is automatically invoked when your object is passed to println, the string concatenation operator (+), or, as of release 1.4, assert.
Importance of ToString. Overriding the toString
method provides a concise and informative string representation of an object, making the class more pleasant to use. This is especially useful for debugging and logging.
Content of ToString. When practical, the toString
method should return all of the interesting information contained in the object. If the object is large or contains state that is not conducive to string representation, a summary should be returned.
Specifying Format. When implementing toString
, decide whether to specify the format of the return value in the documentation. Specifying the format provides a standard, unambiguous representation but limits flexibility for future changes. If you specify the format, provide a matching String constructor or static factory.
9. Cloneable Interface: Implement Judiciously
The Cloneable interface was intended as a mixin interface (Item 16) for objects to advertise that they permit cloning.
Flaws of Cloneable. The Cloneable
interface lacks a clone
method, and Object
's clone
method is protected. This makes it difficult to invoke the clone
method on an object merely because it implements Cloneable
.
Clone Contract. The general contract for the clone
method is weak, stating that it creates and returns a copy of the object. However, it does not specify how this copy should be made or whether constructors should be called.
Implementing Cloneable. To implement Cloneable
properly, a class must override the clone
method with a public method that first calls super.clone
and then fixes any fields that need fixing. This typically means copying any mutable objects that comprise the internal "deep structure" of the object.
Alternatives to Cloneable. A fine approach to object copying is to provide a copy constructor or static factory. These approaches do not rely on a risk-prone extralinguistic object creation mechanism and do not conflict with the proper use of final fields.
10. Comparable Interface: Natural Ordering
By implementing Comparable, a class indicates that its instances have a natural ordering.
Benefits of Comparable. Implementing the Comparable
interface allows a class to interoperate with generic algorithms and collection implementations that depend on ordering, such as Arrays.sort
and TreeSet
.
CompareTo Contract. The general contract for the compareTo
method is similar to that of the equals
method, requiring reflexivity, transitivity, and symmetry. It is strongly recommended that (x.compareTo(y)==0) == (x.equals(y))
.
Writing a CompareTo Method. When writing a compareTo
method, compare object reference fields by invoking the compareTo
method recursively. Compare primitive fields using the relational operators <
and >
. If a class has multiple significant fields, compare them in order of significance.
11. Minimize Accessibility: Information Hiding
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 and other implementation details from other modules.
Benefits of Information Hiding. Information hiding, or encapsulation, decouples modules, allowing them to be developed, tested, optimized, and modified independently. This speeds up system development, eases maintenance, increases software reuse, and decreases risk.
Access Levels. The Java programming language provides access modifiers (private
, package-private, protected
, and public
) to aid information hiding. The rule of thumb is to make each class or member as inaccessible as possible.
Public Fields. Public classes should rarely, if ever, have public fields (as opposed to public methods). The exception is for public static final fields containing primitive values or references to immutable objects.
12. Favor Immutability: Simplicity and Thread Safety
An immutable class is simply a class whose instances cannot be modified.
Advantages of Immutability. Immutable classes are easier to design, implement, and use than mutable classes. They are less prone to error and are more secure.
Rules for Immutability. To make a class immutable:
- Don't provide any methods that modify the object (mutators).
- Ensure that no methods may be overridden (make the class final or use private constructors and static factories).
- Make all fields final.
- Make all fields private.
- Ensure exclusive access to any mutable components (make defensive copies).
Disadvantages of Immutability. The only real disadvantage of immutable classes is that they require a separate object for each distinct value. To cope with this, provide public static final constants for frequently used values and consider providing a public mutable companion class.
Last updated:
Review Summary
Effective Java is highly regarded as an essential read for Java developers, praised for its clear explanations and practical advice. Readers appreciate Bloch's expertise and the book's focus on writing high-quality, maintainable code. Many consider it a must-read for both beginners and experienced programmers. The book covers various Java features, best practices, and common pitfalls. While some find certain sections outdated or too advanced, most agree that it significantly improves coding skills and understanding of Java's intricacies.
Similar Books
Download PDF
Download EPUB
.epub
digital book format is ideal for reading ebooks on phones, tablets, and e-readers.