Skip to main content
A pixel art illustration titled "The Vending Machine Contract" showing three vending machines with identical interfaces (Chips $1.00, Soda $1.50, Candy $0.75) but different internal mechanisms: one uses gravity and ramps, one uses pneumatic tubes with steam gauges, and one uses hamsters running on wheels and conveyor belts. All three successfully deliver the correct products. In the corner, a rejected fourth machine lies broken in a trash heap—it promised chips but dispensed a boot, violating the contract. The caption reads "Same Promise. Many Machines." illustrating how multiple implementations can satisfy the same specification, but violations are rejected.

CS 3100: Program Design and Implementation II

Lecture 4: Specifications and Common Contracts

©2025 Jonathan Bell & Ellen Spertus, CC-BY-SA

Announcements

  • In-person office hours (listed on Oakland Canvas) are held in CPM 200:
    • Monday 3-4 (Ellen)
    • Tuesday 5-6 (Lucia)
    • Wednesday 2-4 (Ran)
    • Thursday 3-4 (Ellen)
    • Thursday 5-6 (Lucia)
  • Ellen and Lucia offer one-on-one appointments.
  • Complete the student survey by Friday.
  • If you are having trouble, ask for help!

How are you feeling about CS 3100?

Poll image
Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

What's your progress on Assignment 1? (anonymous)

Poll image
Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

Learning Objectives

After this lecture, you will be able to:

  1. Describe the role of method specifications in achieving program modularity and improving readability
  2. Evaluate the efficacy of a given specification using the terminology of restrictiveness, generality, and clarity
  3. Utilize type annotations to express invariants such as non-nullness
  4. Define the role of methods common to all Objects in Java (toString, equals, hashCode, compareTo)

Humans Can Only Hold 7±2 Items in Working Memory

Which is easier to remember?

  • Random order: 50, 30, 60, 20, 80, 10, 40, 70
  • Pattern: 10, 20, 30, 40, 50, 60, 70, 80

Chunking Lets Us Manage More Than 7 Items

Chunking = organizing information into meaningful units

  • "FBI" is 1 chunk, not 3 letters
  • "555-1234" is 2 chunks, not 7 digits
  • A chess master sees "castled king position" not 5 pieces

Expert programmers chunk code the same way:

  • "Binary search" → one chunk (not 15 lines)
  • "HashMap lookup" → one chunk (not the internal algorithm)

Specifications Enable Mental Chunking for Code

When reading a program, we want to understand method behavior without reading the implementation.

Without spec: 🤯

public boolean mystery(Object[] arr, Object o) {
for (int i = 0; i < arr.length; i++) {
if (arr[i].equals(o)) return true;
}
return false;
}

Must read every line to understand

With spec: 😌

/**
* Returns true if this collection contains
* the specified element.
*
* @return true if this collection contains o
*/
boolean contains(Object o);

Spec tells you what it does

You Spend 10x More Time Reading Code Than Writing It

Consider understanding this call from L3:

Map<String, List<IoTDevice>> devicesByRoom = new HashMap<>();
devicesByRoom.get("living-room").add(new DimmableLight("lr1", 100));

Without spec:

  • Open HashMap source (2000+ lines)
  • Trace through hash buckets
  • Understand resize logic
  • Time: 30+ minutes

With spec:

  • Read Map.get() Javadoc
  • "Returns the value for this key, or null"
  • Time: 30 seconds

You used HashMap without reading its 2000-line implementation!

A Good Specification Lets You Predict Behavior

The goal: a developer can understand what a method does without reading its code.

  • Any implementation that satisfies the spec is correct
  • Any implementation that violates the spec is incorrect
  • The spec should be easier to understand than the implementation

But how do we evaluate whether a specification is good?

Good Specifications Balance Three Criteria

A whimsical laboratory illustration titled "The Goldilocks Specification Lab." Three stations demonstrate specification failures: "Too Restrictive" shows a cage rejecting valid code like Binary Search; "Too General" depicts a barn door admitting buggy implementations; and "Unclear" features confused developers interpreting smudged text differently. In the glowing center, a "Just Right" machine perfectly filters code, allowing all valid implementations (linear, binary, parallel) to pass while rejecting bugs. A summary panel emphasizes balancing restrictiveness, generality, and clarity to create effective software specifications.

