Chapter 2: Methods Common to All Objects

effective-java object-methods equals hashcode tostring comparable clone java best-practices

Book: Effective Java, 3rd Edition β€” Joshua Bloch
Status: 🟩 Complete
Difficulty: Medium-Hard
Items: 10-14 (5 items)
Time to complete: ~45 min


Overview

Object is the root of every Java class hierarchy. While most of its methods have default implementations, many of those defaults are wrong for value-semantic classes β€” classes where instances represent values rather than identities. This chapter covers the general contracts that equals, hashCode, toString, clone, and Comparable.compareTo must honor. Violating these contracts produces subtle, hard-to-diagnose bugs: collections that lose elements, sorted sets that behave like unsorted sets, and objects that print opaquely in logs. Understanding and implementing these methods correctly is table-stakes knowledge for any professional Java developer.


Items

Item 10: Obey the General Contract When Overriding equals

The Problem

The default equals from Object implements reference equality (this == other) β€” two references are equal only if they point to the same object. This is correct for many classes (threads, connections), but wrong for value classes (classes representing a value like Point, Money, PhoneNumber). On the other hand, a badly implemented equals breaks the general contract and causes collections to behave incorrectly.

// BAD β€” violates symmetry: a.equals(b) != b.equals(a)
public class CaseInsensitiveString {
    private final String s;
    public CaseInsensitiveString(String s) { this.s = Objects.requireNonNull(s); }
 
    @Override public boolean equals(Object o) {
        if (o instanceof CaseInsensitiveString cis)
            return s.equalsIgnoreCase(cis.s);
        if (o instanceof String) // BAD: interoperates with String but String doesn't know about us
            return s.equalsIgnoreCase((String) o);
        return false;
    }
}
// cis.equals("polish") returns true
// "polish".equals(cis)   returns false β€” ASYMMETRY!
// BAD β€” violates transitivity: a.equals(b) and b.equals(c) but !a.equals(c)
public class Point {
    final int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }
 
    @Override public boolean equals(Object o) {
        if (!(o instanceof Point)) return false;
        Point p = (Point) o;
        return p.x == x && p.y == y;
    }
}
 
public class ColorPoint extends Point {
    private final Color color;
    ColorPoint(int x, int y, Color color) { super(x, y); this.color = color; }
 
    // Attempting to add color to equals while preserving symmetry breaks transitivity:
    @Override public boolean equals(Object o) {
        if (!(o instanceof Point)) return false;
        if (!(o instanceof ColorPoint)) return o.equals(this); // symmetric with Point, but...
        return super.equals(o) && ((ColorPoint) o).color == color;
    }
}
// ColorPoint(1,2,RED).equals(Point(1,2))       β†’ true  (falls back to Point.equals)
// Point(1,2).equals(ColorPoint(1,2,BLUE))       β†’ true  (Point.equals ignores color)
// ColorPoint(1,2,RED).equals(ColorPoint(1,2,BLUE)) β†’ false
// Not transitive!

The Solution

The five properties of the equals contract:

  1. Reflexive: x.equals(x) must be true.
  2. Symmetric: x.equals(y) iff y.equals(x).
  3. Transitive: if x.equals(y) and y.equals(z), then x.equals(z).
  4. Consistent: repeated calls return the same result if the objects are not modified.
  5. Non-null: x.equals(null) must be false.

There is no way to extend an instantiable class and add a value component while preserving the equals contract (the Liskov Substitution Principle makes this impossible). The fix is composition over inheritance:

// GOOD β€” composition with a view method, no inheritance
public class ColorPoint {
    private final Point point;
    private final Color color;
 
    public ColorPoint(int x, int y, Color color) {
        point = new Point(x, y);
        this.color = Objects.requireNonNull(color);
    }
 
    public Point asPoint() { return point; } // view method
 
    @Override public boolean equals(Object o) {
        if (!(o instanceof ColorPoint cp)) return false;
        return cp.point.equals(point) && cp.color.equals(color);
    }
 
    @Override public int hashCode() {
        return Objects.hash(point, color);
    }
}

The canonical high-quality equals implementation:

// GOOD β€” correct equals for a value class
public final class PhoneNumber {
    private final short areaCode, prefix, lineNum;
 
