Skip to content

Lesson 02 · Object Contracts & Common Patterns

Beyond the 1Z0-830 exam

The exam touches equals/hashCode (Module 05); here we go deeper into the contracts and the ordering trap with compareTo, then cover the three patterns you'll meet most in real Java and in AI-generated code: builder, factory method, and strategy.

Objectives

After this lesson you will be able to:

  • State and satisfy the equals/hashCode and Comparable contracts.
  • Recognise when natural ordering must stay consistent with equals.
  • Apply the builder, factory-method, and strategy idioms — and know when not to.

The equals / hashCode contract

equals must be reflexive, symmetric, transitive, consistent, and x.equals(null) is false. The link to hashCode:

If a.equals(b), then a.hashCode() == b.hashCode(). (The reverse need not hold — unequal objects may collide.)

Break it and hash-based collections misbehave: a HashMap looks in the bucket chosen by hashCode, so an object with a mismatched hash is invisible to get/contains even though an equal one is stored.

java
public record Money(long cents, String currency) { }  // equals + hashCode generated, correct

Records generate both from the components — the easiest way to get the contract right. Hand-written versions should use the same fields in equals and hashCode, via Objects.equals and Objects.hash.

Gotcha

Use the same fields in both methods. If equals compares id but hashCode also mixes in a mutable name, two "equal" objects can hash differently — the contract is broken and lookups fail.

Comparable — and consistency with equals

compareTo returns negative / zero / positive. The subtle rule:

Natural ordering should be consistent with equals: a.compareTo(b) == 0 iff a.equals(b).

java
public record Money(long cents, String currency) implements Comparable<Money> {
    public int compareTo(Money other) { return Long.compare(cents, other.cents); }
}

This compareTo orders by cents only, so compareTo can be 0 while equals is false (same amount, different currency).

Exam-style trap

Sorted collections (TreeSet, TreeMap) decide equality by compareTo, not equals. With the Money above, adding Money(100,"USD") and Money(100,"EUR") to a TreeSet keeps only one — they compare equal. If you need both, make the comparator a tie-breaker: Comparator.comparingLong(Money::cents).thenComparing(Money::currency).

Builder — taming optional parameters

When a type has several optional fields, telescoping constructors become unreadable (new Req(url, "GET", 5000, true, null)). A builder names each field and supplies defaults:

java
HttpRequest r = HttpRequest.to("https://api.test")   // required
    .method("POST")                                  // optional, defaults to GET
    .timeoutMs(1_000)                                // optional, defaults to 5000
    .build();

Make required fields constructor args of the builder, optional fields fluent setters, and have build() produce an immutable result. Don't reach for a builder when one or two fields suffice.

Factory method — naming and decoupling construction

A static factory can have a meaningful name, return a cached/subtype instance, and hide the concrete class:

java
Optional<String> o = Optional.of("x");      // named, clearer than a constructor
List<Integer> xs = List.of(1, 2, 3);        // returns an immutable impl you don't name
Money usd = Money.ofDollars(5);             // domain-specific, validates once

Factories also let you not create a new object every time (Boolean.valueOf, Integer.valueOf's cache). Trade-off: a class with only private constructors + factories can't be subclassed.

Strategy — pluggable behaviour via a functional interface

"Strategy" in modern Java is usually just passing a lambda — a behaviour parameter:

java
void sort(List<Item> items, Comparator<Item> by) { items.sort(by); }

sort(items, Comparator.comparing(Item::price));      // one strategy
sort(items, Comparator.comparing(Item::name));       // another, no new class

Comparator, Runnable, and the java.util.function types (Module 06) are strategy interfaces. Prefer a lambda/method reference over a named class unless the strategy carries state or needs a name.

SDET note

These three patterns dominate test code: builders create readable test fixtures (Module 15), factories (object mothers) produce valid sample objects, and strategy lambdas parameterise data-driven tests. Recognising them also helps you review AI-generated code — a hand-rolled "builder" that forgets to reset state, or an equals without a matching hashCode, is a common AI slip (Module 21).

Key Takeaways

  • Equal objects must have equal hash codes; use the same fields in equals and hashCode (records do this for you).
  • Natural ordering should be consistent with equals; TreeSet/TreeMap judge by compareTo, so an inconsistent ordering silently drops elements.
  • Builder for many optional params → immutable result; don't over-apply it.
  • Factory method gives construction a name, decoupling, and the option to cache or return a subtype.
  • Strategy today is usually a lambda implementing a functional interface (Comparator, etc.).

Lesson Quiz

Lesson Quiz · Object Contracts & Common Patterns0 / 5
  1. Two objects are equal per equals(). What does the contract require of hashCode()?

    • AHash codes may differ freely
    • BHash codes must be equal
    • CExactly one must be 0
    • DhashCode must throw
  2. A Money compareTo orders by cents only, so compareTo can be 0 while equals is false. What happens in a TreeSet?

    • ABoth objects are stored
    • BA compile error
    • COnly one is kept — TreeSet uses compareTo for equality
    • DhashCode decides
  3. When is a builder the right choice?

    • AAlways, instead of constructors
    • BWhen there are several optional parameters
    • COnly for mutable classes
    • DOnly when there is exactly one field
  4. Which is a benefit of a static factory method over a public constructor?

    • AIt can have a descriptive name and may return a cached or subtype instance
    • BIt is always faster
    • CIt allows subclassing
    • DIt removes the need for equals/hashCode
  5. In modern Java, the 'strategy pattern' is most often expressed as…

    • AA giant switch statement
    • BA lambda / method reference implementing a functional interface
    • CReflection
    • DA subclass per behaviour, always

Next: JVM Awareness. Run the matching code in labs/src/main/java/com/jse21/m12_design/.