Restrictive Specs Rule Out Bad Implementations

A spec is restrictive if it rules out implementations that clients would find unacceptable.

Think of it as: "What BAD behaviors does this spec prohibit?"

  • Does it specify what happens for ALL inputs?
  • Does it prohibit surprising or dangerous behavior?
  • Could a malicious implementer satisfy it while being useless?

Under-Specified Behavior Allows Bugs to Hide

Consider this spec for Map.get():

/**
* Returns the value associated with the specified key.
*
* @param key the key whose value is to be returned
* @return the value associated with key
*/
V get(Object key);

What happens if the key isn't in the map?

  • Return null?
  • Throw KeyNotFoundException?
  • Return a default value?

Every Input Needs Defined Behavior

✓ The actual Map.get() specification:

/**
* Returns the value to which the specified key is mapped,
* or null if this map contains no mapping for the key.
*
* @param key the key whose associated value is to be returned
* @return the value to which the specified key is mapped,
* or null if this map contains no mapping for the key
* @throws ClassCastException if the key is of an inappropriate type
* @throws NullPointerException if the key is null and this map
* does not permit null keys
*/
V get(Object key);

Now every input has defined behavior.

Silence in a Spec Means Undefined Behavior

Underspecified:

/**
* Returns an iterator over
* the elements in this set.
* @return an Iterator over the
* elements in this set
*/
public Iterator<E> iterator()

Client might assume insertion order...

Properly specified:

/**
* Returns an iterator over
* the elements in this set.
* The elements are returned in
* no particular order.
* @return an Iterator over the
* elements in this set
*/
public Iterator<E> iterator()

Assuming Unspecified Behavior Creates Flaky Tests

Two-panel comic: tests pass on developer laptop but fail on CI server due to HashSet iteration order, with Hyrum's Law quote

"Works on my machine" isn't a specification!

General Specs Don't Over-Constrain Implementations

A spec is general if it doesn't rule out implementations that would be correct.

Think of it as: "What GOOD implementations does this spec allow?"

  • Does it describe WHAT the method does, or HOW?
  • Could a faster algorithm satisfy it?
  • Does it over-specify implementation details?

Operational Specs Reject Valid Implementations

❌ Too operational (not general):

/**
* Returns true if this set contains the specified element.
* Iterates through all elements using a for-each loop,
* comparing each element using equals().
*
* @param o the element to search for
* @return true if found, false otherwise
*/
boolean contains(Object o);

This spec requires O(n) linear search!

Describe Results, Not Algorithms

✓ The actual Set.contains() specification:

/**
* Returns true if this set contains the specified element.
* More formally, returns true if and only if this set contains
* an element e such that Objects.equals(o, e).
*
* @param o element whose presence in this set is to be tested
* @return true if this set contains the specified element
* @throws ClassCastException if the type of o is incompatible
* @throws NullPointerException if o is null and this set
* does not permit null elements
*/
boolean contains(Object o);

This permits: HashSet (O(1)), TreeSet (O(log n)), LinkedHashSet...

The Right Balance Depends on What Callers Need

Compare these two List methods:

General (contains):

/**
* Returns true if this list
* contains the specified element.
*/
boolean contains(Object o);

Any implementation OK

Specific (indexOf):

/**
* Returns the index of the first
* occurrence of the specified
* element, or -1 if not found.
*/
int indexOf(Object o);

Must return FIRST index

indexOf is more restrictive because callers need to know which occurrence.

The Most Dangerous Specs Are Misunderstood Specs

A spec is clear if readers understand it correctly.

The most dangerous specs are those where readers think they understand but don't.

  • Too brief: Readers fill in gaps with assumptions
  • Too long: Readers skim and miss important details
  • Jargon-heavy: Readers guess at meaning
  • Redundant: Readers wonder what's different about each statement

Redundancy Creates Confusion, Not Clarity

❌ Redundant (hurts clarity):

/**
* Closes this stream and releases any resources.
* This method closes the stream. After closing,
* the stream is closed and cannot be used.
* Closing releases resources held by the stream.
*/
void close() throws IOException;

"Close closes the closed stream" — we got it the first time!

