Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Java  Module 5 – Classes, objects, records and enums

Java equals, hashCode and toString contracts

Write equals, hashCode and toString that keep the Object contracts, and watch a missing hashCode or a changed key break HashSet and HashMap.

  • Intermediate
  • 20 minutes
  • Examples run with Java temurin-25.0.4+101.0.LTS and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Implement equals and hashCode that keep the contract of Object
  • Predict how an overloaded equals, a missing hashCode or a changed key makes HashSet and HashMap miss an equal object
  • Explain why a subclass that adds a field cannot keep equals symmetric, and choose a fix
  • Estimate what a constant hash code costs HashMap lookups from the number of keys
  • Write a toString that names the value and keeps secrets out of logs

Before you start

What you’ll learn · Before you start

What you will learn

  • Implement equals and hashCode that keep the contract of Object
  • Predict how an overloaded equals, a missing hashCode or a changed key makes HashSet and HashMap miss an equal object
  • Explain why a subclass that adds a field cannot keep equals symmetric, and choose a fix
  • Estimate what a constant hash code costs HashMap lookups from the number of keys
  • Write a toString that names the value and keeps secrets out of logs

Before you start

On this page

Two Point objects for (3, 4) describe the same point, yet with the methods every class inherits from Object a HashSet keeps both of them, 2 elements, and a set that holds one says it does not contain the other. Those methods treat every object as unique: equals is true only for the very same object, hashCode gives each object a number of its own, and toString prints the class name, an @ and a hexadecimal number.

When your objects stand for values, such as a point, a seat on a train or an amount of money, you override equals so that equal values compare equal, and then hashCode as well: the Object documentation makes equal hash code: The int that hashCode returns for an object. A hash table uses it to pick a bucket, so objects that equals calls equal must return the same hash code; unequal objects may share one, which only slows the table down. for equal objects a contract, and HashSet and HashMap rely on it without checking. This lesson writes all three methods, breaks each one in a recorded run, and counts what a poor hash code costs a HashMap: 501,499 calls to equals for 1,000 lookups.

Object treats every object as unique, so value classes override equals

Object.equals returns true only when both references lead to the very same object. Its documentation calls this the most discriminating equivalence relation: A relation that is reflexive (x equals x), symmetric (if x equals y, then y equals x) and transitive (if x equals y and y equals z, then x equals z). The Object documentation requires equals to be one. possible: every object is equal only to itself. Object.hashCode returns a number tied to the object’s identity, its identity hash code: The hash code Object.hashCode gives an object whose class does not override it, and System.identityHashCode gives any object: a number tied to that one object rather than to its field values.; as far as is reasonably practical, distinct objects get distinct numbers. That is the right behaviour for objects with an identity of their own, such as an open file or a running thread, and the wrong one for values.

Every example of this lesson is a small program in a folder of its own: a Main.java compact source file and the classes it uses, each in a file of its own. java points/Main.java compiles Main.java and the classes it needs from the same folder, then runs it; the launcher has run programs made of several source files since JDK 22 (JEP 458).

Here are three classes for a point, each tried the way a HashSet uses them. PlainPoint overrides nothing, EqualsOnlyPoint overrides only equals, and Point overrides both methods:

Three point classes and the program that compares them

Java (Main.java) · points/Main.java

// Two objects for the point (3, 4) from each class, checked the way a HashSet uses them.
void main() {
    IO.println("class            equals  same hash  set size  contains");
    report("PlainPoint", new PlainPoint(3, 4), new PlainPoint(3, 4));
    report("EqualsOnlyPoint", new EqualsOnlyPoint(3, 4), new EqualsOnlyPoint(3, 4));
    report("Point", new Point(3, 4), new Point(3, 4));
}

void report(String name, Object a, Object b) {
    Set<Object> both = new HashSet<>();
    both.add(a);
    both.add(b);                        // a second element, unless the set decides that b equals a
    Set<Object> onlyA = new HashSet<>();
    onlyA.add(a);
    IO.println("%-16s %-7s %-10s %-9d %s".formatted(
            name, a.equals(b), a.hashCode() == b.hashCode(), both.size(), onlyA.contains(b)));
}

Java (PlainPoint.java) · points/PlainPoint.java

/** A point that keeps what Object gives every class: equals means "the very same object". */
final class PlainPoint {
    private final int x;
    private final int y;

    PlainPoint(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Java (EqualsOnlyPoint.java) · points/EqualsOnlyPoint.java

/** Overrides equals but not hashCode: equal points still get the hash codes of two different objects. */
final class EqualsOnlyPoint {
    private final int x;
    private final int y;

    EqualsOnlyPoint(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object other) {
        return other instanceof EqualsOnlyPoint p && x == p.x && y == p.y;
    }
}

Java (Point.java) · points/Point.java

import java.util.Objects;

/** equals and hashCode, both computed from the same two fields. */
final class Point {
    private final int x;
    private final int y;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object other) {
        return other instanceof Point p && x == p.x && y == p.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }
}
Two points for (3, 4) from each class Python · points_session.py
from session import run

# Main.java uses the three point classes, so the launcher compiles their files from points/ too.
# Then javac with its "overrides" lint, which warns about an equals without a hashCode.
run("""
java points/Main.java
javac -Xlint:overrides -d build points/EqualsOnlyPoint.java
""")

Output

# openjdk version "25.0.4.1" LTS
$ java points/Main.java
class            equals  same hash  set size  contains
PlainPoint       false   false      2         false
EqualsOnlyPoint  true    false      2         false
Point            true    true       1         true
$ javac -Xlint:overrides -d build points/EqualsOnlyPoint.java
points/EqualsOnlyPoint.java:2: warning: [overrides] Class EqualsOnlyPoint overrides equals, but neither it nor any superclass overrides hashCode method
final class EqualsOnlyPoint {
      ^
1 warning

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 points_session.py

Read the table one row at a time:

