toString, equals, and hashCode
The three methods every Java class needs
After this lesson, you will be able to:
- Explain why the default
toString()output is useless and write a custom one - Distinguish between
==(reference equality) and.equals()(value equality) - Write a correct
equals()method using the standard five-step recipe - Explain the equals/hashCode contract and why violating it breaks collections
- Implement
hashCode()usingObjects.hash() - Build a complete
Studentclass with all three methods
The Mystery Hash
Create a Student object and print it:
Student alice = new Student("S001", "Alice");
System.out.println(alice);
Output:
Student@6d06d69c
That is the class name followed by an @ sign and a hexadecimal number. It is the default toString() inherited from Java’s Object class, and it tells you almost nothing useful. You wanted to see "Alice (ID: S001)". Instead you got a memory address.
This is the first of three problems that every Java class must solve. The default behavior inherited from Object is almost never what you want. You need to override three methods: toString(), equals(), and hashCode().
Problem 1: toString() — Making Objects Printable
When you call System.out.println(someObject), Java automatically calls someObject.toString(). The default implementation from Object produces that useless ClassName@hexHash output. Override it to return something meaningful.
Writing a Custom toString()
public class Student {
private final String studentId;
private String name;
private double gpa;
private int credits;
public Student(final String studentId, final String name) {
if (studentId == null || studentId.trim().isEmpty()) {
throw new IllegalArgumentException("ID cannot be null or empty");
}
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
this.studentId = studentId;
this.name = name;
this.gpa = 0.0;
this.credits = 0;
}
// Getters and setters omitted for brevity
@Override
public String toString() {
return String.format("%s (ID: %s, GPA: %.2f, Credits: %d)",
name, studentId, gpa, credits);
}
}
Now:
Student alice = new Student("S001", "Alice");
alice.setGpa(3.8);
alice.setCredits(45);
System.out.println(alice); // Alice (ID: S001, GPA: 3.80, Credits: 45)
The @Override annotation tells the compiler you intend to replace the parent class’s method. If you misspell the method name (say, tostring() with a lowercase S), the compiler will catch the mistake and give you an error instead of silently creating a new, unused method.
From CSCD 110: In Python, you define
__str__to control whatprint()shows. Java’s equivalent istoString(). Same idea, different syntax:def __str__(self)becomespublic String toString(). Both are called automatically when you print an object.
What does System.out.println(someObject) print if you have NOT overridden toString()?
Problem 2: equals() — Comparing Objects by Content
Now for a subtler problem. Create two Student objects with identical data:
Student alice1 = new Student("S001", "Alice");
alice1.setGpa(3.8);
Student alice2 = new Student("S001", "Alice");
alice2.setGpa(3.8);
System.out.println(alice1 == alice2); // false
System.out.println(alice1.equals(alice2)); // false (!)
Both are false. The == operator compares references — it asks “are these the exact same object in memory?” Since alice1 and alice2 were created separately with new, they are two different objects. Different memory addresses, so == returns false.
But equals() also returns false. Why? Because the default equals() inherited from Object is identical to ==:
// Default equals() from Object
public boolean equals(Object obj) {
return this == obj; // Just reference comparison!
}
To compare by content, you must override equals().
The Five-Step equals() Recipe
There is a standard recipe that satisfies Java’s contract for equals(). Follow it exactly:
@Override
public boolean equals(final Object obj) {
// 1. Same reference? Short-circuit to true.
if (this == obj) {
return true;
}
// 2. Null? Never equal to null.
if (obj == null) {
return false;
}
// 3. Different class? Not equal.
if (getClass() != obj.getClass()) {
return false;
}
// 4. Cast to our type.
Student other = (Student) obj;
// 5. Compare all relevant fields.
return Objects.equals(this.studentId, other.studentId)
&& Objects.equals(this.name, other.name)
&& Double.compare(this.gpa, other.gpa) == 0
&& this.credits == other.credits;
}
The field comparison rules:
- Object fields (String, etc.): use
Objects.equals(a, b)— handlesnullsafely. - Primitive int, long, boolean: use
==directly. - Primitive double, float: use
Double.compare(a, b) == 0— avoids floating-point precision traps.
Key Insight:
==tests whether two variables point to the same object (reference equality).equals()tests whether two objects contain the same data (value equality). They answer different questions. For objects, you almost always wantequals().
Now it works:
System.out.println(alice1.equals(alice2)); // true!
System.out.println(alice1 == alice2); // still false (different objects)
Memory diagram:
Stack Heap
┌──────────┐
│ alice1: ──┼──────────► [ Student: "S001", "Alice", 3.8, 0 ]
├──────────┤
│ alice2: ──┼──────────► [ Student: "S001", "Alice", 3.8, 0 ]
└──────────┘
Two separate objects, same content.
== returns false. equals() returns true.
Given two Student objects with identical fields created by separate new calls, what do == and equals() return (assuming equals() is properly overridden)?
The equals() Contract
Java specifies strict rules that equals() must follow. If you violate these, collections and algorithms that depend on equality will behave unpredictably.
| Property | Requirement |
|---|---|
| Reflexive | x.equals(x) is always true |
| Symmetric | If x.equals(y), then y.equals(x) |
| Transitive | If x.equals(y) and y.equals(z), then x.equals(z) |
| Consistent | Repeated calls return the same result (if the objects have not changed) |
| Null-safe | x.equals(null) is always false |
The five-step recipe satisfies all five properties automatically. Follow the recipe and you are safe.
Why the Contract Matters
Java’s ArrayList.contains(), ArrayList.indexOf(), and ArrayList.remove() all use equals() internally to find objects:
List<Student> roster = new ArrayList<>();
Student alice = new Student("S001", "Alice");
roster.add(alice);
// Without equals() override --- fails to find the copy:
Student aliceCopy = new Student("S001", "Alice");
System.out.println(roster.contains(aliceCopy)); // false (!)
// With equals() override --- finds it:
System.out.println(roster.contains(aliceCopy)); // true
If contains() cannot find objects with matching data, your program logic breaks silently. No exception, no error message — just wrong results.
Problem 3: hashCode() — The equals/hashCode Contract
If you override equals(), you must also override hashCode(). This is not optional. It is a contract:
If two objects are equal according to
equals(), they must return the samehashCode().
The reverse is not required: two unequal objects may have the same hash code (called a collision). But equal objects with different hash codes will break HashMap and HashSet.
What is a Hash Code?
A hash code is an integer that represents an object. Collections like HashMap use it as a bucket number to quickly locate objects. Think of it like a library call number — it tells you which shelf to check.
The default hashCode() from Object is based on the memory address, so two different objects always get different hash codes, even if their content is identical. That violates the contract if you have overridden equals() to compare by content.
Implementing hashCode() with Objects.hash()
Java provides Objects.hash() to compute a hash code from multiple fields. Include exactly the same fields that you use in equals():
import java.util.Objects;
@Override
public int hashCode() {
return Objects.hash(studentId, name, gpa, credits);
}
That is it. One line. Objects.hash() combines all the field values into a single integer. Because it uses the same fields as equals(), the contract is satisfied: equal objects produce equal hash codes.
The Trick: Always include exactly the same fields in
hashCode()andequals(). If you add a field to one, add it to the other. Let your IDE generate both methods together to avoid this mistake. IntelliJ and Eclipse both have “Generate equals() and hashCode()” as a single menu item.
You override equals() but forget to override hashCode(). What happens when you store the object in a HashMap?
Complete Example: Student with All Three Methods
Here is the full Student class with toString(), equals(), and hashCode(). This is the standard you should follow for every class you write.
import java.util.Objects;
public class Student {
private final String studentId;
private String name;
private double gpa;
private int credits;
public Student(final String studentId, final String name) {
if (studentId == null || studentId.trim().isEmpty()) {
throw new IllegalArgumentException("ID cannot be null or empty");
}
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
this.studentId = studentId;
this.name = name;
this.gpa = 0.0;
this.credits = 0;
}
public String getStudentId() {
return studentId;
}
public String getName() {
return name;
}
public void setName(final String newName) {
if (newName == null || newName.trim().isEmpty()) {
throw new IllegalArgumentException("Name cannot be empty");
}
this.name = newName;
}
public double getGpa() {
return gpa;
}
public void setGpa(final double newGpa) {
if (newGpa < 0.0 || newGpa > 4.0) {
throw new IllegalArgumentException("GPA must be between 0.0 and 4.0");
}
this.gpa = newGpa;
}
public int getCredits() {
return credits;
}
public void setCredits(final int newCredits) {
if (newCredits < 0) {
throw new IllegalArgumentException("Credits cannot be negative");
}
this.credits = newCredits;
}
@Override
public String toString() {
return String.format("%s (ID: %s, GPA: %.2f, Credits: %d)",
name, studentId, gpa, credits);
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Student other = (Student) obj;
return Objects.equals(this.studentId, other.studentId)
&& Objects.equals(this.name, other.name)
&& Double.compare(this.gpa, other.gpa) == 0
&& this.credits == other.credits;
}
@Override
public int hashCode() {
return Objects.hash(studentId, name, gpa, credits);
}
}
Usage demonstrating all three methods:
import java.util.ArrayList;
import java.util.List;
public class StudentDemo {
public static void main(final String[] args) {
Student alice1 = new Student("S001", "Alice");
alice1.setGpa(3.8);
alice1.setCredits(45);
Student alice2 = new Student("S001", "Alice");
alice2.setGpa(3.8);
alice2.setCredits(45);
// toString()
System.out.println(alice1);
// Alice (ID: S001, GPA: 3.80, Credits: 45)
// equals()
System.out.println(alice1.equals(alice2)); // true
System.out.println(alice1 == alice2); // false
// hashCode()
System.out.println(alice1.hashCode() == alice2.hashCode()); // true
// Collections work correctly
List<Student> roster = new ArrayList<>();
roster.add(alice1);
System.out.println(roster.contains(alice2)); // true (found by content!)
}
}
Choosing Which Fields to Include
Not every field necessarily belongs in equals() and hashCode(). The question is: what defines the identity of this object?
For a Student, you might decide that the studentId alone defines identity — two students with the same ID are the same student, regardless of GPA or credits. In that case:
@Override
public boolean equals(final Object obj) {
if (this == obj) { return true; }
if (obj == null) { return false; }
if (getClass() != obj.getClass()) { return false; }
Student other = (Student) obj;
return Objects.equals(this.studentId, other.studentId);
}
@Override
public int hashCode() {
return Objects.hash(studentId);
}
This is a design decision. For this course, include all fields unless you have a specific reason not to. The important rule is: the same fields must appear in both methods.
Common Pitfall: Including a field in
equals()but forgetting it inhashCode()(or vice versa) violates the contract. Two objects could be equal but have different hash codes. Always generate or update both methods together.
In the equals() method, how should you compare two double fields?
Quick Reference
| Task | Code |
|---|---|
| Override toString | @Override public String toString() { return String.format(...); } |
| Reference equality | a == b (same object in memory?) |
| Value equality | a.equals(b) (same content?) |
| Compare Strings in equals | Objects.equals(this.name, other.name) |
| Compare int in equals | this.credits == other.credits |
| Compare double in equals | Double.compare(this.gpa, other.gpa) == 0 |
| Hash code from fields | Objects.hash(field1, field2, field3) |
| Required import | import java.util.Objects; |
Summary
Every Java class inherits three methods from Object that do the wrong thing by default. toString() prints a useless hex hash. equals() compares references instead of content. hashCode() derives from the memory address.
Override all three. toString() uses String.format() to produce readable output. equals() follows the five-step recipe: check reference, check null, check class, cast, compare fields. hashCode() uses Objects.hash() with the same fields as equals().
The equals/hashCode contract is non-negotiable: equal objects must have equal hash codes. Violating it causes silent data loss in collections. The easiest way to stay safe is to generate both methods together using your IDE and include exactly the same fields in both.
With toString(), equals(), and hashCode() in place, your objects print clearly, compare correctly, and work reliably in ArrayList, HashMap, and every other collection in the Java standard library.
Next lesson: How do you know your classes work correctly? We will write automated tests with JUnit to verify constructors, setters, equals, and edge cases.