Domain-Specific Terms Need Definitions

❌ Unclear (assumes domain knowledge):

/**
* Iterates over elements in natural order.
* @return an iterator in natural order
*/
Iterator<E> iterator();

✓ Clear (defines the term):

/**
* Iterates over elements in natural order.
* Natural order is defined by the elements' compareTo() method,
* with smaller elements appearing before larger ones.
* @return an iterator in ascending natural order
*/
Iterator<E> iterator();

The Same Spec Principles Apply at Every Scale

Concept: 'The Recipe Card at Different Scales' (Same Principles, Different Kitchens)

A warm, inviting three-panel illustration showing that recipe-writing principles remain constant whether you're cooking for yourself, running a restaurant, or programming a robot baker. Rendered in a cozy culinary-meets-technical style with consistent visual language across all three scales.

LEFT PANEL - 'HOME KITCHEN' (You as Implementer):
A cozy home kitchen with a handwritten recipe card pinned to the fridge: 'Chocolate Chip Cookies - Mix ingredients, bake until golden.' A home cook (you) stands at the counter, confidently filling in the gaps: thought bubbles show 'I like them crispy,' 'Mom's recipe uses butter not margarine,' 'Golden means 12 minutes in MY oven.' The cookies come out perfect because YOUR context matches YOUR intent. A small note: 'Spec works because you share context with yourself.' Warm, golden lighting. Caption: 'Vague specs work when you implement them yourself.'

CENTER PANEL - 'RESTAURANT KITCHEN' (Team of Humans):
A bustling professional kitchen with multiple cooks at different stations, all reading the SAME recipe card—but now it's more detailed: '225g butter (unsalted), 2.5g salt per 100g flour, bake at 177°C for 11 minutes.' Different cooks have different thought bubbles showing their interpretations: Cook A thinks 'golden brown = deep amber,' Cook B thinks 'golden brown = light tan,' Cook C (new hire) thinks 'wait, what's golden brown?' Some cookies come out perfect, some burnt, some underdone. A quality control inspector looks frustrated. Note: 'Same spec, different contexts, different results.' Caption: 'More implementers = more interpretations.'

RIGHT PANEL - 'ROBOT BAKER' (AI/Automated System):
A gleaming automated bakery line with a robot arm and conveyor belt. The recipe is now a detailed specification document: 'Bake until internal temp reaches 93°C AND surface reflectance measures 0.35 on colorimeter AND elapsed time ≥ 10 min. If flour hopper < 500g, HALT and alert. If oven temp deviates > 2°C, compensate by...' The robot executes EXACTLY what's specified—no more, no less. One batch shows perfect cookies. Another batch shows the robot has stopped mid-production because the spec said 'halt' but didn't say what to do after refilling flour. An engineer scratches their head: 'It did exactly what we said...' Caption: 'AI/automation executes literally. Every gap becomes a decision point.'

BOTTOM UNIFYING ELEMENT:
A horizontal strip showing the same three principles (Restrictive, General, Clear) as measuring cups that appear in all three kitchens—same tools, different scales. Central message: 'The recipe-writing principles don't change. The consequences of ambiguity do. A vague instruction like 'bake until done' works when YOU'RE baking—but fails at scale.'

THE TWIST (Important callout box):
Shows the home cook returning to their kitchen 6 months later, following their own vague recipe... and the cookies come out wrong. Thought bubble: 'Wait, did I used to bake these at 350 or 375? What did I mean by golden?' Caption: 'Plot twist: Even YOUR context changes over time. Future-you is also a different implementer.'

BOTTOM TAGLINE:
'Every implementer—including yourself tomorrow—fills gaps with their own context. The question isn't WHO interprets your spec. It's whether your spec eliminated the wrong interpretations.'

Style: Warm bakery aesthetic with rich browns, warm yellows, and cream colors. Each panel should feel progressively more industrial/technical while maintaining the cooking metaphor. The home kitchen is cozy and personal, the restaurant is professional but human, the robot bakery is precise and clinical. Should feel like a relatable story that makes students think: 'Oh, I've been that home cook who couldn't follow my own recipe later!'

"Process" and "Handle" Are Not Specifications

Consider this IoT device manager API:

/**
* Handles the given device event.
*
* @param event the device event to handle
*/
public void handleEvent(DeviceEvent event)

What does "handle" actually mean? Every implementer fills the gap differently.

  • Log it? Update device state? Notify other devices?
  • What if the device is offline? Retry? Queue for later?
  • Thread-safe? Can it be called concurrently?

Ambiguity Creates Specification Debt

Concept: 'The Hidden Decision Factory' (How Ambiguous Specs Create Unintended Consequences)

An illustrated factory scene showing how ambiguous specifications lead to hidden, consequential decisions being made by implementers.

THE AMBIGUOUS SPEC:
A vague specification document enters the factory on a conveyor belt. It reads: 'Process submissions promptly.' A magnifying glass hovers over it showing NO details about: ordering, fairness, priority, timing. The document has a 'SEEMS FINE' stamp but is leaking question marks.

THE DECISION FACTORY:
Inside the factory, a developer stands at a workstation with multiple levers and buttons—each representing a DECISION the spec didn't make:
- Lever 1: 'Processing Order?' (alphabetical / random / first-come / priority-based)
- Lever 2: 'What is prompt?' (1 second / 1 minute / 1 hour)
- Lever 3: 'Handle ties how?' (arbitrary / fair rotation)
The developer shrugs and pulls levers somewhat randomly: 'The spec didn't say... I'll just do alphabetical. Seems reasonable?'

THE UNINTENDED CONSEQUENCE:
The factory output shows a queue of student submissions being processed. Students named 'Adams', 'Baker', 'Chen' get processed quickly (shown smiling, timer showing '2 min wait'). Students named 'Williams', 'Young', 'Zhang' wait much longer (shown frustrated, timer showing '45 min wait'). A fairness meter shows a red warning. Caption: 'Alphabetical ordering = systematic disadvantage for names late in alphabet. Developer didn't intend this. Spec didn't prevent it.'

THE HIDDEN COST:
A timeline showing:
- 'SPEC PHASE: Fix ambiguity for $100' (small, easy)
- 'DEV PHASE: Fix ambiguity for $1,000' (medium, refactoring)
- 'PRODUCTION: Fix ambiguity for $100,000' (huge—user complaints, legal review, PR crisis, data migration)
Caption: 'Specification debt compounds like financial debt. The longer you wait, the more expensive the fix.'

THE LESSON:
'Ambiguous specs don't eliminate decisions—they delegate them to people who don't realize they're making them. Hidden decisions become invisible assumptions become unintended consequences.'

Style: Whimsical factory/Rube-Goldberg aesthetic with clear cause-and-effect flow. Show the chain from 'vague spec' → 'arbitrary implementation choice' → 'real-world unfairness'. Color palette: grays and browns for the ambiguous input, increasingly alarming reds for the consequences. Should feel like a cautionary tale that makes students want to clarify their specs upfront.

Type Annotations Let Compilers Enforce Specs

Concept: 'The Factory Quality Control Line' (Comments vs Type Annotations)

A detailed factory floor illustration in a clean industrial design style, showing two parallel assembly lines for 'Method Calls' being quality-checked before shipping to production.

LEFT LINE - 'HONOR SYSTEM QUALITY CONTROL' (Comment-Only Specs):
A conveyor belt carrying method calls (visualized as packages labeled with their arguments). A laminated sign hangs above: '📋 PLEASE CHECK: arr should not be null - Javadoc says so!' A tired human inspector sits on a stool, barely glancing at packages as they roll by. One package clearly labeled 'null' rolls past—the inspector is looking at their phone. Behind the inspection point: CHAOS. Packages explode on contact with the 'Production' area (labeled 'Runtime'), sparks fly, a NullPointerException alarm blares, developers in hard hats scramble with fire extinguishers. A whiteboard shows: 'Days since last null crash: 0̶ ̶3̶ ̶7̶ 0'. Annotation: 'Comments are suggestions. Humans skip reading them. Bugs ship to production.'

