Object Class — Practical Demo
Hands-on examples for Object Class. Covers building a correct
equals/hashCodepair, diagnosing the silentHashMapbug that broken contracts produce, and safe cloning.
Before running these examples, make sure you understand the Object Class note — particularly the equals–hashCode contract and what happens when it is violated.
Example 1: Correct equals and hashCode with java.util.Objects
The cleanest way to override both methods using JDK 8+ helpers.
import java.util.Objects;
public class Product {
private final String sku;
private final String name;
private final double price;
public Product(String sku, String name, double price) {
this.sku = sku;
this.name = name;
this.price = price;
}
@Override
public boolean equals(Object o) {
if (this == o) return true; // same-reference fast path
if (!(o instanceof Product p)) return false; // pattern-matching instanceof (Java 16+)
return Double.compare(p.price, price) == 0 // use compare for doubles, not ==
&& Objects.equals(sku, p.sku)
&& Objects.equals(name, p.name);
}
@Override
public int hashCode() {
return Objects.hash(sku, name, price); // must use the same fields as equals
}
@Override
public String toString() {
return "Product{sku='%s', name='%s', price=%.2f}".formatted(sku, name, price);
}
}
import java.util.*;
public class Example1Runner {
public static void main(String[] args) {
Product a = new Product("SKU-1", "Keyboard", 49.99);
Product b = new Product("SKU-1", "Keyboard", 49.99);
System.out.println(a.equals(b)); // true
System.out.println(a.hashCode() == b.hashCode()); // true
Set<Product> catalog = new HashSet<>();
catalog.add(a);
System.out.println(catalog.contains(b)); // true — contract honoured
System.out.println(catalog.size()); // 1 — no duplicate
}
}
Expected Output:
true
true
true
1
Use the same fields in both equals and hashCode. If sku is in equals but not in hashCode, catalog.contains(b) returns false even though a.equals(b) is true.
Example 2: Diagnosing the Broken hashCode Bug
This is the most common Object contract violation. Watch what happens when equals is overridden but hashCode is not.
import java.util.Objects;
public class BrokenProduct {
private final String sku;
private final String name;
public BrokenProduct(String sku, String name) {
this.sku = sku;
this.name = name;
}
@Override
public boolean equals(Object o) { // equals overridden …
if (!(o instanceof BrokenProduct p)) return false;
return Objects.equals(sku, p.sku) && Objects.equals(name, p.name);
}
// hashCode NOT overridden! // … but hashCode still returns identity hash
}
import java.util.*;
public class BrokenHashCodeDemo {
public static void main(String[] args) {
BrokenProduct a = new BrokenProduct("SKU-1", "Keyboard");
BrokenProduct b = new BrokenProduct("SKU-1", "Keyboard");
System.out.println("equals: " + a.equals(b)); // true
System.out.println("same hash: " + (a.hashCode() == b.hashCode())); // FALSE — different identity hashes
Map<BrokenProduct, Integer> prices = new HashMap<>();
prices.put(a, 50);
// HashMap can't find 'b' because it looks in the wrong bucket:
System.out.println("contains b: " + prices.containsKey(b)); // false ← silent data loss!
System.out.println("get b: " + prices.get(b)); // null
}
}
Expected Output:
equals: true
same hash: false
contains b: false
get b: null
Overriding equals but forgetting hashCode creates a category of bug that is almost impossible to find by reading the code. IntelliJ, SonarQube, and SpotBugs all flag this — enable those checks.
Example 3: Safe Cloning with a Copy Constructor
clone() is error-prone. This example shows the correct alternative: a copy constructor that deep-copies mutable fields.
import java.util.ArrayList;
import java.util.List;
public class ShoppingCart {
private final String owner;
private final List<String> items; // mutable field — must be deep-copied
public ShoppingCart(String owner) {
this.owner = owner;
this.items = new ArrayList<>();
}
public void add(String item) { items.add(item); }
public List<String> getItems() { return List.copyOf(items); } // defensive copy on read
// Copy constructor — explicit, safe, no CloneNotSupportedException
public ShoppingCart(ShoppingCart other) {
this.owner = other.owner;
this.items = new ArrayList<>(other.items); // deep copy of mutable list
}
@Override public String toString() {
return "ShoppingCart{owner='" + owner + "', items=" + items + "}";
}
}
public class CopyConstructorDemo {
public static void main(String[] args) {
ShoppingCart original = new ShoppingCart("Alice");
original.add("Keyboard");
original.add("Mouse");
ShoppingCart copy = new ShoppingCart(original); // copy constructor
copy.add("Monitor"); // ← should NOT affect original
System.out.println("Original: " + original); // no Monitor
System.out.println("Copy: " + copy); // has Monitor
}
}
Expected Output:
Original: ShoppingCart{owner='Alice', items=[Keyboard, Mouse]}
Copy: ShoppingCart{owner='Alice', items=[Keyboard, Mouse, Monitor]}
Copy constructors are safer than clone(): no marker interface needed, no checked exception, no shallow-copy gotcha — and they are far more readable.
Exercises
Try these on your own to solidify understanding:
- Easy: Add a
categoryfield toProductand include it inequals/hashCode. Verify that two products with different categories are not equal even whenskuandnamematch. - Medium: Create a
MutableKeyclass with a mutableidfield. Put it in aHashSet, modifyid, then trycontains. Observe the result and explain why. - Hard: Implement
equalsfor a class hierarchy:Shape→Circle. UseinstanceofinCircle.equals. Then demonstrate a symmetry violation if aShapereference holds aCircleand is compared with a plainShape.