Appearance
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/hashCodeandComparablecontracts. - 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), thena.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, correctRecords 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) == 0iffa.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 onceFactories 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 classComparator, 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
equalsandhashCode(records do this for you). - Natural ordering should be consistent with equals;
TreeSet/TreeMapjudge bycompareTo, 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
Two objects are equal per equals(). What does the contract require of hashCode()?
A Money compareTo orders by cents only, so compareTo can be 0 while equals is false. What happens in a TreeSet?
When is a builder the right choice?
Which is a benefit of a static factory method over a public constructor?
In modern Java, the 'strategy pattern' is most often expressed as…
Next: JVM Awareness. Run the matching code in labs/src/main/java/com/jse21/m12_design/.