  • PlainPoint behaves as Object intends. The two objects are different, so the set keeps both and does not contain the second one. For a value that is wrong, but at least it is consistent.
  • EqualsOnlyPoint is the dangerous one. equals now says true, yet the set still holds two elements and contains still says false. The set never asked equals at all, because the two hash codes differ.
  • Point gives equal objects equal hash codes, so the set keeps one element and finds it.

The middle row is a broken contract, and running the program reports nothing: it compiles, runs and prints wrong answers. The second command shows the one place that can warn you. Compiled by javac with the overrides lint switched on (-Xlint:overrides, or every lint with -Xlint:all), EqualsOnlyPoint gets a warning that it overrides equals but not hashCode. Build tools pass such options to javac, so it is worth turning on.

Checkpoint

Check: What Object's equals and hashCode compare

  1. Question 1 of 2 In the recorded run, EqualsOnlyPoint says equals is true, yet the set holds 2 elements and contains says false. Why?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The set compares hash codes first, and the two points kept the different hash codes Object gives two objects

    A hash table looks at hash codes before it ever calls equals. EqualsOnlyPoint inherits Object.hashCode, which gives two objects two different numbers, so the second point is looked for under the wrong number and equals is never asked.

  2. Question 2 of 2 PlainPoint overrides nothing. What does the equals it inherits from Object compare?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Whether both references lead to the very same object

    The Object documentation calls its equals the most discriminating equivalence relation: x.equals(y) is true only when x == y. That is why both PlainPoints stay in the set.

A hash table asks hashCode first and equals only inside one bucket

A HashSet is backed by a HashMap that keeps its elements as keys. To look a key up, the map calls hashCode on it, uses that number to pick one bucket: One slot of a hash table's array. A HashMap picks a key's bucket from its hash code, keeps the keys that land in the same bucket together, and searches them with equals. More in “String hashing, interning and locale traps”, and compares the key only with the entries in that bucket. Each entry keeps the hash code it was stored with, and the map calls equals only on an entry whose stored hash code is the same:

A HashSet lookup: hashCode picks one bucket, stored hash codes are compared, and equals runs only where they match.set.contains(new Point(3, 4))1. hashCode()Objects.hash(3, 4) = 10582. The one bucket that 1058 picks1074 is not 1058:equals is not called3. 1058 = 1058, so equals() runs:true, the set contains itPoint(3, 20)stored hash 1074Point(3, 4)stored hash 1058picks

A hash code picks the bucket; equals confirms the match

Text description of the diagram

The diagram follows the call set.contains(new Point(3, 4)) through three steps.

  1. The set calls hashCode on the argument. Objects.hash(3, 4) gives 1058.
  2. The hash code picks one bucket. Every other bucket is ignored. This bucket holds two entries: Point(3, 20), whose stored hash code is 1074, and Point(3, 4), whose stored hash code is 1058. In a table of 16 buckets both numbers lead to the same bucket.
  3. For Point(3, 20) the stored hash code 1074 is not 1058, so equals is not called at all (a dashed arrow). For Point(3, 4) the hash codes match, so equals runs, returns true, and contains returns true.

If two equal points had different hash codes, step 2 would pick a different bucket and step 3 would never see the equal point.

The diagram shows where the lookup goes. Step through the same call as it happens, one message at a time, and then the same call for EqualsOnlyPoint:

Interactive · message by message One contains call, with and without hashCode Step through set.contains(new Point(3, 4)). Watch which calls the set makes, and the one it skips.
One contains call, with and without hashCode, step 11 of 11. Messages so far: main to the HashSet: contains(new Point(3, 4)); the HashSet to the argument: hashCode(); the argument to the HashSet: 1058; the HashSet to the argument: equals(stored point); the argument to the HashSet: true; the HashSet to main: true; main to the HashSet: contains(EqualsOnlyPoint); the argument to the HashSet: its identity hash code; the HashSet to main: false.mainthe HashSetholds an EqualsOnlyPointthe argumenta second EqualsOnlyPointcontains(new Point(3, 4))hashCode()1058equals(stored point)truetruecontains(EqualsOnlyPoint)its identity hash codefalseno stored hash code matches

The drawing shows the whole scene; its 11 steps are listed below.

Every step as text
  1. contains is called. main asks the set whether it holds new Point(3, 4), a second object equal to the point the set already holds.
  2. hashCode first. Before it compares anything, the set calls hashCode on the argument.
  3. 1058. Objects.hash(3, 4) starts from 1 and works out 31 × (31 × 1 + 3) + 4 = 1058.
  4. One bucket. The number picks one bucket. The set ignores every other bucket and looks only at the entries filed there.
  5. A stored hash that differs. The first entry there, Point(3, 20), was stored with its hash code 1074. 1074 is not 1058, so the set moves on without calling equals.
  6. A stored hash that matches. The next entry was stored with 1058, the same number. Only now does the set call equals, on the argument, with that entry.
  7. equals compares x and y. equals finds that x and y match and returns true.
  8. Found. contains returns true. The set made one equals call, and only on the entry whose stored hash code matched.
  9. Now EqualsOnlyPoint. The same call on a set of EqualsOnlyPoint, a class that overrides equals but not hashCode.
  10. An identity number. The set calls hashCode again, and the argument answers with the hash code it inherits from Object, a number tied to that one object. The recorded run shows that the two points get different ones.
  11. equals never runs. No entry in that bucket was stored with that number, so the set never calls equals, and contains returns false although equals would have said true.

The set skipped Point(3, 20) without calling equals, because its stored hash code was another number, and it called equals once, on the argument, as the HashMap documentation describes get. For EqualsOnlyPoint it never got that far. That design is why the documentation of Object.hashCode lists three rules:

