عقد equals/hashCode
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
Lesson 2 ended with a decision: for a value such as Money, two objects with the same data should count as equal. In this lesson you make that happen by overriding equals, and you learn the rule that comes with it: whenever you override equals, you must override hashCode too, and the two must agree.
Back to the banknotes: a bank clerk who sorts notes into piles by value (hashCode) before checking two notes closely (equals) will only find a matching note if equal notes always land in the same pile. That is the whole contract in one picture.
Why this matters: hash-based collections, caches, and database-mapping frameworks all rely on this pair. Break the agreement and sets show duplicates, lookups fail, and nothing crashes to warn you. The next lessons and the lab show the failure with real output.
This lesson builds on Lessons 1 and 2, on instanceof with a pattern variable from the polymorphism module, and on final fields. Collections are not taught yet (a later module covers them); where one appears, treat it as a black box.
Learning Objectives
By the end of this lesson you will be able to:
- List the five rules of the
equalscontract. - State the
hashCoderule that links it toequals. - Write a correct
equalsandhashCodepair for a value class. - Use
Objects.equalsandObjects.hashto handlenullfields and combine fields. - Choose which fields belong in the pair and explain why mutable ones are risky.
- Explain why overriding only one of the two is a bug.
Written Explanation
The equals contract
When you override equals(Object), the Java documentation requires that it is an equivalence relation on non-null objects:
| Rule | Meaning |
|---|---|
| Reflexive | x.equals(x) is true |
| Symmetric | x.equals(y) is true exactly when y.equals(x) is |
| Transitive | if x.equals(y) and y.equals(z), then x.equals(z) |
| Consistent | repeated calls give the same answer if nothing used in the comparison changed |
| Non-null | x.equals(null) is false |
Notice that the parameter type is Object, not your class. If you write equals(Money other), you have overloaded the method instead of overriding it, so @Override will give a compile error. Always write @Override and the Object parameter.
The hashCode contract
hashCode() returns an int. The documented rules:
- Within one run, it returns the same value for the same object, as long as the information used in
equalsdoes not change. - If
x.equals(y)is true, thenx.hashCode() == y.hashCode()must be true. - If two objects are not equal, they are not required to have different hash codes. Different codes help performance (fewer clashes), but sharing one is allowed.
The reverse is not a rule: the same hash does not prove equality. A hash code is the pile number, not the identity card.
Why they travel together
Hash-based collections (such as HashSet and HashMap, which you will meet in a later module) work in two steps. First they use hashCode() to pick a bucket, a small group of entries. Then they use equals only on entries in that bucket. So if two equal objects produce different hash codes, they go to different buckets and the collection never even compares them. The result is silent duplicates and failed lookups. The default hashCode is identity-based, so a class that overrides only equals is exactly in this trap.
A reliable recipe
For a value class:
- Decide which fields define "the same value". (For
Money:centsandcurrency. For aCustomeridentified byid: onlyid.) - In
equals: return true for the same object, return false if the other is not your type (this also handlesnull), then compare the chosen fields. - In
hashCode: combine the same fields.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Money m)) return false;
return cents == m.cents && currency.equals(m.currency);
}
@Override
public int hashCode() {
return Objects.hash(cents, currency);
}o instanceof Money m is false for null, so the non-null rule holds. Compare primitive fields with == and object fields with equals. If a field can be null, use Objects.equals(a, b) instead of a.equals(b).
Objects.hash(...) accepts any number of values and combines them; it copes with null values too. The exact numbers it produces are an implementation detail, so never hard-code them in tests.
Which fields should you use?
- Use exactly the fields that define equality, in both methods.
- Do not use fields that change after the object is stored in a hash-based collection. A
hashCodebased on a mutable field means the object is filed in one bucket, then the field changes, the hash changes, and later lookups search another bucket. The object seems to vanish, although it is still inside. The safest design is to base the pair onfinalfields, or on a stable identifier such as an id. - A class can be equal by id only. Two
Customerobjects with the same id and different names can be "the same customer at different moments", so you may choose to ignore the name.
A note on subclasses
Using instanceof in equals is fine when the class is final or when subclasses add no fields that matter. When subclasses add fields and are compared with their parents, symmetry can break. For now, keep value classes final (or immutable and simple) and you avoid the problem. Lesson 5 shows a way to skip this boilerplate altogether.
Examples
Example 1: Money with a matching pair (simple)
Money a = new Money(100, "SAR");
Money b = new Money(100, "SAR");
Money c = new Money(100, "SAR");
Money d = new Money(250, "SAR");
System.out.println(a.equals(a));
System.out.println(a.equals(b) + " " + b.equals(a));
System.out.println(a.equals(b) && b.equals(c) && a.equals(c));
System.out.println(a.equals(null));
System.out.println(a.equals("100 SAR"));
System.out.println(a.hashCode() == b.hashCode());
System.out.println(a.hashCode() + " " + d.hashCode());Real output:
true
true true
true
false
false
true
85921 90571Lines 1 to 4 are reflexive, symmetric, transitive, and non-null. A Money is never equal to a String. Equal objects share a hash code; the unequal one has a different number here, but it is allowed to share.
Example 2: Customer equal by id only (practical)
class Customer {
private final long id;
private String name;
// constructor and rename(String) omitted
@Override public boolean equals(Object o) {
return this == o || (o instanceof Customer c && id == c.id);
}
@Override public int hashCode() { return Long.hashCode(id); }
}
Customer c1 = new Customer(7, "Ana");
Customer c2 = new Customer(7, "Ana Maria");
System.out.println(c1.equals(c2) + " " + (c1.hashCode() == c2.hashCode()));
c1.rename("A.");
System.out.println(c1.equals(c2));true true
trueThe name is mutable and not part of identity, so renaming does not change equals or hashCode. That is what makes it safe inside a hash-based collection. Using name in hashCode would not be safe.
Example 3: a field that may be null (realistic)
class Tag {
private final String label; // may be null
Tag(String label) { this.label = label; }
@Override public boolean equals(Object o) {
return o instanceof Tag t && Objects.equals(label, t.label);
}
@Override public int hashCode() { return Objects.hashCode(label); }
}
System.out.println(new Tag(null).equals(new Tag(null)) + " " + new Tag(null).hashCode());
System.out.println(new Tag("x").equals(new Tag(null)));true 0
falselabel.equals(...) would throw for a null label. Objects.equals and Objects.hashCode are null-safe (Objects.hashCode(null) is 0). In real data, optional fields such as a middle name or a phone number are often null.
Backend Insight
Nearly every data class in a backend ends up in a collection, a cache key, or a framework that compares and hashes it. Typical places this pair matters:
- Sets and maps of identifiers and values (covered in the collections module).
- Cache keys: a cache stores an answer under a key object. If the key class lacks a correct pair, you get cache misses or, worse, duplicate work.
- Entities loaded from a database: when the same row is loaded twice you often get two objects; the business meaning of "same row" is its id, so
equalsby id is common (you will see it again when mapping entities).
A design habit follows: keep key fields final, decide equality by a stable value, and write a small test: equal objects must have equal hash codes.
Common Mistakes
Mistake 1: Overriding equals without hashCode
The compiler does not complain. The code runs and sets contain duplicates. Lesson 6 shows it live. Always change both together.
Mistake 2: Mutable fields in hashCode
An object stored in a hash-based collection and then modified can no longer be found. Use final fields or an id.
Mistake 3: Using different fields in the two methods
If equals compares id but hashCode mixes in name, two equal objects can have different hash codes, which breaks the contract.
Mistake 4: Wrong signature
public boolean equals(Money m) overloads rather than overrides. @Override turns this into a compile error.
Mistake 5: Calling equals on a null field
name.equals(other.name) throws if name is null. Use Objects.equals.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. State the hashCode rule that links it to equals.
أظهر الحل A1
A1. If a.equals(b) is true, then a.hashCode() must equal b.hashCode().
A2. Name three of the five rules of the equals contract.
أظهر الحل A2
A2. Any three of: reflexive, symmetric, transitive, consistent, and x.equals(null) is false.
A3. True or false: two unequal objects must have different hash codes.
أظهر الحل A3
A3. False. Unequal objects may share a hash code; distinct codes only help performance.
Level B: Application
B1. Write equals and hashCode for Book(String isbn, String title) where only isbn defines equality (assume isbn is never null).
أظهر الحل B1
B1.
@Override public boolean equals(Object o) {
return this == o || (o instanceof Book b && isbn.equals(b.isbn));
}
@Override public int hashCode() { return isbn.hashCode(); }B2. This pair is broken. Explain why in one sentence and fix it.
@Override public boolean equals(Object o) {
return o instanceof User u && id == u.id;
}
@Override public int hashCode() { return name.hashCode(); }أظهر الحل B2
B2. equals uses id but hashCode uses name, so two users with the same id and different names are equal yet have different hash codes. Fix: return Long.hashCode(id); (assuming id is a long), so both methods use the same field.
B3. What is wrong with public boolean equals(Book other)?
أظهر الحل B3
B3. The parameter is Book, not Object, so it overloads instead of overriding. Collections and Object-typed calls will still use the inherited equals. Write @Override public boolean equals(Object o) and the compiler will catch the mistake.
Level C: Challenge
C1. A class Session(String token, long lastSeen) has a hashCode using both fields. lastSeen is updated on every request, and sessions are kept in a hash-based set. Describe the failure and give a fix.
أظهر الحل C1
C1. The set files a session under a bucket from its hash at insertion time. When lastSeen changes, the hash changes, so contains searches another bucket and fails to find it, even though it is stored. Fix: base equals and hashCode on token only, which does not change.
C2. Write equals and hashCode for Address(String street, String city) when either field may be null.
أظهر الحل C2
C2.
@Override public boolean equals(Object o) {
return this == o || (o instanceof Address a
&& Objects.equals(street, a.street) && Objects.equals(city, a.city));
}
@Override public int hashCode() { return Objects.hash(street, city); }Mini Challenge
Take a class Product(String sku, String name, long priceCents). Decide which fields define equality (give a reason), write the pair, and write a short main that checks: reflexive, symmetric, null, a different class, and equal hash codes for two equal products.
Check your work: you used the same fields in both methods, your main prints true, true, false, false, true, and the fields you used are final.
Summary & Next Lesson
Key takeaways
- A correct
equalsis reflexive, symmetric, transitive, consistent, and false fornull. - If two objects are equal, their hash codes must be equal. Unequal objects may share a hash code.
- Hash-based collections use
hashCodeto find a bucket andequalsinside it, so a mismatch gives silent duplicates. - Use the same, stable fields in both methods, and prefer
finalfields or an id. - Use
Objects.equalsandObjects.hashfornull-safe code.
Next lesson
The next lesson, "toString", gives your objects a readable description for logs and debugging, and warns about leaking secrets.
Self-check
- If
a.equals(b)is true, what musta.hashCode()andb.hashCode()satisfy? - Why must
equalstakeObjectas its parameter type? - Why is a mutable field risky in
hashCode? - Which method makes
nullfields safe when comparing? - Can two unequal objects have the same hash code?
أظهر الإجابات
- They must be equal.
- To override
Object.equals; a different parameter type only overloads it. - The hash changes after storage, so the object is looked up in the wrong bucket and appears lost.
Objects.equals(a, b).- Yes, it is allowed, though many clashes slow collections down.
References
- Oracle, Java SE 21 API:
java.lang.Object. It lists theequalsrules (reflexive, symmetric, transitive, consistent, null) and thehashCoderules (consistent, equal objects have equal hash codes, unequal objects need not differ), and advises overridinghashCodewithequals. - Oracle, Java SE 21 API:
java.util.Objects. It coversObjects.equals,Objects.hash(computed asArrays.hashCodeover the values), andObjects.hashCodefornull. Its note thatObjects.hash(x)differs fromObjects.hashCode(x)is why this lesson useshashCodefor the single-field case. - The bucket explanation is a simplified description of how hash-based collections work; that detail was not checked against the
HashMapdocumentation in this lesson and the module on collections will cover it properly. - The numbers
85921and90571come from running the examples on JDK 21 and are not guaranteed by the documentation.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.