11 Lecture 11: Defining sameness for complex data, part 1
Casting, type-testing, and “customized" type-testing mechanisms for checking sameness
Motivation
For now! As we’ll see in a few weeks, when we add new language features, we’ll have to revisit this and modify our approach.
11.1 What do we mean by “sameness”?
Reflexivity: every object should be the same as itself.
Symmetry: if object x is the same as object y, then y is the same as x.
Transitivity: if two objects are both the same a third object, then they are the same as each other.
Totality: we can compare any two objects of the same type, and obtain a correct answer.
11.2 Review: sameness for built-in types
t.checkExpect(4 == 4, true) t.checkExpect(true == false, false)
t.checkExpect(4.3333 == 4.3333, true) // BAD idea t.checkExpect(4.3333 - 4.3333 < 0.001, true) // MUCH BETTER
t.checkExpect("hello".equals("hel" + "lo"), true) t.checkExpect("hello".equals("goodbye"), false)
Do Now!
Convince yourself that == for integers and booleans and equals on Strings obey the four properties above.
11.3 Review: sameness of structured data
class Book { String title; String author; Book(String title, String author) { this.title = title; this.author = author; } }
Do Now!
How might we define sameness for books?
// In Book boolean sameBook(Book that) { /* Fields: * this.title * this.author * * Methods of fields: * this.title.equals(String) -> Boolean * this.author.equals(String) -> Boolean * * Fields of parameters: * that.title * that.author */ return this.title.equals(that.title) && this.author.equals(that.author); }
Do Now!
Define the method samePoint for the CartPt class.
Do Now!
Revise the definition of Book so that its author field is now of type Author, where Authors have first and last names, and two Authors are the same when both names are the same. Revise the sameBook method. What methods must it delegate to?
11.4 Sameness of union data: Warmup
interface IShape { } class Circle implements IShape { int x, y; int radius; Circle(int x, int y, int radius) { this.x = x; this.y = y; this.radius = radius; } } class Rect implements IShape { int x, y; int w, h; Circle(int x, int y, int w, int h) { this.x = x; this.y = y; this.w = w; this.h = h; } }
// In Circle public boolean sameCircle(Circle that) { /* Template: * Fields: * this.x, this.y, this.radius * * Fields of parameters: * that.x, that.y, that.radius */ return this.x == that.x && this.y == that.y && this.radius == that.radius; } // In Rect public boolean sameRect(Rect that) { /* Template: * Fields: * this.x, this.y, this.w, this.h * * Fields of parameters: * that.x, that.y, that.w, that.h */ return this.x == that.x && this.y == that.y && this.w == that.w && this.h == that.h; }
// In test method in an Examples class Circle c1 = new Circle(3, 4, 5); Circle c2 = new Circle(4, 5, 6); Circle c3 = new Circle(3, 4, 5); Rect r1 = new Rect(3, 4, 5, 5); Rect r2 = new Rect(4, 5, 6, 7); Rect r3 = new Rect(3, 4, 5, 5); t.checkExpect(c1.sameCircle(c2), false) t.checkExpect(c2.sameCircle(c1), false) t.checkExpect(c1.sameCircle(c3), true) t.checkExpect(c3.sameCircle(c1), true) t.checkExpect(r1.sameRect(r2), false) t.checkExpect(r2.sameRect(r1), false) t.checkExpect(r1.sameRect(r3), true) t.checkExpect(r3.sameRect(r1), true)
11.5 Sameness of union data: flawed attempt #1 using “casting” and type-testing
boolean sameShape(IShape that)
// In Circle: public boolean sameShape(IShape that) { /* * Fields: * this.x, this.y, this.radius * * Methods: * this.sameShape(IShape) -> boolean * this.sameCircle(Circle) -> boolean * * Methods on fields: * * Fields on parameters: * * Methods on parameters * that.sameShape(IShape) -> boolean */ ??? }
11.5.1 Casting
Do Now!
What could go wrong with taking this position?
// In Circle public boolean sameShape(IShape that) { return this.sameCircle((Circle)that); // cast that to a Circle, and use the sameCircle helper }
// In the Examples class t.checkExpect(c1.sameShape(c3), true) // works t.checkExpect(c1.sameShape(c2), false) // works t.checkExpect(r1.sameShape(r3), true) // works t.checkExpect(r1.sameShape(r2), false) // works t.checkExpect(c1.sameShape(r1), false) // CRASH! with a ClassCastException
Quite literally, we’re trying to fit a square peg into a round hole...
Do Now!
Which of our properties for sameness have we violated here?
11.5.2 Type-testing using instanceof
We’ve made some progress, but not enough. We can convince Java to compile our code without any type errors,
but we can’t safely use casts, since they may throw exceptions at runtime. When would casting a value to Circle fail? Precisely
when the value is not a Circle (by definition) —
In Racket, we had type-testing predicates (circle? and rect?) that would let us check what kind of a structure we had. Until now, we have said that Java has no such mechanism. In fact, Java actually does have a type-testing mechanism, but it does not work as well as we might hope. Let’s see how to use it, and how it breaks.
t.checkExpect(new Circle(3, 4, 5) instanceof Circle, true) // because Circle is a Circle. t.checkExpect(new Circle(3, 4, 5) instanceof IShape, true) // because Circle implements IShape t.checkExpect(new Circle(3, 4, 5) instanceof Book, false) // because a Circle is not a Book t.checkExpect(new Rect(3, 4, 5, 6) instanceof Circle, false) // because a Rect is not a Circle
// In Circle public boolean sameShape(IShape that) { if (that instanceof Circle) { // that is-a Circle -- we can safely cast! return this.sameCircle((Circle)that); } else { // that is not a Circle return false; } }
This is a good example of the difference between static and dynamic information available in our code, and goes to show that static types sometimes cause problems (that must then be worked around), even as they help solve others.
// In the Examples class t.checkExpect(c1.sameShape(r1), false) // works
Do Now!
Implement sameShape for the Rect class, following the same pattern as for Circle. What test should you write to confirm that it works?
// In Rect public boolean sameShape(IShape that) { if (that instanceof Rect) { // that is-a Rect -- we can safely cast! return this.sameRect((Rect)that); } else { // that is not a Rect return false; } }
// In the Examples class t.checkExpect(r1.sameShape(c1), false); // works
11.5.3 What goes wrong with casting and instanceof?
Do Now!
Implement sameShape for Square, along with a sameSquare helper method. Write tests to confirm that it works properly.
class Square extends Rect { Square(int x, int y, int s) { super(x, y, s, s; } public boolean sameShape(IShape that) { if (that instanceof Square) { return this.sameSquare((Square)that); } else { return false; } } public boolean sameSquare(Square that) { return this.x == that.x && this.y == that.y && this.w == that.w; // No need to check the h field, too... } }
// In test method in an Examples class Square s1 = new Square(3, 4, 5); Square s2 = new Square(4, 5, 6); Square s3 = new Square(3, 4, 5); // basic checks comparing two Squares should work t.checkExpect(s1.sameShape(s2), false) t.checkExpect(s2.sameShape(s1), false) t.checkExpect(s1.sameShape(s3), true) t.checkExpect(s3.sameShape(s1), true) // Comparing a Square with a Rect of a different size t.checkExpect(s1.sameShape(r2), false) // Good t.checkExpect(r2.sameShape(s1), false) // Good // Comparing a Square with a Rect of the same size t.checkExpect(s1.sameShape(r1), false) // Good t.checkExpect(r1.sameShape(s1), true) // Not so good
Do Now!
Which property of sameness have we violated?
11.6 Sameness of union data: flawed attempt #2 using “custom” type-testing
s1 is a Square, so we invoke the sameShape method defined in Square.
This method checks whether that (which is r1) is an instance of Square, which it is not, so the method returns false.
r1 is a Rect, so we invoke the sameShape method defined in Rect.
This method checks whether that (which is s1) is an instance of Square. Since Square is a subclass of Rect, this instanceof test returns true. So the method then casts that to a Rect (which is perfectly fine), and invokes sameRect.
The sameRect method compares all the fields of this (which is r1) to all the fields of that (which is s1), and since they all match, it returns true.
interface IShape { boolean sameShape(IShape that); // is this shape a Circle? boolean isCircle(); // is this shape a Rect? boolean isRect(); // is this shape a Square? boolean isSquare(); }
// in Circle public boolean isCircle() { return true; } public boolean isRect() { return false; } public boolean isSquare() { return false; }
// in Rect public boolean isCircle() { return false; } public boolean isRect() { return true; } public boolean isSquare() { return false; }
// in Square public boolean isCircle() { return false; } public boolean isRect() { return false; } public boolean isSquare() { return true; }
// in Rect public boolean sameShape(IShape that) { if (that.isRect()) { return this.sameRect((Square)that); } else { return false; } }
// in Square public boolean sameShape(IShape that) { if (that.isSquare()) { return this.sameSquare((Square)that); } else { return false; } }
Do Now!
Try making these changes, and confirm that our bad test above is now better.
// Comparing a Square with a Rect of the same size t.checkExpect(s1.sameShape(r1), false) // Good t.checkExpect(r1.sameShape(s1), false) // Good
Do Now!
But r1 and s1 both describe rectangles with width and height of 5, at position (3,4). Why are they not equal?
Do Now!
Why must we have Square respond to isRect with false —surely all Squares are Rects?
// Distinguishing between a Square and a Rect of the same size t.checkExpect(s1 instanceof Square, true) t.checkExpect(r1 instanceof Square, false)
Wrapup
Exercise
Try to figure this out. Hint: you may want to look at the youngerIAT method from Lecture 7: Accumulator methods, continued, which deals with a similar problem.