  1. During one run of a program, calling hashCode on the same object must give the same number every time, as long as nothing that equals looks at has changed. The number may differ from one run to the next.
  2. If two objects are equal according to equals, their hash codes must be the same.
  3. Two unequal objects may share a hash code, a hash collision: Two different keys with the same hash code, such as Aa and BB, which both hash to 2112. A hash table keeps both and tells them apart with equals; many collisions in one place make its lookups slow. More in “String hashing, interning and locale traps”. The contract allows it, but every shared hash code makes the table slower, as a later section measures.

EqualsOnlyPoint breaks rule 2. Its two equal points have the identity hash codes of two different objects, so the lookup goes to the wrong bucket and never meets the equal point. The documentation of Object.equals says it directly: whenever you override equals, you generally have to override hashCode too.

Point.hashCode returns Objects.hash(x, y), which hashes its arguments as if they were the elements of a list: start from 1, then for each value multiply by 31 and add the value’s own hash code. For (3, 4) that is 31 × (31 × 1 + 3) + 4 = 1058, the number in the diagram. Two cautions come with it. Objects.hash with a single argument does not return that argument’s hash code (the Javadoc warns about exactly this), so for one field write Objects.hashCode(field) instead. And it creates an array and boxes primitive values on every call, which can matter in code that hashes millions of keys; a hand-written 31 * x + y avoids both.

Key idea

A hash table compares hash codes before it ever calls equals, so objects that equals calls equal must return the same hash code, or the table looks for them in the wrong place.

Checkpoint

Check: How a hash table uses hashCode and equals

  1. Question 1 of 2 Objects.hash starts from 1 and, for each value, multiplies by 31 and adds the value's hash code (an int's hash code is the int itself). What does Objects.hash(5, 2) return?

    Type a number.

    A hint

    Work it the way the section works Objects.hash(3, 4) = 1058.

    Show the answer: Question 1 of 2

    Answer: 1118

    31 × (31 × 1 + 5) + 2 = 31 × 36 + 2 = 1,116 + 2 = 1,118. The same rule gives 1,058 for (3, 4) and 1,074 for (3, 20).

  2. Question 2 of 2 Which rule of the hashCode contract does EqualsOnlyPoint break?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Objects that equals calls equal must have equal hash codes

    The second rule of the contract says equal objects must have equal hash codes. A class that overrides equals without hashCode breaks exactly that rule, and the Object documentation says so in its note on equals.

equals takes an Object, and @Override proves it

Point.equals is one line: return other instanceof Point p && x == p.x && y == p.y;. Each part has a job.

The parameter type is Object, because that is the method HashSet, HashMap, List.contains and every other library method call. A method equals(Point other) is a new, overloading: Declaring several methods with the same name and different parameter lists in one class. The compiler picks one of them for each call from the types of its arguments: an exact or widened match first, then boxing, then varargs. More in “Methods, parameters, return and overloading” method that those libraries never use. The @Override annotation turns that mistake into a compile error, which is the reason to write it every time:

overload/Point.java Java · overload/Point.java
import java.util.Objects;

final class Point {
    private final int x;
    private final int y;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    // Takes a Point, not an Object: this overloads equals instead of overriding it.
    @Override
    public boolean equals(Point other) {
        return other != null && x == other.x && y == other.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }
}
javac refuses an equals that overrides nothing Java · overload/Main.java
void main() {
    Set<Point> points = new HashSet<>();
    points.add(new Point(3, 4));
    IO.println(points.contains(new Point(3, 4)));
}

Output (exit status 1)

Printed as an error (standard error)

Point.java:13: error: method does not override or implement a method from a supertype
    @Override
    ^
1 error
error: compilation failed

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Main.java

Without the annotation the same class compiles, and a HashSet of these points behaves like PlainPoint above.

other instanceof Point p tests the type and, when it matches, names the same object p with the type Point, a type pattern: A pattern such as Point p in other instanceof Point p: the test is true when other is a Point that is not null, and then p names the same object with the type Point, so no cast is needed. Part of Java since JDK 16. (JEP 394, part of Java since JDK 16). The test is false when other is null, so x.equals(null) returns false without a separate check, as the contract requires. It is also false for any object that is not a Point, so comparing a point with a string is simply false.

Then compare the fields that make up the value, and only those:

Field type Compare with Why
int, long, char, boolean … == The values are exact
double, float Double.compare(a, b) == 0, Float.compare == says NaN is not equal to itself, and 0.0 equals -0.0
An object that may be null Objects.equals(a, b) It handles null on either side, then calls equals
An array Arrays.equals(a, b) An array’s own equals is the identity test of Object

The Double documentation explains why its equals and compare treat every NaN as equal and the two zeros as different: it keeps equals reflexive, so an object holding NaN still equals itself.

The documentation of Object.equals asks for five properties, which make it an equivalence relation. It must be reflexive (x.equals(x) is true), symmetric (x.equals(y) and y.equals(x) agree), transitive (if x equals y and y equals z, x equals z), consistent (the answer only changes if the compared fields change) and x.equals(null) must be false. A final class that compares the same fields on both sides keeps all five with little effort. Symmetry and transitivity are the ones that break once a subclass joins in.

Writing equals(Point) instead of equals(Object)

public boolean equals(Point other) compiles, and no collection ever calls it: HashSet and HashMap call equals(Object), which stays the identity test. Declare the parameter as Object, test it with a type pattern, and write @Override so that javac refuses the overload.

Checkpoint

Check: Writing equals(Object) that keeps the contract

  1. Question 1 of 2 This class has no @Override annotations. What does the program print?

    Read the code, then choose one answer.

    final class Point {
        final int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
        public boolean equals(Point other) {
            return other != null && x == other.x && y == other.y;
        }
        public int hashCode() { return 31 * x + y; }
    }
    
    void main() {
        Set<Point> set = new HashSet<>();
        set.add(new Point(3, 4));
        IO.println(set.contains(new Point(3, 4)));
    }
    Show the answer: Question 1 of 2

    Answer: false, because HashSet calls equals(Object), which this class does not override

    equals(Point) is an overload, not an override. The set finds the bucket by the matching hash codes, then calls equals(Object), still the identity test, and gets false. With @Override, javac rejects the method instead.

  2. Question 2 of 2 A value class has a field double price. How should its equals compare that field?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Double.compare(price, other.price) == 0