RIGHT LINE - 'AUTOMATED QUALITY GATE' (Type Annotations):
The same conveyor belt, but now it passes through an imposing automated scanner (glowing, precise, robotic). The scanner display shows: '@NonNull int[] arr — SCANNING...' Valid packages (with proper non-null arrays) get a green checkmark stamp and proceed smoothly to 'Production'. But the 'null' package hits an invisible force field—red lights flash, the scanner announces 'REJECTED: null violates @NonNull constraint', and the package is automatically diverted to a 'Fix Before Shipping' chute that loops back to the developer's desk. Beyond the gate: calm, orderly production floor. A whiteboard shows: 'Days since last null crash: 847'. Annotation: 'Type annotations are machine-enforced. Bugs caught before shipping.'

CENTER COMPARISON PANEL:
Split view showing the same code:
- Top: '// @param arr must not be null' + 'sum(null);' → Package makes it to production → 💥 RUNTIME CRASH
- Bottom: '@NonNull int[] arr' + 'sum(null);' → Package rejected at compile time → 🔧 Developer fixes it

DETAIL CALLOUTS:
- On the scanner: 'Powered by NullAway + JSpecify' with small logos
- Developer reaction left: 😰 'Why did this crash in production at 3am?!'
- Developer reaction right: 😊 'Caught it before I even ran the tests'

BOTTOM:
A timeline showing: 'Java 8 (2014): Type annotations added' with small portraits of the UW researchers. Caption: 'Moving specs from comments (that humans ignore) to types (that compilers enforce).'

Style: Industrial factory aesthetic with warm yellows/oranges on the chaotic left side, cool blues/greens on the orderly right side. Clean lines, clear labels, immediate visual contrast. Should feel like a safety training poster that makes a compelling case for type annotations. The factory metaphor works because it emphasizes: quality control BEFORE shipping is cheaper than recalls AFTER.

Type annotations let the compiler enforce specification invariants.

@NonNull Turns Documentation Into Enforcement

import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;

public int sum(@NonNull int[] arr) {
int sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}

With @NonNull, the compiler enforces that arr is never null.

Note: Java has many competing @NonNull definitions. We use JSpecify.

@NullMarked Makes Non-Null the Default

Mark the whole package as "non-null by default":

// In package-info.java
@NullMarked
package edu.neu.cs3100.myproject;

import org.jspecify.annotations.NullMarked;

Now only mark exceptions with @Nullable:

// In a @NullMarked package:
public int sum(int[] arr) { ... } // arr assumed non-null

public String format(@Nullable String prefix, String value) { ... }
// prefix can be null, value cannot

Annotations Make Nullability Explicit at a Glance

// In a @NullMarked package

/**
* Formats a user's display name.
*
* @param firstName the user's first name
* @param middleName the user's middle name, or null if none
* @param lastName the user's last name
* @return formatted name (such as "John Q. Public")
*/
public String formatName(
String firstName, // non-null (default)
@Nullable String middleName, // explicitly nullable
String lastName // non-null (default)
) {
if (middleName == null) {
return firstName + " " + lastName;
}
return firstName + " " + middleName.charAt(0) + ". " + lastName;
}

Legacy Code Requires Gradual Annotation Adoption

Two approaches for existing codebases:

Approach A: @NullMarked (new code)

  • Mark new packages @NullMarked
  • Existing code left unannotated
  • Gradually migrate package by package

Approach B: @NonNull everywhere (legacy)

  • Don't use @NullMarked
  • Add @NonNull as you verify each type
  • Safer but more verbose

For this course: new projects use @NullMarked. You'll see this in starter code.

Unannotated Libraries Need Runtime Assertions

Even java.util isn't fully annotated! The checker can't know if library methods return null.

String name = "Alice";
List<String> names = List.of(name); // Checker: "might be null!"
System.out.println(names.size()); // Warning!

Use Objects.requireNonNull to assert your domain knowledge:

List<String> names = Objects.requireNonNull(List.of(name));
// Checker now knows names is non-null

Every Java Object Inherits Four Key Contracts

Every class extends java.lang.Object, which defines methods you should consider overriding:

  • toString() — human-readable representation
  • equals(Object) — logical equality
  • hashCode() — hash value for collections

Plus the Comparable interface:

  • compareTo(T) — natural ordering

toString Should Be Concise But Informative

From the Java documentation:

"Returns a string that textually represents this object. The result should be a concise but informative representation that is easy for a person to read."