    public PhoneNumber(int areaCode, int prefix, int lineNum) {
        this.areaCode = rangeCheck(areaCode, 999, "area code");
        this.prefix   = rangeCheck(prefix,   999, "prefix");
        this.lineNum  = rangeCheck(lineNum,  9999, "line num");
    }
 
    @Override public boolean equals(Object o) {
        if (o == this)                    return true;   // optimization: same reference
        if (!(o instanceof PhoneNumber))  return false;  // handles null + type check
        PhoneNumber pn = (PhoneNumber) o;
        return pn.lineNum  == lineNum
            && pn.prefix   == prefix
            && pn.areaCode == areaCode;
    }
    // Always override hashCode when you override equals β€” see Item 11
}

Recipe for a correct equals:

  1. Check if (o == this) return true; β€” performance shortcut.
  2. Check if (!(o instanceof MyClass)) return false; β€” handles null, wrong type.
  3. Cast o to MyClass.
  4. Compare all significant fields: use == for primitives, Float.compare/Double.compare for floats/doubles, Objects.equals for objects (null-safe), and Arrays.equals for arrays.
  5. Ask: is it symmetric? transitive? consistent?

When to Apply

Override equals when a class has a notion of logical equality different from object identity β€” value classes like Integer, String, LocalDate, Money, Point.

When NOT to Apply

  • When object identity is the correct equality semantics (threads, connections, services).
  • When a superclass has already overridden equals correctly.
  • For non-public implementation classes that are never compared by value.

Java 17 Update

Pattern matching for instanceof (Java 16+) simplifies the cast inside equals:

@Override public boolean equals(Object o) {
    if (o == this) return true;
    if (!(o instanceof PhoneNumber pn)) return false; // pattern variable pn
    return pn.lineNum == lineNum && pn.prefix == prefix && pn.areaCode == areaCode;
}

Records (Java 16+) auto-generate a correct equals based on all record components β€” you should not override it unless you have a very specific reason:

public record PhoneNumber(int areaCode, int prefix, int lineNum) {}
// equals, hashCode, toString all generated correctly
PhoneNumber p1 = new PhoneNumber(707, 867, 5309);
PhoneNumber p2 = new PhoneNumber(707, 867, 5309);
p1.equals(p2); // true β€” value semantics, auto-generated

Item 11: Always Override hashCode When You Override equals

The Problem

The hashCode contract requires: equal objects must have equal hash codes. If you override equals without overriding hashCode, objects that are logically equal will have different hash codes, completely breaking hash-based collections (HashMap, HashSet, LinkedHashMap, Hashtable).

// BAD β€” equals overridden, hashCode not: HashMap lookup fails!
Map<PhoneNumber, String> map = new HashMap<>();
map.put(new PhoneNumber(707, 867, 5309), "Jenny");
 
// This returns null instead of "Jenny" β€” different hash bucket!
String name = map.get(new PhoneNumber(707, 867, 5309));
System.out.println(name); // null β€” WRONG!

The Solution

Always override hashCode when you override equals. The hash code should incorporate all fields used in equals, in a consistent, deterministic manner.

// GOOD β€” correct hashCode using Objects.hash (convenient but slightly slower due to varargs)
@Override public int hashCode() {
    return Objects.hash(areaCode, prefix, lineNum);
}
 
// GOOD β€” manually computed hashCode (more control, avoids varargs autoboxing)
@Override public int hashCode() {
    int result = Short.hashCode(areaCode);
    result = 31 * result + Short.hashCode(prefix);
    result = 31 * result + Short.hashCode(lineNum);
    return result;
}

Why 31? It is an odd prime, which avoids losing information on overflow, and 31 * i == (i << 5) - i β€” the JIT often optimizes this to a shift-and-subtract instruction.

Lazy initialization for expensive hash codes:

// GOOD β€” lazily compute and cache the hash code for immutable objects with expensive computation
public final class BigValueClass {
    private final String data; // expensive to hash
    private int hashCode; // 0 is the default β€” check if computed
 
    @Override public int hashCode() {
        int result = hashCode;
        if (result == 0) {
            result = Objects.hash(data);
            hashCode = result;
        }
        return result;
    }
}