    Floating-point fields are compared with Double.compare or Float.compare, the comparison the Double documentation describes for equals; integer types with ==; object fields with equals or Objects.equals.

A subclass that adds a field breaks symmetry

A shop wrote Amount when it sold in one currency only: a sum of money in the smallest unit of that currency, so 5,000 for 50.00, compared by that number alone. The class is not final. When the shop started to sell in other currencies, someone extended it: CurrencyAmount adds a currency code, and its equals accepts only other CurrencyAmount objects and compares the code too:

An amount class and a subclass that adds a field

Java (Amount.java) · symmetry/Amount.java

/** An amount of money in the smallest unit of its currency (paise, cents). Not final, so a subclass can add a field. */
class Amount {
    final long units;

    Amount(long units) {
        this.units = units;
    }

    @Override
    public boolean equals(Object other) {
        return other instanceof Amount a && units == a.units;
    }

    @Override
    public int hashCode() {
        return Long.hashCode(units);
    }
}

Java (CurrencyAmount.java) · symmetry/CurrencyAmount.java

import java.util.Objects;

/** Adds a currency code and compares it, so it accepts only other CurrencyAmounts. */
class CurrencyAmount extends Amount {
    final String currency;

    CurrencyAmount(long units, String currency) {
        super(units);
        this.currency = Objects.requireNonNull(currency, "currency");
    }

    @Override
    public boolean equals(Object other) {
        return other instanceof CurrencyAmount c && super.equals(c) && currency.equals(c.currency);
    }

    @Override
    public int hashCode() {
        return Objects.hash(units, currency);
    }
}
Each side gives a different answer Java · symmetry/Main.java
// The same 5,000 units with and without a currency, compared both ways round.
void main() {
    Amount plain = new Amount(5_000);
    Amount usd = new CurrencyAmount(5_000, "USD");
    IO.println("plain.equals(usd) = " + plain.equals(usd));
    IO.println("usd.equals(plain) = " + usd.equals(plain));
    IO.println("same hash code?     " + (plain.hashCode() == usd.hashCode()));

    // Each collection calls equals on a different side, so they disagree.
    Set<Amount> setOfPlain = new HashSet<>(List.of(plain));
    List<Amount> listOfUsd = new ArrayList<>(List.of(usd));
    IO.println("HashSet [plain] contains usd:   " + setOfPlain.contains(usd));
    IO.println("ArrayList [usd] contains plain: " + listOfUsd.contains(plain));
}

Output

plain.equals(usd) = true
usd.equals(plain) = false
same hash code?     false
HashSet [plain] contains usd:   false
ArrayList [usd] contains plain: true

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Main.java

The plain amount says it equals the 5,000 US cents, because a CurrencyAmount is an instance of Amount and the units match. The dollar amount says the opposite. Collections inherit the disagreement: a HashSet holding plain says it does not contain usd, while an ArrayList holding usd says it contains plain, because ArrayList.contains calls equals on its argument. Which answer you get depends on which object happens to be on the left. The third line shows a second break: plain.equals(usd) is true while the two hash codes differ, which is rule 2 of the hash code contract broken, so the HashSet never even asks equals.

Letting CurrencyAmount accept plain amounts as well would only move the problem. Then $50.00 equals the plain 5,000, which equals €50.00, while the dollars and the euros differ: transitivity is gone, and the shop would call two different prices equal. As long as equals tests with instanceof, a subclass that adds a field to a value class has to give up one of the two properties. There are three ways out:

Way out plain and usd, both ways What it costs
Make value classes final Cannot arise No subclass can exist at all
Compare classes with getClass() false both ways A subclass that adds no state never equals its parent
Hold an Amount instead of extending it false both ways CurrencyAmount is no longer an Amount for code that wants one

A final value class leaves nobody a way to subclass it, and records, three lessons on, are always final. Comparing classes exactly with getClass() == other.getClass() makes an Amount and a CurrencyAmount unequal either way round, at the price of a subclass that adds no state never equalling its parent. A CurrencyAmount that holds an Amount and a currency code, instead of extending Amount, has its own equals and no relationship to break; the module on inheritance comes back to that choice.

Checkpoint

Check: Symmetry when a subclass adds a field

  1. Question 1 of 2 List<Amount> list = new ArrayList<>(List.of(plain)); holds the plain amount of 5,000 units. What does list.contains(usd) return for the dollar amount of this section?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: false

    The run shows ArrayList [usd] contains plain as true, because there the argument is plain. Swap the two and the argument is usd, whose equals says false: which answer you get depends on which object is asked.

  2. Question 2 of 2 Amount's equals changes to compare getClass() == other.getClass() and then the units. CurrencyAmount's equals stays as it is. What do plain.equals(usd) and usd.equals(plain) return now?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: false and false, so equals is symmetric again