Default (useless):

DimmableLight@1a2b3c4d

Overridden (helpful):

DimmableLight(color=2700K,
brightness=100, on=true)

Good toString Saves Hours of Debugging

// From the IoT device hierarchy in L3
@Override
public String toString() {
return "DimmableLight(id=" + id + ", " +
"brightness=" + brightness + ", " +
"on=" + isOn() + ")";
}

Best practices:

  • Include the class name
  • Include fields that matter for understanding the object
  • Format for readability (not just dump all fields)

When you System.out.println(deviceList), good toString() makes debugging easy.

What does this print (==)?

public class DimmableLight {
private String id;
private int brightness;

public DimmableLight(String id, int brightness) {
this.id = id;
this.brightness = brightness;
}

public static void main(String[] args) {
DimmableLight light1 = new DimmableLight("living-room", 100);
DimmableLight light2 = new DimmableLight("living-room", 100);
System.out.println(light1 == light2);
}
}

A. true

B. false

C. I have no idea

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

What does this print (equals()) ?

public class DimmableLight {
private String id;
private int brightness;

public DimmableLight(String id, int brightness) {
this.id = id;
this.brightness = brightness;
}

public static void main(String[] args) {
DimmableLight light1 = new DimmableLight("living-room", 100);
DimmableLight light2 = new DimmableLight("living-room", 100);
System.out.println(light1.equals(light2));
}
}

A. true

B. false

C. I have no idea

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

Where does equals() come from?

Object's implementation:

public boolean equals(Object obj) {
return (this == obj);
}

Since DimmableLight doesn't override equals(), it inherits this version—which just checks reference equality.

Overriding equals()

public class DimmableLight {
private String id;
private int brightness;

// ... constructor ...

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof DimmableLight other)) return false;
return brightness == other.brightness
&& Objects.equals(id, other.id);
}

@Override
public int hashCode() {
return Objects.hash(id, brightness);
}
}

What's hashCode()? And why do we need it?

hashCode Enables O(1) Lookup in HashSets and HashMaps

Concept: 'The Hash Table Post Office' (How hashCode and equals Work Together)

A detailed post office / mail sorting facility illustration showing how HashMap finds objects in O(1) time.

THE MAIL SORTING FACILITY:
A bird's-eye view of a mail sorting center with numbered bins (0-15, representing hash buckets). Each bin can hold multiple packages (objects).

STEP 1 - COMPUTING THE HASH:
A package (DimmableLight object) arrives at the 'Hash Calculator' station—a machine that examines the object's fields (color=2700, brightness=100, on=true) and produces a number. The machine display shows: 'hashCode() → 234789234 → mod 16 → Bucket 7'. An arrow shows the package being directed toward Bucket 7. Annotation: 'hashCode() tells us WHICH bucket to check—like a ZIP code for objects.'

STEP 2 - FINDING THE BUCKET:
Bucket 7 is highlighted, showing it contains 3 packages (objects with the same hash). Each package has a label showing its identity. A postal worker (the equals() method) stands at the bucket, examining each package one by one. Annotation: 'Multiple objects can have the same hashCode (collision). That's fine—buckets can hold several.'

STEP 3 - THE EQUALS CHECK:
The postal worker compares the incoming package against each package in the bucket using equals(). Two packages don't match (red X). One matches exactly (green ✓)! The worker calls out: 'Found it! This is the one.' Annotation: 'equals() confirms exact identity within the bucket—like checking the full address, not just ZIP code.'

BOTTOM COMPARISON - O(1) vs O(n):
Left: 'Without hashCode: Check EVERY object in the collection' — shows searching through 1000 packages one by one. Time: O(n).
Right: 'With hashCode: Go directly to bucket, check only 2-3 objects' — shows jumping to bucket 7 and checking 3 items. Time: O(1) average.

KEY INSIGHT CALLOUT:
'hashCode = fast, approximate (which bucket?)'
'equals = slow, exact (is this the one?)'
'BOTH are needed. hashCode narrows the search; equals confirms the match.'

