15b: Equality
ICS211, Fall 2012
Dr. Zach
(switch view)
Status check
- A11 extended 2 days (pushed A12 back too)
- eCafe course eval link added to course site main page (EC)
- (May not get to Immutability today...)
Exam 3 Results
- (compared to Exam 2)
- average = 65.6% (if missing == 0; 76% if ignore missing), median = 73.3%
- Score breakdown:
- 5 - Over 100% (capped at 100)
- 1 - 100
- 5 - 90s (200+)
- 4 - 80s (180+)
- 14 - 70s (160+)
- 8 - 60s (135+)
- 5 - 50s (112+)
- 2 - 40s (90+)
- (6 - Missing)
Equality and Immutability
- Using objects in Java Collections relies on a few common methods
- equals (Object)
- hashCode (Object)
- compareTo (Comparable)
- Often need to write these, but quite easy to mess them up → subtle bugs
- (a bit advanced)
PlayingCard (from last time)
public class PlayingCard {
private int value;
private Suit suit;
public PlayingCard(int value, Suit suit) {
if (value < 1 || value > 13) {
throw new IllegalArgumentException("Value " + value +
" out of valid 1 - 13 range.");
}
this.value = value;
this.suit = suit;
}
public int getValue() {
return this.value;
}
public Suit getSuit() {
return this.suit;
}
@Override
public String toString() {
String v = "" + this.value;
switch (this.value) {
case 1: v = "A"; break;
case 11: v = "J"; break;
case 12: v = "Q"; break;
case 13: v = "K"; break;
}
return v + this.suit;
}
// nested class for Suit
public static enum Suit {
C, D, H, S;
}
}
At this point
PlayingCard fourC = new PlayingCard(4, PlayingCard.Suit.C);
PlayingCard fourC2 = new PlayingCard(4, PlayingCard.Suit.C);
PlayingCard jackH = new PlayingCard(11, PlayingCard.Suit.H);
// overridden toString() (vs orginal)
System.out.println(fourC); // 4C (was: PlayingCard@4d29dcc0)
System.out.println(fourC2); // 4C (was: PlayingCard@4c53ccba)
System.out.println(jackH); // JH (was: PlayingCard@11a5ee7c)
// equals
System.out.println(fourC.equals(fourC)); // true
System.out.println(fourC.equals(jackH)); // false
System.out.println(fourC.equals(fourC2)); // false
// as Set
List<PlayingCard> hand = Arrays.asList(fourC, fourC2, jackH);
Set<PlayingCard> set = new HashSet<PlayingCard>(hand);
System.out.println(set); // [4C, JH, 4C]
- Equals method: by default uses ==
- == tests object identity (compares memory addresses)
Equals method
- public boolean equals(Object o)
- Implement whenever you want instances with same internal values to be equal
- Note the Object parameter
- 5 rules given in docs: reflexive, symmetric, transitive, consistent, never equal to null
- Artificial implementation examples:
- return true;
- return false;
- return o instanceof ThisClass;
- instanceof reminder
PlayingCard equals
@Override
public boolean equals(Object obj) {
if (obj instanceof PlayingCard) {
PlayingCard pc = (PlayingCard) obj;
return this.getValue() == pc.getValue() && this.getSuit() == pc.getSuit();
}
return false;
}
- Optional optimitization for intenstive equals checks:
- First check:
if (this == obj) return true;
Effect of new equals
PlayingCard fourC = new PlayingCard(4, PlayingCard.Suit.C);
PlayingCard fourC2 = new PlayingCard(4, PlayingCard.Suit.C);
PlayingCard jackH = new PlayingCard(11, PlayingCard.Suit.H);
// equals
System.out.println(fourC.equals(fourC)); // true
System.out.println(fourC.equals(jackH)); // false
System.out.println(fourC.equals(fourC2)); // true
// as set
List<PlayingCard> hand = Arrays.asList(fourC, fourC2, jackH);
Set<PlayingCard> set = new HashSet<PlayingCard>(hand);
System.out.println(set); // [4C, JH, 4C] still the same?
System.out.println(set.contains(new PlayingCard(4, PlayingCard.Suit.C)); //false !?!
More complex equals example
- (Example from: Joshua Bloch's book, Effective Java)
- class Point (x, y) and subclass Pixel (x, y, color)
- want Point and Pixels to be equal if at the same location
//In Point...
@Override
public boolean equals(Object obj) {
if (obj instanceof Point) {
Point p = (Point) obj;
return this.x == p.x && this.y == p.y;
}
return false;
}
//In Pixel...
@Override
public boolean equals(Object obj) {
if (obj instanceof Pixel) {
Pixel p = (Pixel) obj;
return this.x == p.x && this.y == p.y && this.color == p.color;
}
return false;
}
- point.equals(point) is fine, and pixel.equals(pixel) is fine
- Consider symmetry: point.equals(pixel) == pixel.equals(point)?
Second attempt
//In Point...
@Override
public boolean equals(Object obj) {
if (obj instanceof Point) {
Point p = (Point) obj;
return this.x == p.x && this.y == p.y;
}
return false;
}
//In Pixel...
@Override
public boolean equals(Object obj) {
if (obj instanceof Point) {
if (obj instanceof Pixel) {
Pixel p = (Pixel) obj;
return this.x == p.x && this.y == p.y && this.color == p.color;
}else {
//just a Point, so ignore color
return super.equals(obj);
}
}
return false;
}
- point.equals(point) is fine, and pixel.equals(pixel) is fine
- Consider symmetry: point.equals(pixel) == pixel.equals(point)?
- Consider transitivity: (pixel.equals(point) && point.equals(pixel2)) == pixel.equals(pixel2)?
- Conclusion: Can't (easily) share equals behavior across levels of class hierarchy
- See this article for very good discussion of other approaches that get closer
Returning to PlayingCard
- Both 4Cs are equal... but not when in a Set!
- Why not?
- Related: how does HashSet hash every possible object into an integer?
- Current behavior:
// hashCode
System.out.printf("%x\n", fourC.hashCode()); // 4d29dcc0
System.out.printf("%x\n", fourC2.hashCode()); // 4c53ccba
System.out.printf("%x\n", jackH.hashCode()); // 11a5ee7c
hashCode
- hashCode method
- Requirements:
- same hash value as long as object does not change (in an equals-changing way)
- equal objects have same hash value
- When you change equals, you still have the default hashCode(): memory address
- Recommended: Unequal objects have unequal hashCodes (as much as possible)
PlayingCard's hashCode
@Override
public int hashCode() {
int total = 17;
total = total * 31 + this.value;
total = total * 31 + this.suit.hashCode(); //or .ordinal() for enums
return total;
}
- Many good IDEs can generate this method (and .equals) for you.
- You may or may not agree with their implementation though!
Effect of new hashCode()
// hashCode
System.out.printf("%x\n", fourC.hashCode()); // 3312f22a (404d with .ordinal())
System.out.printf("%x\n", fourC2.hashCode()); // 3312f22a (404d with .ordinal())
System.out.printf("%x\n", jackH.hashCode()); // 24cc5917 (4128 with .ordinal())
// as set
List<PlayingCard> hand = Arrays.asList(fourC, fourC2, jackH);
Set<PlayingCard> set = new HashSet<PlayingCard>(hand);
System.out.println(set); // [4C, JH]
System.out.println(set.contains(new PlayingCard(4, PlayingCard.Suit.C)); // true
- Hooray: fourC and fourC2 and any other 4C have same hash value
Comparable
- Comparable also has clear rules like equals
- x.compareTo(y) has opposite sign as y.compareTo(x)
- transitive (x < y < z, then x < z)
- if x == y, then both must have the same sign when compared to z.
- Strongly recommended: "A natural ordering that is consistent with equals"
- Means: (x.compare(y) == 0) == (x.equals(y))
- If not, should clearly indicate that in docs
Making PlayingCard Comparable
public class PlayingCard implements Comparable<PlayingCard> {
//...
/**
* Natural ordering is by value only (smaller to larger).
* This is inconsistent with equals.
*/
@Override
public int compareTo(PlayingCard pc) {
return this.value - pc.value;
}
- BTW: This subtraction trick is great only if you know your ints are small (else overflow)
Potential bugs
// as a Set
PlayingCard fourD = new PlayingCard(4, PlayingCard.Suit.D);
List<PlayingCard> hand = Arrays.asList(fourC, fourD, jackH);
Set<PlayingCard> set = new HashSet<PlayingCard>(hand);
System.out.println(set); // [4C, JH, 4D]
System.out.println(set.contains(new PlayingCard(4, PlayingCard.Suit.S))); // false
- TreeSet maintains a BST using compareTo (not equals). (Warns about this in API docs.)
// as a Set
PlayingCard fourD = new PlayingCard(4, PlayingCard.Suit.D);
List<PlayingCard> hand = Arrays.asList(fourC, fourD, jackH);
Set<PlayingCard> set = new TreeSet<PlayingCard>(hand);
System.out.println(set); // [4C, JH] (but no 4D)
System.out.println(set.contains(new PlayingCard(4, PlayingCard.Suit.S))); // true
- Supposed to be able to change Collections implementation without any change in behavior
- Wouldn't be a problem if our PlayingCard's "natural ordering was consistent with equals"
- How to fix? Either make equals depend only on value or (better) consider suit in compareTo
For Next Time
- You: A11
- Me: A11 grader, A12, Quiz 13?, update Laulima grades
- Next time: Immutability (originally for today), Graphs and other advanced ADTs