    Comparing classes exactly makes an Amount and a CurrencyAmount unequal either way round. The price is that a subclass which adds no state is never equal to its parent either.

A constant hash code keeps answers right and makes lookups slow

return 42; is a legal hashCode: equal objects get equal hash codes, so the contract holds and every answer is right. This program counts how many times HashMap calls equals while it looks up every key once, for keys with a spread-out hash code, keys that all return 42, and keys that return 42 but also implement Comparable:

equals calls for one lookup of every key Java · collisions/Main.java
// Counts the equals calls HashMap makes while it looks up every key once, for three hashCode methods.
static long equalsCalls = 0;

/** A spread-out hash code: every id gets its own. */
static final class SpreadKey {
    final int id;
    SpreadKey(int id) { this.id = id; }
    @Override public boolean equals(Object o) { equalsCalls++; return o instanceof SpreadKey k && k.id == id; }
    @Override public int hashCode() { return Integer.hashCode(id); }
}

/** Legal but useless: every key gets the same hash code. */
static final class ConstantKey {
    final int id;
    ConstantKey(int id) { this.id = id; }
    @Override public boolean equals(Object o) { equalsCalls++; return o instanceof ConstantKey k && k.id == id; }
    @Override public int hashCode() { return 42; }
}

/** The same constant hash code, but the keys can be put in order. */
static final class SortableConstantKey implements Comparable<SortableConstantKey> {
    final int id;
    SortableConstantKey(int id) { this.id = id; }
    @Override public boolean equals(Object o) { equalsCalls++; return o instanceof SortableConstantKey k && k.id == id; }
    @Override public int hashCode() { return 42; }
    @Override public int compareTo(SortableConstantKey other) { return Integer.compare(id, other.id); }
}

long lookups(int n, IntFunction<Object> newKey) {
    Map<Object, Integer> map = new HashMap<>();
    for (int i = 0; i < n; i++) map.put(newKey.apply(i), i);
    equalsCalls = 0;
    for (int i = 0; i < n; i++) {
        if (map.get(newKey.apply(i)) == null) throw new AssertionError("lost key " + i);   // a new, equal key
    }
    return equalsCalls;
}

void main() {
    IO.println("keys    spread hash  constant 42  constant 42 + Comparable");
    for (int n : new int[] {1_000, 10_000}) {
        IO.println("%-7d %-12d %-12d %d".formatted(n,
                lookups(n, SpreadKey::new), lookups(n, ConstantKey::new), lookups(n, SortableConstantKey::new)));
    }
}

Output

keys    spread hash  constant 42  constant 42 + Comparable
1000    1000         501499       10405
10000   10000        50014999     138852

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Main.java

With a spread-out hash code, each lookup finds its key at the first entry it compares: 1,000 lookups, 1,000 calls. With a constant hash code every key lands in the same bucket. Since JDK 8 a bucket that grows past a small size turns from a list into a balanced tree (JEP 180), but a tree needs an order, and keys with equal hash codes give it none. So each lookup searches the bucket until it meets its key: 501,499 calls for 1,000 keys, about n² ÷ 2. Ten times the keys means a hundred times the work, 50,014,999 calls for 10,000 lookups.

The third column is the escape hatch the HashMap documentation mentions: when keys are Comparable, the map may use their order to break ties. The tree is then searched like a sorted tree, about log₂ n comparisons per lookup, and 10,000 lookups cost 138,852 calls instead of fifty million. That rescues a hash code that collides by accident, but it is still about fourteen times the work of a spread-out hash, so the fix for return 42; is a real hash code. Counting calls rather than timing them gives the same numbers on every computer, which is why the program does it.

Key idea

A constant hash code keeps every answer right and puts every key in one bucket, so the work of n lookups grows like n² ÷ 2: 501,499 equals calls for 1,000 keys instead of 1,000.

Checkpoint

Check: What a constant hash code costs

  1. Question 1 of 2 In the recorded run, 1,000 keys whose hashCode returns 42 needed 501,499 equals calls for 1,000 lookups. About how many would 2,000 such keys need for 2,000 lookups?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: About 2 million, because the work grows with the square of the number of keys

    The total is about n² ÷ 2: 501,499 for 1,000 keys and 50,014,999 for 10,000. For 2,000 keys it is about 2,000² ÷ 2 = 2,000,000.

  2. Question 2 of 2 Why did the keys that return 42 but implement Comparable need only 138,852 equals calls for 10,000 lookups?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The map used compareTo to order the shared bucket and searched it like a sorted tree

    The HashMap documentation says that when keys are Comparable, the map may use their order to break ties. A sorted tree needs about log₂ n comparisons per lookup instead of about n ÷ 2.

A key that changes after it goes in is lost

A hash table remembers each entry under the hash code the key had when it went in. If a field that hashCode uses changes afterwards, the entry sits in the bucket for the old number while the key now produces a new one. This Seat can move to another coach:

lostkey/Seat.java Java · lostkey/Seat.java
import java.util.Objects;

/** A seat whose coach can be changed after it is created: the mistake this example shows. */
final class Seat {
    private String coach;
    private final int number;

    Seat(String coach, int number) {
        this.coach = coach;
        this.number = number;
    }

    void moveTo(String newCoach) {
        coach = newCoach;
    }

    @Override
    public boolean equals(Object other) {
        return other instanceof Seat s && number == s.number && coach.equals(s.coach);
    }

    @Override
    public int hashCode() {
        return Objects.hash(coach, number);
    }

    @Override
    public String toString() {
        return coach + "-" + number;
    }
}
The booking is still there, and nothing can find it Java · lostkey/Main.java
void main() {
    Map<Seat, String> bookings = new HashMap<>();
    Seat seat = new Seat("S4", 23);
    bookings.put(seat, "Asha");
    IO.println("booked: " + bookings + ", filed under hash code " + seat.hashCode());

    seat.moveTo("S5");                  // changes a field that hashCode uses, while seat is a key
    IO.println("map now prints:       " + bookings + ", key's hash code now " + seat.hashCode());
    IO.println("get(seat):            " + bookings.get(seat));
    IO.println("get(new Seat S5-23):  " + bookings.get(new Seat("S5", 23)));
    IO.println("get(new Seat S4-23):  " + bookings.get(new Seat("S4", 23)));
    IO.println("remove(seat):         " + bookings.remove(seat) + ", size still " + bookings.size());

    bookings.keySet().removeIf(key -> key == seat);   // only a full scan by identity finds it
    IO.println("after removeIf:       size " + bookings.size());
}

Output

booked: {S4-23=Asha}, filed under hash code 82359
map now prints:       {S5-23=Asha}, key's hash code now 82390
get(seat):            null
get(new Seat S5-23):  null
get(new Seat S4-23):  null
remove(seat):         null, size still 1
after removeIf:       size 0

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Main.java

The map was given the hash code 82359, the number of S4-23, and after the move the same key says 82390. Step through where each lookup goes:

Interactive · step by step A key that changes is filed under its old hash code Step through lostkey/Main.java. The boxes on the left are keys, the ones on the right the buckets their hash codes pick.
A key that changes is filed under its old hash code, step 6 of 6. Showing seat (S5-23, hash 82390), new Seat("S5", 23), new Seat("S4", 23), bucket for 82359 (empty after removeIf), bucket for 82390. In focus: bucket for 82359.putseatS5-23, hash 82390new Seat("S5", 23)hash 82390new Seat("S4", 23)hash 82359bucket for 82359empty after removeIfbucket for 82390no entry

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. put files the entry. bookings.put(seat, "Asha") files the entry under the hash code the key has at that moment, 82359, as the first line of the run prints. The entry keeps that number.
  2. The key moves. seat.moveTo("S5") changes coach, a field that both equals and hashCode read. The key in the map is the same object, so it now says S5-23 and hashes to 82390. The entry stays where it was filed.
  3. get(seat). get(seat) works out 82390 and searches the bucket that number picks, where nothing was ever filed, so it returns null.
  4. A new S5-23. A new Seat for S5-23 hashes to 82390 too and searches the same empty bucket. null again.
  5. A new S4-23. A new Seat for S4-23 hashes to 82359 and finds the entry, stored with 82359. Then equals compares it with the key, which now says S5, and returns false, so get still returns null.
  6. Only a scan finds it. remove(seat) fails for the same reason as get(seat). Only removeIf over keySet(), which visits every key and compares each with == as the program asks, finds the entry and removes it.

Printing the map shows the booking under S5-23, yet every lookup fails. get(seat) and a new S5-23 compute the new hash code and search the wrong bucket. A new S4-23 finds the right bucket and a matching stored hash code, but equals now compares it with a key that says S5. remove fails for the same reasons, and only a scan of every key, comparing by identity, gets rid of the entry. The documentation of Map warns that a map’s behaviour is not specified at all when a key changes in a way that affects equals; Set says the same for its elements.

The rule that follows: the fields that equals and hashCode read must not change while the object is a key or a set element. The reliable way to keep that rule is to make those fields final, which is where the next lesson, on immutability, starts.

Checkpoint

Check: Keys that change after they go in