The full hashCode contract:

  1. Within a single execution of a program, hashCode() must consistently return the same value, provided no information used in equals comparisons is modified.
  2. If two objects are equal according to equals, they must have the same hashCode.
  3. If two objects are not equal according to equals, it is not required that they have different hash codes β€” but different hash codes improve hash table performance.

What not to do:

// TERRIBLE β€” all instances return the same hash code, defeating hash tables (O(n) lookup)
@Override public int hashCode() { return 42; }
 
// BAD β€” excludes fields used in equals, breaking the contract
@Override public int hashCode() {
    return Objects.hash(areaCode); // lineNum and prefix used in equals but not here
}

When to Apply

Whenever you override equals. No exceptions.

Java 17 Update

Records (Java 16+) auto-generate hashCode from all record components β€” in sync with the auto-generated equals. No manual implementation needed for records.

Pattern matching does not affect hashCode directly, but Objects.hash and Objects.hashCode (Java 7+) remain the preferred utility methods. No new language feature changes the hashCode contract.


Item 12: Always Override toString

The Problem

The default toString() from Object returns ClassName@hexHashCode (e.g., PhoneNumber@163b91). This is useless in debugging, logging, and error messages. When a PhoneNumber is printed in a log, you see PhoneNumber@163b91 β€” zero actionable information.

// BAD β€” relying on default toString
System.out.println("Failed to connect to " + phoneNumber); // "Failed to connect to PhoneNumber@163b91"

The Solution

Override toString to return all significant information about the object in a concise, human-readable format.

// GOOD β€” informative toString
public final class PhoneNumber {
    private final short areaCode, prefix, lineNum;
 
    @Override public String toString() {
        return String.format("(%03d) %03d-%04d", areaCode, prefix, lineNum);
        // Output: (707) 867-5309
    }
}

Documenting the format (for value classes):

If you specify the format in the Javadoc, provide a static factory or constructor that parses it β€” so the format is truly a reliable external representation:

/**
 * Returns a string representation of this phone number.
 * The string consists of twelve characters whose format is
 * "(XXX) XXX-XXXX", where X is a decimal digit.
 * ...
 */
@Override public String toString() {
    return String.format("(%03d) %03d-%04d", areaCode, prefix, lineNum);
}
 
// Paired factory for round-tripping
public static PhoneNumber of(String s) { /* parse (XXX) XXX-XXXX */ }

If you do NOT specify a format, document that the format is subject to change:

/**
 * Returns a brief description of this potion. The exact details
 * of the representation are unspecified and subject to change,
 * but the following may be regarded as typical:
 * "[Potion #9: type=love, smell=turpentine, look=yucky]"
 */
@Override public String toString() { ... }

Provide programmatic access to all fields returned by toString:

// GOOD β€” callers don't have to parse the string to get components
public int getAreaCode() { return areaCode; }
public int getPrefix()   { return prefix; }
public int getLineNum()  { return lineNum; }

When to Apply

  • All concrete classes, especially value classes and exception classes.
  • Any class whose instances will appear in logs, error messages, or debug output.

When NOT to Apply

  • static utility classes (they have no instances).
  • Enum types (Java already provides a good toString β€” the constant name).
  • Classes where Object.toString is already overridden in a superclass correctly.

Java 17 Update

Records (Java 16+) auto-generate toString that prints all components:

record Point(int x, int y) {}
System.out.println(new Point(3, 4)); // "Point[x=3, y=4]"

Text blocks (Java 15+) make complex multi-field toString implementations more readable:

@Override public String toString() {
    return """
            Order{id=%d, customer='%s', total=%.2f, status=%s}
            """.formatted(id, customer, total, status).strip();
}

String.formatted() (Java 15+) is an instance method alternative to String.format():

@Override public String toString() {
    return "(%03d) %03d-%04d".formatted(areaCode, prefix, lineNum);
}

Item 13: Override clone Judiciously

The Problem

Cloneable is a broken contract. Despite being a marker interface (no methods), implementing it changes the behavior of Object.clone(), which returns a field-by-field copy of the object. The contract is specified in a confusing place (the Object javadoc, not the interface), is not enforced at compile time, and has serious shortcomings:

  1. Shallow copy by default β€” object references are copied, not the referenced objects. This breaks invariants for classes with mutable fields.
  2. No checked exceptions from clone β€” but the superclass (Object) throws CloneNotSupportedException, requiring casts and try-catch.
  3. Bypasses constructors β€” object creation happens without calling any constructor, making it impossible to enforce invariants or count instances.
  4. Arrays are fine, but mutable containers are not β€” a cloned Stack would share its elements array with the original.
