# NOTES — three versions of one value type, and what each one breaks

## Run it first

```
.toolchain\jdk-21\bin\javac.exe -d out *.java
.toolchain\jdk-21\bin\java.exe -cp out Main
```

Real output, verbatim:

```
--- 1. == on two objects with the same value
a == b        : false
a.equals(b)   : true

--- 2. no equals at all: the list lookup that misses
size                      : 1
contains(equal plate)     : false
map.get(equal plate)      : null

--- 3. equals without hashCode: the list works, the map does not
contains(equal plate)     : true
map.get(equal plate)      : null
set size after 2 equal adds: 2

--- 4. equals and hashCode together: every lookup lands
contains(equal plate)     : true
map.get(equal plate)      : T-9001
set size after 2 equal adds: 1

--- 5. toString: default versus overridden
no override : LooseRegistration@1c20c684
override    : Registration[KA01AB1234]

--- 6. ordering without operator<
natural order   : [Registration[KA01AB1234], Registration[KA03CD5678], Registration[KA05ZZ0002]]
reversed        : [Registration[KA05ZZ0002], Registration[KA03CD5678], Registration[KA01AB1234]]
TreeMap keys    : [Registration[KA01AB1234], Registration[KA03CD5678], Registration[KA05ZZ0002]]

--- 7. a TreeMap key that is not Comparable
ClassCastException: class LooseRegistration cannot be cast to class java.lang.Comparable (LooseRegistration is in unnamed module of loader 'app'; java.lang.Comparable is in module java.base of loader 'bootstrap')
```

The hex in `LooseRegistration@1c20c684` is an identity hash and changes between JVM runs. Every
other line is deterministic, including the two `null`s and the `set size` of 2.

**Read block 3 twice.** `contains` says the plate is there and `get` says it is not, for the same
object, in the same program. That is the shape of the bug this lesson exists to prevent, and it is
the one that reaches production, because a list-based test passes.

---

## `Registration.java` — the version that works

```java
public final class Registration implements Comparable<Registration> {

    private final String code;
```

**`final` on the class as well as the field.** A subclass could override `equals` and break
symmetry: `sub.equals(base)` true while `base.equals(sub)` false. `HashMap` then behaves
differently depending on which object it happens to hold, which is a bug with no reliable
reproduction. Sealing the class removes the possibility. The alternative, `getClass() != other.getClass()`
inside `equals`, is the other accepted answer and is what an interviewer may ask you to compare it to.

```java
    public Registration(String code) {
        if (code == null || code.isBlank()) {
            throw new IllegalArgumentException("registration code must not be blank");
        }
        this.code = code;
    }
```

**The null check is load-bearing for `hashCode`.** `hashCode` delegates to `code.hashCode()`, which
would throw `NullPointerException` on a null field. Rejecting null in the constructor means no
`Registration` exists that cannot be hashed. That invariant is what A5 calls "no invalid instance
can exist", and here it is the difference between a clean exception at construction and a
`NullPointerException` from inside `HashMap.put`.

```java
    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
```

**The parameter is `Object`.** Narrow it to `Registration` and you have written an overload that
the whole standard library ignores. `@Override` is what makes that a compile error rather than a
silent wrong answer — see experiment 1 below.

**`this == other` is a fast path, not a correctness requirement.** It matters because
`HashMap.getNode` calls `equals` after finding a bucket match, and in the common case the key found
is the key given. Removing this line keeps the code correct and slower.

```java
        if (!(other instanceof Registration that)) {
            return false;
        }
```

**One line covers null and wrong-type.** `instanceof` is `false` for `null`, so no separate null
check is needed. The `that` on the end is a pattern variable, in scope only where the test
succeeded, and it removes the cast that would otherwise throw `ClassCastException`.

The contract `equals` must satisfy: reflexive, symmetric, transitive, consistent, and
`x.equals(null)` false. Symmetry is the one people break, and `instanceof` against a non-final
class is the usual way they break it.

```java
    @Override
    public int hashCode() {
        return code.hashCode();
    }
```

**Derived from exactly the fields `equals` compares.** The contract runs one way only: equal objects
must have equal hashes; unequal objects may share a hash. Break it and every hash collection
breaks, because `HashMap` compares hashes first and never reaches `equals`.

`Objects.hash(code)` is the usual form for several fields and is correct here too. For one field it
allocates a varargs array on every call, which is measurable when a key is hashed inside a hot
loop. Two fields or more: `Objects.hash(a, b)`.

Nothing requires the hash to be unique or well distributed. A `hashCode` returning `0` for every
instance is legal, correct, and turns every `HashMap` operation into a linear scan.

```java
    @Override
    public String toString() {
        return "Registration[" + code + "]";
    }
```