  1. Question 1 of 2 After seat.moveTo("S5"), why does get(new Seat("S4", 23)) return null, although the entry was filed under the hash code of S4-23?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It reaches the entry by its hash code, then equals compares it with the key, which now says S5

    82359 leads the lookup to the right entry, but equals compares S4-23 with the key, which says S5-23 now. No lookup by value can find the entry any more: S5-23 hashes to another bucket, S4-23 fails equals.

  2. Question 2 of 2 How do you make Seat safe as a HashMap key?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Make coach final and drop moveTo; a move becomes a new Seat, put under its own hash code

    The fields that equals and hashCode read must not change while the object is a key or a set element. Making them final is the dependable way, which is where the next lesson, on immutability, starts.

toString is written for people, and leaves secrets out

Without an override, toString returns the class name, an @ and the hash code in hexadecimal. That text names the class and nothing else. The Object documentation recommends that every class override it with a concise text that a person can read easily, and requires the result never to be null.

An order with a toString, and one without

Java (Order.java) · tostring/Order.java

import java.util.List;

/** A toString written for whoever reads the log: what the order is, without the card number. */
final class Order {
    private final String id;
    private final List<String> items;
    private final String cardNumber;

    Order(String id, List<String> items, String cardNumber) {
        this.id = id;
        this.items = List.copyOf(items);
        this.cardNumber = cardNumber;
    }

    @Override
    public String toString() {
        String last4 = cardNumber.substring(cardNumber.length() - 4);
        return "Order[id=" + id + ", items=" + items + ", card=**** " + last4 + "]";
    }
}

Java (PlainOrder.java) · tostring/PlainOrder.java

/** No toString: printing it shows Object's default. */
final class PlainOrder {
    private final String id;

    PlainOrder(String id) {
        this.id = id;
    }
}
Printed by println, string concatenation and a list Java · tostring/Main.java
void main() {
    IO.println(new PlainOrder("A-1009"));

    Order order = new Order("A-1009", List.of("kettle", "mug"), "1234567812345678");
    IO.println(order);                                  // println calls toString
    IO.println("placed " + order);                      // so does string concatenation
    IO.println(List.of(order));                         // and a list, which calls it for each element
}

Output

PlainOrder@1f0f1111
Order[id=A-1009, items=[kettle, mug], card=**** 5678]
placed Order[id=A-1009, items=[kettle, mug], card=**** 5678]
[Order[id=A-1009, items=[kettle, mug], card=**** 5678]]

This output changes from run to run: The hexadecimal number after PlainOrder@ is the object's identity hash code, which can differ from one run to the next.

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Main.java

println, string concatenation and a collection’s own toString all call your toString, so the method ends up in logs, exception messages and debugger views whether you planned it or not. Three habits keep it useful:

  • Name the class and the fields that identify the value, in one line. Order[id=A-1009, items=[kettle, mug]] tells you which order and what is in it.
  • Leave out secrets. The OWASP Logging Cheat Sheet lists passwords, session identifiers, access tokens and payment card data among the things that should not reach logs as they are. Order prints only the last four digits of the card, so logging an order does not leak the card number.
  • Treat it as text for people. The Object documentation says the output is not necessarily stable over time or across runs of the JVM, and a format meant for reading changes when someone improves it, so never parse it back into an object. When you need the identity form even though a class overrides toString, Objects.toIdentityString(object) (JDK 19 and later) returns it.

Records, three lessons on, generate all three methods from their fields, in the same shape as Order’s toString.

Checkpoint

Check: A toString for people and logs

  1. Question 1 of 2 Which of these call Order's toString?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • "placed " + order
    • List.of(order).toString()
    • IO.println(order)

    println, string concatenation and a collection's toString all end up calling yours, which is why toString reaches logs, exception messages and debugger views whether you planned it or not.

  2. Question 2 of 2 An Order holds a payment card number. What should its toString print?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The order id, the items and only the last four digits of the card