// BAD β€” shallow clone of a class with mutable state
public class Stack implements Cloneable {
    private Object[] elements;
    private int size = 0;
 
    @Override public Stack clone() {
        try {
            return (Stack) super.clone(); // BAD: elements array is shared!
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(); // never happens
        }
    }
}
// original.pop() removes from the same array as clone.pop() β€” catastrophic!

The Solution

For the uncommon cases where clone must be implemented correctly:

// GOOD β€” deep clone for a class with an internal array
@Override public Stack clone() {
    try {
        Stack result = (Stack) super.clone();
        result.elements = elements.clone(); // array clone is always correct
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

For hash tables or other classes with internal linked structure, a recursive clone approach is needed:

// GOOD β€” deep clone for a hash table with chained buckets
public class HashTable implements Cloneable {
    private Entry[] buckets;
 
    private static class Entry {
        final Object key;
        Object value;
        Entry next;
        Entry(Object key, Object value, Entry next) { ... }
 
        // Iteratively clone the chain (recursive version can overflow stack)
        Entry deepCopy() {
            Entry result = new Entry(key, value, null);
            Entry p = result;
            for (Entry e = next; e != null; e = e.next) {
                p.next = new Entry(e.key, e.value, null);
                p = p.next;
            }
            return result;
        }
    }
 
    @Override public HashTable clone() {
        try {
            HashTable result = (HashTable) super.clone();
            result.buckets = new Entry[buckets.length];
            for (int i = 0; i < buckets.length; i++)
                if (buckets[i] != null)
                    result.buckets[i] = buckets[i].deepCopy();
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

The preferred alternative: a copy constructor or copy factory

// BEST β€” copy constructor: no Cloneable machinery, constructor enforces invariants, works with finals
public final class Stack {
    private Object[] elements;
    private int size;
 
    // Copy constructor
    public Stack(Stack source) {
        this.elements = source.elements.clone();
        this.size     = source.size;
    }
 
    // Copy factory (static)
    public static Stack copyOf(Stack source) {
        return new Stack(source);
    }
}

Copy constructors and copy factories are superior to Cloneable/clone because:

  • They go through the normal constructor β€” invariants are enforced.
  • They are not constrained by a flawed interface.
  • They can accept an argument of any type the class can accommodate (conversion constructors/factories, e.g., new TreeSet<>(hashSet)).
  • They work with final fields (clone often cannot).
  • They do not throw a spurious checked exception.

When to Apply

  • When implementing array-like containers that extend Cloneable in a class hierarchy.
  • For performance-critical code where clone of arrays is the right tool (arrays implement Cloneable and array.clone() is a well-defined, correct operation).

When NOT to Apply

  • For new classes β€” use copy constructors or copy factories instead.
  • Classes with final mutable fields β€” cannot be cloned correctly.
  • Classes that must enforce invariants during construction.

Java 17 Update

Records (Java 16+) do not implement Cloneable and there is no reason to add it. To β€œcopy” a record with a field changed, use the pattern:

record Point(int x, int y) {}
 
// "With" pattern β€” create a modified copy
Point p1 = new Point(1, 2);
Point p2 = new Point(p1.x(), 42); // manually, no clone needed

Java does not yet have native with expressions (proposed for a future version), but records make immutable copying natural and cheap. Lombok’s @With annotation generates withFieldName(newValue) methods for records and classes.


Item 14: Consider Implementing Comparable

The Problem

Comparable<T> has a single method compareTo(T o) that defines a natural ordering for instances of a class. Classes that implement Comparable interoperate with: TreeSet, TreeMap, Arrays.sort(), Collections.sort(), PriorityQueue, and any algorithm that relies on natural ordering. Not implementing it means losing all this infrastructure for free.

The compareTo method must obey a general contract similar to equals:

  1. Antisymmetric: sign(x.compareTo(y)) == -sign(y.compareTo(x)).
  2. Transitive: if x.compareTo(y) > 0 and y.compareTo(z) > 0, then x.compareTo(z) > 0.
  3. Consistent with equals (strongly recommended): x.compareTo(y) == 0 implies x.equals(y).

Violating consistency with equals produces unexpected behavior in sorted collections:

// BAD β€” BigDecimal inconsistency with equals
Set<BigDecimal> hashSet = new HashSet<>();
hashSet.add(new BigDecimal("1.0"));
hashSet.add(new BigDecimal("1.00"));
System.out.println(hashSet.size()); // 2 β€” equals treats "1.0" != "1.00"
 
Set<BigDecimal> treeSet = new TreeSet<>();
treeSet.add(new BigDecimal("1.0"));
treeSet.add(new BigDecimal("1.00"));
System.out.println(treeSet.size()); // 1 β€” compareTo treats them equal!
// Same objects, different set behavior depending on Set implementation!

The Solution

Implement Comparable<T> for any value class with an obvious natural ordering. Use Comparator.comparingInt() / Comparator.comparing() / comparator chaining to build comparators correctly:

// GOOD β€” correct compareTo using Integer.compare (never subtract! can overflow)
public final class PhoneNumber implements Comparable<PhoneNumber> {
    private final short areaCode, prefix, lineNum;
 
    @Override
    public int compareTo(PhoneNumber pn) {
        int result = Short.compare(areaCode, pn.areaCode); // most significant field first
        if (result == 0) {
            result = Short.compare(prefix, pn.prefix);
            if (result == 0)
                result = Short.compare(lineNum, pn.lineNum);
        }
        return result;
    }
}

Better: use Comparator construction methods (Java 8+):

// GOOD β€” Comparator-based compareTo: concise, correct, composable
public final class PhoneNumber implements Comparable<PhoneNumber> {
    private static final Comparator<PhoneNumber> COMPARATOR =
            Comparator.comparingInt((PhoneNumber pn) -> pn.areaCode)
                      .thenComparingInt(pn -> pn.prefix)
                      .thenComparingInt(pn -> pn.lineNum);
 
    @Override
    public int compareTo(PhoneNumber pn) {
        return COMPARATOR.compare(this, pn);
    }
}

The subtraction anti-pattern β€” never do this:

// TERRIBLE β€” subtraction-based comparison: can overflow and return wrong sign!
static Comparator<Object> hashCodeOrder = new Comparator<>() {
    public int compare(Object o1, Object o2) {
        return o1.hashCode() - o2.hashCode(); // OVERFLOW: if o1=Integer.MIN_VALUE, o2=0, result is positive!
    }
};
 
// CORRECT alternatives
static Comparator<Object> hashCodeOrder =
    Comparator.comparingInt(Object::hashCode); // GOOD
 
// Or use Integer.compare
static Comparator<Object> hashCodeOrder = (o1, o2) ->
    Integer.compare(o1.hashCode(), o2.hashCode()); // GOOD

Comparator for sorting by multiple fields in different orders:

// Sort employees: by department ascending, then salary descending, then name ascending
Comparator<Employee> employeeOrder =
    Comparator.comparing(Employee::getDepartment)
              .thenComparing(Comparator.comparingDouble(Employee::getSalary).reversed())
              .thenComparing(Employee::getName);

When to Apply

  • Any value class with an obvious natural ordering: String, Integer, LocalDate, BigDecimal.
  • When instances will be stored in TreeSet/TreeMap or sorted via Arrays.sort().
  • When the class is part of a domain model where ordering is a business concept (order priority, task scheduling).

When NOT to Apply

  • When there is no single natural ordering β€” use named Comparator constants instead.
  • When the ordering would be inconsistent with equals β€” document this explicitly (see BigDecimal).

Java 17 Update

Records (Java 16+) do not auto-implement Comparable β€” you must add it manually if needed:

public record Employee(String name, int salary) implements Comparable<Employee> {
    @Override
    public int compareTo(Employee other) {
        return Comparator.comparing(Employee::name)
                         .thenComparingInt(Employee::salary)
                         .compare(this, other);
    }
}

Switch expressions (Java 14+) can simplify comparators involving enums with non-natural ordering:

// Map an enum to an integer rank for comparison
int rank = switch (priority) {
    case CRITICAL -> 0;
    case HIGH     -> 1;
    case MEDIUM   -> 2;
    case LOW      -> 3;
};

Pattern matching for instanceof (Java 16+) can simplify compareTo implementations that need type checking in heterogeneous hierarchies, though this is uncommon.


Interview Questions & Exercises

Q1: What are the five properties of the equals contract, and what breaks each one?

Context: Classic senior Java question; often followed by β€œgive me an example of a violation.”
Answer:

PropertyMeaningCommon Violation
Reflexivex.equals(x) == trueAlmost never broken accidentally
Symmetricx.equals(y) == y.equals(x)Mixed-type comparison: CaseInsensitiveString.equals(String)
Transitivea==b, b==c β†’ a==cAdding a value component to a subclass while trying to maintain symmetry
ConsistentRepeated calls return same resultUnreliable resources in equals (URL β†’ IP lookup)
Non-nullx.equals(null) == falseMissing null check before cast

The hardest to preserve is transitivity when adding value components to a subclass. There is no fix that preserves the contract while using inheritance β€” use composition instead.

Follow-up: β€œWhy does instanceof in equals handle the null check for you?” β€” Because null instanceof Foo is always false, so the if (!(o instanceof Foo)) check catches both the null case and the wrong-type case in a single expression.


Q2: What happens if you override equals but not hashCode? Walk me through a concrete failure.

Context: Asked universally at Java interviews; tests understanding of the hashCode/equals contract.
Answer:
The general contract: if a.equals(b), then a.hashCode() == b.hashCode().

Concrete failure with HashMap:

Map<PhoneNumber, String> map = new HashMap<>();
PhoneNumber key1 = new PhoneNumber(707, 867, 5309);
map.put(key1, "Jenny");
 
PhoneNumber key2 = new PhoneNumber(707, 867, 5309); // logically equal to key1
System.out.println(key1.equals(key2)); // true (we overrode equals correctly)
System.out.println(map.get(key2));     // null β€” WRONG!

Why: HashMap.get() first computes key2.hashCode(). Since hashCode is inherited from Object (reference-based), key1.hashCode() != key2.hashCode(). The map looks in the wrong bucket and finds nothing. equals is never even called for elements in the wrong bucket.

Also breaks: HashSet.contains(), LinkedHashMap, Hashtable, any hash-based structure.

Follow-up: β€œIs the reverse true β€” if hashCodes are equal, must equals return true?” β€” No. Hash collisions are allowed (and common). Multiple unequal objects can have the same hash code. The contract is one-directional: equals β†’ same hashCode. Not: same hashCode β†’ equals.


Q3: Explain the subtraction anti-pattern in compareTo. Why is subtraction wrong?

Context: Regularly asked at mid-to-senior level; often used as a β€œgotcha” in code review questions.
Answer:
Using subtraction to compare integers for ordering is wrong because of integer overflow:

// WRONG β€” can overflow
return o1.hashCode() - o2.hashCode();
// If o1.hashCode() = Integer.MAX_VALUE (2_147_483_647)
// and o2.hashCode() = -1
// result = 2_147_483_647 - (-1) = 2_147_483_648 β†’ overflows to Integer.MIN_VALUE (negative!)
// Comparator says o1 < o2, which is WRONG (positive minus negative should be positive)

This can cause Arrays.sort() and TreeSet to produce incorrect orderings that appear random and are very hard to debug.

Correct alternatives:

  1. Integer.compare(o1.hashCode(), o2.hashCode()) β€” dedicated comparison method, no overflow.
  2. Comparator.comparingInt(Object::hashCode) β€” built-in comparator construction.
  3. For all numeric types: use Type.compare(a, b) (e.g., Long.compare, Double.compare).

Follow-up: β€œWhat about for negative numbers only β€” is subtraction safe then?” β€” Only if you can prove with absolute certainty that overflow is impossible (both values are always non-negative and their difference never exceeds Integer.MAX_VALUE). In practice, always use Integer.compare β€” it is just as fast and always correct.


Q4: What is wrong with the Cloneable interface and why is clone hard to implement correctly?

Context: Tests deep Java knowledge; relevant in interviews for platform/framework engineering roles.
Answer:
Cloneable is a broken design:

  1. Specifies no method β€” it is a marker interface that silently changes Object.clone() behavior. The method it activates (Object.clone()) is protected, not public. To expose clone, each class must override it.
  2. Bypasses constructors β€” super.clone() creates a new instance without calling any constructor. Invariant enforcement and instance counting break.
  3. Shallow by default β€” only copies field references, not the objects they point to. Mutable fields are shared between original and clone β€” aliasing bugs.
  4. Cannot work with final fields β€” if a field is final, you cannot assign to it in the clone (since you are not in a constructor).
  5. Spurious checked exception β€” CloneNotSupportedException must be caught even when it can never be thrown.
  6. No contract enforcement β€” the compiler cannot check that a class implementing Cloneable provides a correct clone().

Better alternatives:

  • Copy constructor: new Foo(Foo source) β€” uses normal constructor, works with finals, no checked exception.
  • Copy factory: static Foo copyOf(Foo source) β€” same benefits, more flexible.

Arrays are a special case: array.clone() is always correct and is the idiomatic way to copy arrays.

Follow-up: β€œWhen would you still use Cloneable?” β€” When extending a class hierarchy that already uses it (like the Stack example in the book), or for performance-sensitive array-like container cloning. It is essentially a legacy mechanism.


Q5: What does β€œconsistent with equals” mean for compareTo, and what happens when it is violated?

Context: Tests nuanced understanding; often arises in TreeMap/TreeSet usage questions.
Answer:
Consistent with equals: compareTo returns 0 if and only if equals returns true. More precisely: (x.compareTo(y) == 0) == (x.equals(y)) should hold for all x, y.

When violated, sorted collections behave differently from hash-based collections for the same data:

// BigDecimal violates this β€” compareTo("1.0", "1.00") == 0, but equals("1.0", "1.00") == false
Set<BigDecimal> hashSet = new HashSet<>();
hashSet.add(new BigDecimal("1.0"));
hashSet.add(new BigDecimal("1.00"));
hashSet.size(); // 2 β€” uses equals; treats as different
 
Set<BigDecimal> treeSet = new TreeSet<>();
treeSet.add(new BigDecimal("1.0"));
treeSet.add(new BigDecimal("1.00"));
treeSet.size(); // 1 β€” uses compareTo; treats as same

This is intentional for BigDecimal (compareTo ignores trailing zeros; equals does not), but it means TreeSet and HashSet behave differently for the same data. The Javadoc documents this inconsistency.

When you are designing a class, avoid this inconsistency unless you have a strong reason β€” it creates confusion and subtle bugs.

Follow-up: β€œHow do TreeMap and HashMap use comparison differently?” β€” HashMap uses equals and hashCode. TreeMap uses only compareTo (or a provided Comparator) β€” equals is not called at all. So two objects can be β€œthe same key” in TreeMap (compareTo == 0) but β€œdifferent keys” in HashMap (equals == false).


Q6: When should you override toString, and what should it include?

Context: Practical question; tested at all levels. Often followed by format documentation questions.
Answer:
Override toString in every concrete class you write that does not inherit a good one from a superclass. Include all significant information about the instance β€” the same information used in equals.

A good toString:

  1. Is concise β€” not a page of noise.
  2. Is informative β€” identifies the object and its important state.
  3. Is readable by humans.
  4. Ideally is parseable β€” if you document the format, provide a matching factory/constructor.
  5. Provides programmatic access to all fields it exposes (so callers don’t have to parse the string).
// BAD β€” opaque: PhoneNumber@163b91
// GOOD β€” informative: (707) 867-5309
@Override public String toString() {
    return "(%03d) %03d-%04d".formatted(areaCode, prefix, lineNum);
}

For multi-field objects, records auto-generate toString like Point[x=3, y=4] β€” clean and complete.

Follow-up: β€œShould you always specify the format in Javadoc?” β€” Only if the class is a value class with a stable, well-defined string representation intended for serialization or display. If you specify it, commit to it β€” changing it breaks clients. If not stable, document that the format is unspecified.


Q7: How does pattern matching for instanceof (Java 16+) improve equals implementations?

Context: Tests awareness of modern Java; relevant for greenfield projects on Java 17+.
Answer:
Before Java 16, an equals implementation required an explicit cast after the instanceof check:

// Pre-Java 16
@Override public boolean equals(Object o) {
    if (o == this) return true;
    if (!(o instanceof PhoneNumber)) return false;
    PhoneNumber pn = (PhoneNumber) o; // redundant cast
    return pn.lineNum == lineNum ...;
}

With pattern matching for instanceof (Java 16+), the cast and binding happen simultaneously:

// Java 16+
@Override public boolean equals(Object o) {
    if (o == this) return true;
    if (!(o instanceof PhoneNumber pn)) return false; // pn is bound here
    return pn.lineNum == lineNum && pn.prefix == prefix && pn.areaCode == areaCode;
}

Benefits:

  • Eliminates the redundant cast.
  • pn is in scope only where the pattern matched (after the null/type check).
  • Code is more concise and easier to read.
  • The compiler can verify type safety more clearly.

For records specifically, equals is auto-generated and you should not write it manually at all.

Follow-up: β€œCan pattern matching be used for more complex equals logic?” β€” Yes. Future Java versions are expected to bring pattern matching for switch, which will enable expressing complex multi-type dispatch more cleanly. In Java 21+, sealed classes + switch patterns allow exhaustive matching across type hierarchies.


Q8: Implement a correct Comparator for sorting Employee objects by salary descending, then name ascending.

Context: Practical coding question testing knowledge of Comparator API.
Answer:

public record Employee(String name, double salary) {}
 
// Using Comparator composition (Java 8+)
Comparator<Employee> comp =
    Comparator.comparingDouble(Employee::salary).reversed()
              .thenComparing(Employee::name);
 
// Or as a static field
public record Employee(String name, double salary) implements Comparable<Employee> {
    private static final Comparator<Employee> NATURAL_ORDER =
        Comparator.comparingDouble(Employee::salary).reversed()
                  .thenComparing(Employee::name);
 
    @Override
    public int compareTo(Employee other) {
        return NATURAL_ORDER.compare(this, other);
    }
}
 
// Usage
List<Employee> employees = List.of(
    new Employee("Alice", 90_000),
    new Employee("Bob",   90_000),
    new Employee("Carol", 100_000)
);
employees.stream()
    .sorted(comp)
    .forEach(System.out::println);
// Output: Carol (100k), Alice (90k, name A before B), Bob (90k)

Key points:

  • Use .reversed() for descending, not negation of compareTo.
  • .thenComparing() chains secondary criteria.
  • Never subtract salaries (double precision and overflow issues).

Follow-up: β€œWhat if salary could be null?” β€” Use Comparator.comparingDouble(..., Comparator.nullsFirst(...)) or handle nulls explicitly. For double fields, Double.compare(a, b) handles NaN correctly.


Key Takeaways

  • Always override hashCode when you override equals β€” this is the single most consequential rule in this chapter. Failure breaks all hash-based collections silently.
  • The equals contract has five properties: reflexive, symmetric, transitive, consistent, and non-null. Transitivity is the hardest to preserve when adding value components to subclasses β€” the fix is composition, not inheritance.
  • There is no clean way to extend an instantiable class and add a value component while preserving the equals contract. Use composition with a view method.
  • Never subtract integers to implement comparison β€” use Integer.compare(), Long.compare(), etc. Subtraction can overflow and produce wrong ordering.
  • toString should include all significant state β€” if it is not informative, it does not help with debugging. For value classes, document or leave the format unspecified, and provide programmatic field access either way.
  • Cloneable is broken β€” it bypasses constructors, has shallow-copy defaults, does not work with final fields, and leaks implementation details. Prefer copy constructors or copy factories for new code.
  • Comparable gives you free sorting infrastructure β€” TreeSet, TreeMap, Arrays.sort, Collections.sort all use it. Implement it whenever there is a natural ordering.
  • compareTo should be consistent with equals β€” inconsistency (BigDecimal) causes TreeSet and HashSet to disagree on membership, which is a source of subtle bugs.
  • Records (Java 16+) auto-generate equals, hashCode, and toString correctly for all components β€” prefer records for simple value classes and avoid manual implementation.
  • Pattern matching for instanceof (Java 16+) simplifies the cast in equals β€” bind the variable and check type in a single expression.

Last Updated: 2026-05-10