**This changes the output of code you did not write.** String concatenation, `println`, and every
JUnit failure message call `toString` implicitly. Compare the two lines in block 5: the default
tells you nothing, and it is what a failing assertion will show you at 1am.

```java
    @Override
    public int compareTo(Registration other) {
        return code.compareTo(other.code);
    }
```

**Delegating to `String.compareTo` gets the sign convention right for free.** The contract is
negative, zero, or positive — never specifically -1 and 1. Writing
`return this.code.length() - other.code.length()` is the classic bug: correct for short strings,
and wrong on overflow for large `int` values.

**`compareTo` must agree with `equals`.** `TreeMap` uses `compareTo` alone and never calls `equals`.
A `compareTo` returning 0 for objects that are not `equals` therefore gives you a `TreeMap` and a
`HashMap` that disagree about how many keys you have.

---

## `LooseRegistration.java` — no `equals`, no `hashCode`

The whole class is a field, a constructor and an accessor. It is what a C++ developer writes first,
because in C++ the missing `operator==` would be a compile error at the comparison site.

Here `equals` is inherited from `Object`, where it is defined as `this == other`. So:

```
size                      : 1
contains(equal plate)     : false
map.get(equal plate)      : null
```

The list holds one element, and `contains` says it does not hold an equal one. Both statements are
true, because "equal" means "the same object" until you say otherwise.

**The invariant this class fails to hold:** two registrations with the same code are
interchangeable. Every lookup in the parking lot depends on it — find a ticket from a plate, check
whether a vehicle is already parked, count distinct vehicles.

---

## `HalfEqualRegistration.java` — `equals` without `hashCode`

```
contains(equal plate)     : true
map.get(equal plate)      : null
set size after 2 equal adds: 2
```

**`contains` works and `get` does not, deterministically.** Not "usually" and not "depending on
timing". `HashMap.getNode` reads:

```java
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
```

The hash comparison comes first and short-circuits. Two equal objects with different identity
hashes never reach the `equals` call, whatever bucket they land in. Same reason the `HashSet` holds
two elements it considers equal.

Plain `javac` prints nothing. `javac -Xlint:overrides` prints this:

```
HalfEqualRegistration.java:6: warning: [overrides] Class HalfEqualRegistration overrides equals, but neither it nor any superclass overrides hashCode method
public final class HalfEqualRegistration {
             ^
1 warning
```

`-Xlint:all` includes it. Adding that flag once is the cheapest defence against the only mistake
here a compiler can see.

---

# Two experiments, run for real

## 1 · The `equals` overload that is not an override

Write the C++-shaped signature, with no `@Override`:

```java
public final class Plate {
    private final String code;
    public Plate(String code) { this.code = code; }

    public boolean equals(Plate other) { return code.equals(other.code); }

    @Override public int hashCode() { return code.hashCode(); }
}
```

Compiled with `-Xlint:all`. **Zero warnings.** Then:

```
p.equals(q) directly     : true
list.contains(q)         : false
p.equals((Object) q)     : false
```

The first line is your overload, chosen by the compiler because the static type is `Plate`. The
second and third are `Object.equals`, chosen because `List.contains` holds `Object` references.
So the method works everywhere you call it directly and nowhere the library calls it.

Add `@Override` and the file stops compiling:

```
Registration.java:28: error: method does not override or implement a method from a supertype
    @Override
    ^
1 error
```

That is the entire argument for writing `@Override` on `equals` every time.

## 2 · A `TreeMap` key with no `Comparable`

`new TreeMap<>()` compiles for any key type, because supplying a `Comparator` is the other legal
way to give an order. The failure lands on the first `put`:

```
ClassCastException: class LooseRegistration cannot be cast to class java.lang.Comparable (LooseRegistration is in unnamed module of loader 'app'; java.lang.Comparable is in module java.base of loader 'bootstrap')
```

Read the message past the module noise: the key type is not `Comparable`. Two fixes, and they mean
different things. `implements Comparable<Registration>` declares the type's one natural order.
`new TreeMap<>(comparator)` supplies an order for this map only, and leaves the type alone.

---

## Which type to write

Reach for a `record` first:

```java
public record Vehicle(String registration, VehicleType type) {}
```

That generates `equals`, `hashCode` and `toString` from the components, and none of this lesson's
traps apply. It generates no `compareTo`.

Write the four methods by hand when a `record` will not do the job: the type has mutable state, or
identity semantics are what you actually want. A `ParkingLot` is not a value and should have no
`equals` — two lots with equal contents are not the same lot. Overriding `equals` on an entity is
its own bug, and the tell is that the type has a lifecycle rather than a value.