    Write toString for the person reading a log: the class and the fields that identify the value, in one line, and nothing secret, as Order does with card=**** 5678.

Practise: a ticket that works as a key

Ticket stands for one seat on one train: a train number, a coach and a seat. Two tickets with the same three values must count as the same ticket, in a HashSet and as a HashMap key. The starter class has the fields and a constructor, and the sample checks use it the way a booking system would:

ticket/Ticket.java Java · ticket/Ticket.java
import java.util.Objects;

/** One seat on one train. Two tickets for the same train, coach and seat should count as the same ticket. */
final class Ticket {
    private final int train;
    private final String coach;
    private final int seat;

    Ticket(int train, String coach, int seat) {
        this.train = train;
        this.coach = Objects.requireNonNull(coach, "coach");
        this.seat = seat;
    }

    // Your turn: override equals(Object), hashCode() and toString().
}
The sample checks against the starter class Java · ticket/TicketChecks.java
// Sample checks for Ticket. Edit Ticket.java, then run: java ticket/TicketChecks.java
int passed = 0;
int total = 0;

void check(String what, boolean ok) {
    total++;
    if (ok) passed++;
    IO.println((ok ? "pass  " : "FAIL  ") + what);
}

void main() {
    Ticket a = new Ticket(12951, "B2", 34);
    Ticket same = new Ticket(12951, "B2", 34);

    check("equal tickets are equal, both ways", a.equals(same) && same.equals(a));
    check("another seat is not equal", !a.equals(new Ticket(12951, "B2", 35)));
    check("another coach is not equal", !a.equals(new Ticket(12951, "B3", 34)));
    check("another train is not equal", !a.equals(new Ticket(12952, "B2", 34)));
    check("equals(null) is false", !a.equals(null));
    check("a String is never equal to a Ticket", !a.equals("12951 B2 34"));
    check("equal tickets have equal hash codes", a.hashCode() == same.hashCode());

    Set<Ticket> unique = new HashSet<>(List.of(a, same, new Ticket(12951, "B2", 35)));
    check("a HashSet keeps one of two equal tickets", unique.size() == 2);
    Map<Ticket, String> holders = new HashMap<>();
    holders.put(a, "Asha");
    check("a HashMap finds the holder by an equal ticket", "Asha".equals(holders.get(same)));
    check("toString is Ticket[train=12951, coach=B2, seat=34]", a.toString().equals("Ticket[train=12951, coach=B2, seat=34]"));

    IO.println(passed + " of " + total + " checks passed");
    if (passed < total) System.exit(1);
}

Output (exit status 1)

FAIL  equal tickets are equal, both ways
pass  another seat is not equal
pass  another coach is not equal
pass  another train is not equal
pass  equals(null) is false
pass  a String is never equal to a Ticket
FAIL  equal tickets have equal hash codes
FAIL  a HashSet keeps one of two equal tickets
FAIL  a HashMap finds the holder by an equal ticket
FAIL  toString is Ticket[train=12951, coach=B2, seat=34]
5 of 10 checks passed

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java TicketChecks.java

Edit Ticket.java until java ticket/TicketChecks.java prints 10 of 10 checks passed:

  1. Override equals(Object) with @Override. Use a type pattern and compare all three fields: the numbers with == and the coach with equals.
  2. Override hashCode so that it reads the same three fields, for example with Objects.hash.
  3. Override toString to return exactly Ticket[train=12951, coach=B2, seat=34] for that ticket.

Then break it on purpose: change the parameter of equals to Ticket, keep the body working (with a null check), and delete @Override. The checks that call equals directly still pass, because the compiler picks the overload, but the HashSet and HashMap checks fail, as they did for EqualsOnlyPoint.

Then test yourself on the seven ideas, from what Object compares to toString. The idea you have answered worst so far comes up first, and each choice explains itself as soon as you pick it:

Practice that adapts to you

Practise this lesson

16 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 16 Two new PlainPoint(3, 4) objects are added to one HashSet. How many elements does the set hold?

    Choose one answer.

    Show the answer: Question 1 of 16

    Answer: 2

    With the methods of Object, every object is equal only to itself, so the first row of the run says set size 2 and contains false.

  2. Question 2 of 16 Which of these classes should keep the identity-based equals and hashCode of Object?

    Choose one answer.

    Show the answer: Question 2 of 16

    Answer: An open network connection

    Object's equals is right for objects that have an identity of their own and wrong for values. Values override equals and hashCode; resources and running things usually do not.

  3. Question 3 of 16 When HashSet.contains(x) runs, what happens first?

    Choose one answer.

    Show the answer: Question 3 of 16

    Answer: x.hashCode() is called, and the number picks one bucket

    hashCode picks the bucket, the stored hash codes are compared next, and equals runs only on an entry whose stored hash code is the same.

  4. Question 4 of 16 Point(3, 4) and Point(2, 35) both have the hash code 1058. A set holds Point(3, 4). What does set.contains(new Point(2, 35)) return?

    Choose one answer.

    Show the answer: Question 4 of 16

    Answer: false, because the hash codes match, so equals runs and compares x and y

    31 × (31 × 1 + 2) + 35 = 1,058, the same number as for (3, 4). The set reaches the stored Point(3, 4), calls equals, and gets false.

  5. Question 5 of 16 What does @Override on public boolean equals(Point other) do?

    Choose one answer.

    Show the answer: Question 5 of 16

    Answer: It makes javac refuse the method, because it overrides nothing in Object

    @Override turns the overload mistake into a compile error: "method does not override or implement a method from a supertype". Write it on every equals, hashCode and toString.

  6. Question 6 of 16 equals is return other instanceof Point p && x == p.x && y == p.y;. What does point.equals(null) return?

    Choose one answer.

    Show the answer: Question 6 of 16

    Answer: false

    A type pattern tests the type and names the object in one step, and it is false for null, which is exactly what the contract's rule for null asks.

  7. Question 7 of 16 A value class holds int[] digits, and its equals compares Objects.equals(digits, other.digits). Two objects are built from two arrays that both hold 4, 2. What does equals return?

    Choose one answer.

    Show the answer: Question 7 of 16

    Answer: false, because an array's own equals is Object's identity test

    Compare array fields with Arrays.equals and hash them with Arrays.hashCode; their own equals and hashCode are the identity versions of Object.