THE CONTRACT WARNING:
Shows what happens when equals and hashCode disagree (red alert box):
- Two 'equal' objects with different hashCodes → they go to different buckets → 'light2' is in bucket 7, but 'light1' (which equals light2) is in bucket 3 → search for light1 looks in bucket 3, never finds light2 → 'OBJECT DISAPPEARED!'
A small diagram shows the failed lookup, with the object seemingly vanishing. Caption: 'If equal objects have different hashCodes, HashMap BREAKS.'

Style: Warm, friendly postal/logistics aesthetic with clear numbering, organized bins, and helpful workers. Color-code the buckets, highlight the 'found' path in green. Should make the two-phase lookup (hash → bucket → equals → found) crystal clear. The post office metaphor works because everyone understands: ZIP code gets you to the right area, then you need the full address to find the exact house.

Why override hashCode()?

Object's hashCode() returns a value based on the object's memory address.

Can two different objects have the same memory address? No.

// DimmableLight with equals() but NO hashCode() override
DimmableLight light1 = new DimmableLight("living-room", 100);
DimmableLight light2 = new DimmableLight("living-room", 100);

System.out.println(light1.equals(light2)); // true (we overrode equals)

Set<DimmableLight> lights = new HashSet<>();
lights.add(light1);
System.out.println(lights.contains(light2)); // false!

The problem: light1 and light2 are equal, but have different hash codes, so HashSet looks in the wrong bucket.

Equal Objects Must Have Equal hashCode() Values

The contract (simplified):

  • Required: If x.equals(y), then x.hashCode() == y.hashCode()
  • Recommended: If !x.equals(y), hash codes should differ
  • Required: Consistent within one execution

⚠️ If you override equals, you must override hashCode!

Use the Same Fields in hashCode as in equals

// For DimmableLight with fields: id, brightness
@Override
public int hashCode() {
int result = Objects.hashCode(id);
result = 31 * result + Integer.hashCode(brightness);
return result;
}

// Or simply:
@Override
public int hashCode() {
return Objects.hash(id, brightness);
}

Use the same fields as equals. See Effective Java Item 11 for details.

Sorting with the Comparable Interface

An educational infographic titled 'The Universal Sorting Machine' that uses a factory metaphor to explain Java's Comparable interface. Diverse objects, such as fruit boxes representing Strings, numbered blocks representing Integers, and people representing Student objects, are shown on a conveyor belt. Despite their differences, each object carries an identical electrical plug labeled compareTo(). These objects are fed into a large central machine labeled Collections.sort(), which accepts the universal plugs to produce sorted output lists. The illustration demonstrates how implementing the single compareTo() method allows different object types to be sorted by the same generic algorithm, contrasting this efficiency with a disorganized scene of a programmer writing repetitive code without the interface.

The Comparable Interface

public interface Comparable<T> {
/**
* Compares this object with the
* specified object for order.
*
* @return negative if this < o,
* zero if this == o,
* positive if this > o
*/
int compareTo(T o);
}

compareTo Defines How Objects Sort Naturally

For classes with a natural order, implement Comparable<T>:

/**
* A dimmable light. Lights are ordered by id, then by brightness.
*/
public class DimmableLight implements Comparable<DimmableLight> {
private String id;
private int brightness;

@Override
public int compareTo(DimmableLight other) {
int idCompare = this.id.compareTo(other.id);
if (idCompare != 0) {
return idCompare; // different ids
}
// same id, sort by brightness
return Integer.compare(this.brightness, other.brightness);
}
}

Returns: negative if this < other, zero if equal, positive if this > other

Summary: Specifications as Contracts

  1. Specifications enable modularity — you used HashMap without reading its 2000 lines
  2. Good specs balance three criteria:
    • Restrictiveness — Map.get() specifies null for missing keys
    • Generality — Set.contains() permits O(1) or O(log n) implementations
    • Clarity — no redundancy, domain terms defined
  3. Type annotations make specs machine-checkable (@NullMarked, @Nullable)
  4. Object contracts (toString, equals, hashCode, compareTo) enable Collections to work

Critical rule: Override equals → MUST override hashCode (or HashSet breaks!)

Next Steps

  • Assignment 1 due Thursday, January 15 at 11:59 PM
  • Complete flashcard set 4 (Specifications & Contracts)

Optional readings:

Next time: Functional Programming and Readability