Version without JavaScript, read-only: workshops, quizzes and running code are not available here. Full version · Catalogue · Sign in
Lecture notes · chapter 11
The contract of Object
The methods every class inherits from Object and that often need overriding, the contract linking equals and hashCode, and what happens when it is not respected.
This chapter is a review of COOA: its notion (N4) is checked by the diagnostic of 21/10, and is not in the exam.
Every Java class implicitly inherits from java.lang.Object:
class CanardEnPlastique { }
// is exactly
class CanardEnPlastique extends Object { }
It therefore inherits toString(), equals(Object), hashCode(), clone() and getClass(). The default implementations are correct, but they rarely correspond to what one wants.
toString()
By default, toString() returns the name of the class followed by an address in hexadecimal.
CanardEnPlastique c = new CanardEnPlastique("Gérard", 499);
System.out.println(c); // CanardEnPlastique@6d06d69c
This result is of no use in a log, in a debugger or in an error message. The method is therefore overridden:
@Override
public String toString() {
return "CanardEnPlastique[nom=" + nom + ", prix=" + prix + "]";
}
== and equals
CanardEnPlastique a = new CanardEnPlastique("Jacques", 399);
CanardEnPlastique b = new CanardEnPlastique("Jacques", 399);
a == b // false: two distinct objects in memory
a.equals(b) // false too, as long as equals is not overridden
== compares references: it tells whether this is the same object in memory. equals compares what you choose to compare: it tells whether the two ducks are equal in the business sense.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CanardEnPlastique autre = (CanardEnPlastique) o;
return prix == autre.prix && nom.equals(autre.nom);
}
A correct equals is reflexive (a.equals(a)), symmetric (if a.equals(b) then b.equals(a)), transitive, consistent over time, and returns false for null.
The contract with hashCode
If you override equals without overriding hashCode, hash-based collections no longer work correctly, as the following example shows.
Before running the program, try to predict how many ducks each of the three tubs contains.
Bassine.java
import java.util.HashSet;
import java.util.Set;
/** Neither equals nor hashCode: two identical ducks remain two objects. */
class CanardBrut {
final String nom;
CanardBrut(String nom) { this.nom = nom; }
}
/** equals alone: the trap. */
class CanardBancal {
final String nom;
CanardBancal(String nom) { this.nom = nom; }
@Override public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
return nom.equals(((CanardBancal) o).nom);
}
// hashCode() is NOT overridden
}
/** Both, consistent. */
class CanardCorrect {
final String nom;
CanardCorrect(String nom) { this.nom = nom; }
@Override public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
return nom.equals(((CanardCorrect) o).nom);
}
@Override public int hashCode() { return nom.hashCode(); }
}
class Demo {
public static void main(String[] args) {
Set<CanardBrut> a = new HashSet<CanardBrut>();
a.add(new CanardBrut("Jacques"));
a.add(new CanardBrut("Jacques"));
System.out.println("1. sans rien : " + a.size() + " canard(s)");
CanardBancal j1 = new CanardBancal("Jacques"), j2 = new CanardBancal("Jacques");
Set<CanardBancal> b = new HashSet<CanardBancal>();
b.add(j1);
b.add(j2);
System.out.println("2. equals seul : " + b.size() + " canard(s)"
+ " alors que j1.equals(j2) = " + j1.equals(j2));
System.out.println(" contains(j2) : " + b.contains(j2)
+ " (il y est, mais on ne le retrouve pas toujours)");
Set<CanardCorrect> c = new HashSet<CanardCorrect>();
c.add(new CanardCorrect("Jacques"));
c.add(new CanardCorrect("Jacques"));
System.out.println("3. equals+hashCode : " + c.size() + " canard(s)");
}
}
A workshop goes here: you write and run code in it. It requires JavaScript.
The same contract applies to HashMap, HashSet and LinkedHashMap, and to the methods contains, remove and get.
clone() and the shallow copy
CanardCompose original = new CanardCompose("Jean", 799, new Accessoire("chapeau", "rouge"));
CanardCompose copie = (CanardCompose) original.clone();
original.getAccessoire() == copie.getAccessoire(); // true: the SAME accessory
The default clone() copies the fields one by one. For a field that contains a reference, it is the reference that is copied: the two ducks share the same accessory, and a change made by one is visible to the other.
A deep copy recreates the objects that must belong to each copy:
@Override
public CanardCompose clone() {
Accessoire copieAcc = new Accessoire(accessoire.getType(), accessoire.getCouleur());
return new CanardCompose(getNom(), getPrix(), copieAcc);
}
This is the question from chapter 3: composition or aggregation? A wing must be copied, whereas a trainer can be shared. clone() cannot guess this: it is up to you to decide.
Exam question
What happens if you override
equalswithout overridinghashCode?
Hash-based collections no longer work correctly: two equal objects have different hashes and are stored in different buckets, so that a HashSet accepts duplicates that equals declares identical. The result of contains and remove becomes unpredictable.