  8. Question 8 of 16 What does it mean that equals is symmetric?

    Choose one answer.

    Show the answer: Question 8 of 16

    Answer: x.equals(y) and y.equals(x) always give the same answer

    The Object documentation lists five properties for equals: reflexive, symmetric, transitive, consistent, and false for null. A subclass that adds a field breaks symmetry or transitivity.

  9. Question 9 of 16 A dollar amount must never equal a plain amount, and Amount must stay a class others can extend. Which design keeps equals symmetric and transitive?

    Choose one answer.

    Show the answer: Question 9 of 16

    Answer: CurrencyAmount holds an Amount and a currency code instead of extending Amount

    As long as equals tests with instanceof, a subclass that adds a field cannot keep both symmetry and transitivity. Make value classes final, compare classes exactly, or use composition.

  10. Question 10 of 16 Is public int hashCode() { return 42; } a legal hash code?
    Show the answer: Question 10 of 16

    Answer: Yes, because equal objects get equal hash codes, but every key then shares one bucket

    A constant hash code keeps every answer right and puts every key in one bucket, so lookups compare with about half of all keys.

  11. Question 11 of 16 For keys whose hashCode returns 42, 1,000 lookups among 1,000 keys cost 501,499 equals calls and 10,000 among 10,000 keys cost 50,014,999. About how many times the work is that?

    Type a number.

    Show the answer: Question 11 of 16

    Answer: 100 times (anything from 98 to 102 counts)

    50,014,999 ÷ 501,499 is about 99.7, so about 100 times. Ten times the keys gives a hundred times the work, because the total grows with the square of n.

  12. Question 12 of 16 Seat compares coach and number in equals. Which hashCode keeps the contract?

    Choose one answer.

    Show the answer: Question 12 of 16

    Answer: Objects.hash(coach, number)

    A hash code should read the same fields as equals: all of them for the best spread, never anything that changes on its own or that equals ignores.

  13. Question 13 of 16 What does the Map documentation say about a key that changes in a way that affects equals while it is in the map?

    Choose one answer.

    Show the answer: Question 13 of 16

    Answer: The behaviour of the map is not specified

    A changed key leaves the map in a state its documentation does not define. The Set documentation says the same for set elements.

  14. Question 14 of 16 A HashSet<Seat> holds the seat S4-23. After seat.moveTo("S5"), what does set.contains(seat) return?

    Choose one answer.

    Show the answer: Question 14 of 16

    Answer: false, because the set searches the bucket for 82390 and the element is filed under 82359

    The element's hash code is now 82390, the number of S5-23, and the set looks only in the bucket that number picks. The element was filed under 82359.

  15. Question 15 of 16 PlainOrder has no toString. What does IO.println(new PlainOrder("A-1009")) print?

    Choose one answer.

    Show the answer: Question 15 of 16

    Answer: PlainOrder, an @ and a hexadecimal number

    Without an override, println shows getClass().getName() + "@" + Integer.toHexString(hashCode()), which names the class and nothing else.

  16. Question 16 of 16 A colleague's log tool reads orders back by parsing Order's toString. What is wrong with that?

    Choose one answer.

    Show the answer: Question 16 of 16

    Answer: toString is text for people, and its output is not promised to stay the same, so an improvement breaks the tool

    Treat toString as text for people. When a program needs the data, give it a method or a format meant for programs.

Interview questions

Fresher · What is the contract between equals and hashCode in Java?

Equal objects must have equal hash codes: if a.equals(b) is true, a.hashCode() == b.hashCode() must be true too. The reverse is not required, so unequal objects may share a hash code, but every collision makes hash tables slower. A hash code must also stay the same during a run as long as the fields that equals compares do not change. The reason is how HashMap and HashSet work: they use the hash code to pick a bucket and call equals only inside it, so an object whose equal twin has another hash code is never found. That is why overriding equals alone is a bug, and why both methods should read exactly the same fields. A good answer adds that fields used by both should be final, because a key whose hash code changes after insertion is lost in the map.

Two longer questions of this module start from this lesson: a set of orders that keeps growing, and a cache whose key class returns 42 from hashCode.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A service keeps Order objects in a HashSet to drop duplicates. The set keeps growing, and contains() returns false for orders you can see in it when you print the set. What could cause this, and how do you find out which cause it is?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A key class returns 42 from hashCode "to be safe". Every answer is still right, but a cache keyed by it got slower and slower as it grew. Explain why, estimate the work for 10,000 lookups, and say what changes if the key class implements Comparable.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Object treats every object as unique; a value class overrides equals so that equal values compare equal, and hashCode with it, because hash tables compare hash codes before they ever call equals.
  • Override equals(Object) with @Override, a type pattern and the same fields as hashCode: an equals(Point) overload compiles silently, and no collection ever calls it.
  • Compare integer fields with ==, floating-point ones with Double.compare, objects with Objects.equals and arrays with Arrays.equals; a non-final class whose subclass adds a field cannot keep equals symmetric.
  • A constant hash code keeps every answer right and makes lookups slow: 1,000 lookups cost 501,499 equals calls instead of 1,000.
  • A key whose hash code changes after it goes into a map or a set is lost, so the fields that equals and hashCode read should be final.
  • Write toString for the person reading a log: the class and its identifying fields, never a password or a full card number, and never parse it back.

Terms in this lesson

equivalence relation
A relation that is reflexive (x equals x), symmetric (if x equals y, then y equals x) and transitive (if x equals y and y equals z, then x equals z). The Object documentation requires equals to be one.
hash code
The int that hashCode returns for an object. A hash table uses it to pick a bucket, so objects that equals calls equal must return the same hash code; unequal objects may share one, which only slows the table down.
identity hash code
The hash code Object.hashCode gives an object whose class does not override it, and System.identityHashCode gives any object: a number tied to that one object rather than to its field values.
type pattern
A pattern such as Point p in other instanceof Point p: the test is true when other is a Point that is not null, and then p names the same object with the type Point, so no cast is needed. Part of Java since JDK 16.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.