Lecture: Abstract classes
Design methods for unions of classes of data.
Practice using wish lists.
Wish lists.
IShape.java
AShapeData.java
AShape.java
AShapeCombo.java
Lecture outline
Design recipe for abstractions
- Designing abstract class
Lifting fields
Lifting methods: abstract methods
Lifting methods: concrete methods in an abstract class
Overriding concrete methods
Abstraction by defining a subclass
Common interface - yes or no
1 Design recipe for abstractions
The complete code for this section is in the file IShape.java
Here are our two classes that represent shapes that we have defined to implement the common interface IShape.
The class diagram below includes the five methods we have designed for these classes.
+---------------------------------+ |
| IShape | |
+---------------------------------+ |
+---------------------------------+ |
| double area() | |
| boolean distTo0() | |
| IShape grow(int inc) | |
| boolean biggerThan(IShape that) | |
| boolean contains(CartPt pt) | |
+---------------------------------+ |
| |
/ \ |
--- |
| |
------------------------------- |
| | |
+----------------------------+ +----------------------------+ |
| Circle | | Square | |
+----------------------------+ +----------------------------+ |
+-| CartPt center | +-| CartPt nw | |
| | int radius | | | int size | |
| | String color | | | String color | |
| +----------------------------+ | +----------------------------+ |
| | double area() | | | double area() | |
| | boolean distTo0() | | | boolean distTo0() | |
| | IShape grow(int) | | | IShape grow(int) | |
| | boolean biggerThan(IShape) | | | boolean biggerThan(IShape) | |
| | boolean contains(CartPt) | | | boolean contains(CartPt) | |
| +----------------------------+ | +----------------------------+ |
| | |
+----+ +--------------------------+ |
| | |
v v |
+-----------------------+ |
| CartPt | |
+-----------------------+ |
| int x | |
| int y | |
+-----------------------+ |
| double distTo0() | |
| double distTo(CartPt) | |
+-----------------------+ |
We decide to add another class to this hierarchy - to represent rectangles:
+----------------------------+ |
| Rect | |
+----------------------------+ |
+-| CartPt nw | |
| | int width | |
| | int height | |
| | String color | |
| +----------------------------+ |
| | double area() | |
| | boolean distTo0() | |
| | IShape grow(int) | |
| | boolean biggerThan(IShape) | |
| | boolean contains(CartPt) | |
| +----------------------------+ |
v |
+-----------------------+ |
| CartPt | |
+-----------------------+ |
| int x | |
| int y | |
+-----------------------+ |
| double distTo0() | |
| double distTo(CartPt) | |
+-----------------------+ |
Let us recall the definition of the class Square:
// to represent a square |
class Square implements IShape { |
CartPt nw; |
int size; |
String color; |
|
Square(CartPt nw, int size, String color) { |
this.nw = nw; |
this.size = size; |
this.color = color; |
} |
|
/* TEMPLATE |
FIELDS |
... this.nw ... -- CartPt |
... this.size ... -- int |
... this.color ... -- String |
METHODS |
... this.area() ... -- double |
... this.distTo0() ... -- double |
... this.grow(int inc) ... -- IShape |
... this.biggerThan(IShape that) ... -- boolean |
... this.contains(CartPt pt) ... -- boolean |
METHODS FOR FIELDS: |
... this.nw.distTo0() ... -- double |
... this.nw.distTo(CartPt) ... -- double |
*/ |
|
// to compute the area of this shape |
public double area(){ |
return this.size * this.size; |
} |
|
// to compute the distance form this shape to the origin |
public double distTo0(){ |
return this.nw.distTo0(); |
} |
|
// to increase the size of this shape by the given increment |
public IShape grow(int inc){ |
return new Square(this.nw, this.size + inc, this.color); |
} |
|
// is the area of this shape is bigger than the area of the given shape? |
public boolean biggerThan(IShape that){ |
return this.area() >= that.area(); |
} |
|
// does this shape (including the boundary) contain the given point? |
public boolean contains(CartPt pt){ |
return (this.nw.x <= pt.x) && (pt.x <= this.nw.x + this.size) && |
(this.nw.y <= pt.y) && (pt.y <= this.nw.y + this.size); |
} |
} |
We will not go through all the deatails of the design recipe, just show the final definition of the new class Rect:
// to represent a rectangle |
class Rect implements IShape { |
CartPt nw; |
int width; |
int height; |
String color; |
|
Rect(CartPt nw, int width, int height, String color) { |
this.nw = nw; |
this.width = width; |
this.height = height; |
this.color = color; |
} |
|
/* TEMPLATE |
FIELDS |
... this.nw ... -- CartPt |
... this.width ... -- int |
... this.height ... -- int |
... this.color ... -- String |
METHODS |
... this.area() ... -- double |
... this.distTo0() ... -- double |
... this.grow(int inc) ... -- IShape |
... this.biggerThan(IShape that) ... -- boolean |
... this.contains(CartPt pt) ... -- boolean |
METHODS FOR FIELDS: |
... this.nw.distTo0() ... -- double |
... this.nw.distTo(CartPt) ... -- double |
*/ |
|
// to compute the area of this shape |
public double area(){ |
return this.width * this.height; |
} |
|
// to compute the distance form this shape to the origin |
public double distTo0(){ |
return this.nw.distTo0(); |
} |
|
// to increase the size of this shape by the given increment |
public IShape grow(int inc){ |
return new Rect(this.nw, this.width + inc, this.height + inc, |
this.color); |
} |
|
// is the area of this shape is bigger than the area of the given shape? |
public boolean biggerThan(IShape that){ |
return this.area() >= that.area(); |
} |
|
// does this shape (including the boundary) contain the given point? |
public boolean contains(CartPt pt){ |
return (this.nw.x <= pt.x) && (pt.x <= this.nw.x + this.width) && |
(this.nw.y <= pt.y) && (pt.y <= this.nw.y + this.height); |
} |
} |
We add examples:
|
IShape r1 = new Rect(new CartPt(50, 50), 30, 20, "red"); |
IShape r2 = new Rect(new CartPt(50, 50), 50, 40, "red"); |
IShape r3 = new Rect(new CartPt(20, 40), 10, 20, "green"); |
|
// test the method area in the class Rect |
boolean testRectArea(Tester t) { |
return |
t.checkInexact(this.r1.area(), 600.0, 0.01); |
} |
|
// test the method distTo0 in the class Rect |
boolean testRectDistTo0(Tester t) { |
return |
t.checkInexact(this.r1.distTo0(), 70.71, 0.01) && |
t.checkInexact(this.r3.distTo0(), 44.72, 0.01); |
} |
|
// test the method grow in the class Rect |
boolean testRectGrow(Tester t) { |
return |
t.checkExpect(this.r1.grow(20), this.r2); |
} |
|
// test the method biggerThan in the class Rect |
boolean testRectBiggerThan(Tester t) { |
return |
t.checkExpect(this.r1.biggerThan(this.r2), false) && |
t.checkExpect(this.r2.biggerThan(this.r1), true) && |
t.checkExpect(this.r1.biggerThan(this.c1), true) && |
t.checkExpect(this.r3.biggerThan(this.s1), false); |
} |
|
// test the method contains in the class Rect |
boolean testRectContains(Tester t) { |
return |
t.checkExpect(this.r1.contains(new CartPt(100, 100)), false) && |
t.checkExpect(this.r2.contains(new CartPt(55, 60)), true); |
} |
We see that a lot of code is repeated. We also realize that for every shape we define its location (a CartPt value) and its color given as a String. We recall the Design Recipe for Abstractions and see if we can apply it here.
Design Recipe for Abstractions
Compare two or more pieces of code that look very similar.
Highlight the places where they differ.
Replace the places where the code differs with parameters and rewrite the original code using there parameters.
Rewrite the original tests in terms of the new abstraction by providing the appropriate values for the arguments.
Now run the original tests and make sure all tests pass.
2 Lifting fields
The complete code for this section is in the file AShapeData.java
We will start by working on the data definitions only, ignoring the methods defined in these classes.
All three classes contain the field color of the type String, and also the location of the type CartPt, even though the field may not have the same name.
In Java we can define an abstract class that contains these fields, and declare that the three classes that implement three different shapes extend this class.
// to represent a geometric shape |
abstract class AShape implements IShape { |
CartPt loc; |
String color; |
|
AShape(CartPt loc, String color) { |
this.loc = loc; |
this.color = color; |
} |
} |
The class defines a constructor, but we cannot construct any abstract shape - a real shape will have other fields, and will represent additional information.
Each of the three classes will become a subclass of the class AShape and will inherit all fields defined in its super class:
|
// to represent a circle |
class Circle extends AShape { |
int radius; |
|
Circle(CartPt center, int radius, String color) { |
super(center, color); |
this.radius = radius; |
} |
} |
|
// to represent a square |
class Square extends AShape { |
int size; |
|
Square(CartPt nw, int size, String color) { |
super(nw, color); |
this.size = size; |
} |
} |
|
// to represent a rectangle |
class Rect extends AShape { |
int width; |
int height; |
|
Rect(CartPt nw, int width, int height, String color) { |
super(nw, color); |
this.width = width; |
this.height = height; |
} |
} |
Even though we do not see the fields loc and color anywhere in the class definitions, the class definition starts with
class Circle extends AShape
class Square extends AShape
class Rect extends AShape
declaring that this class will be a sub-class of the class AShape and so it contains all fields defined in the super class.
The constructor for each class starts with
super(nw, color) (though we use ctr for the Circle class).
It invokes the constructor we have defined in the class AShape and initializes te values of the fields loc and color.
We have re-named the field that represents the current location of the shape to loc, but that does not change any computations or examples. Indeed, we make sure that the original examples will work as they did before.
CartPt pt1 = new CartPt(0, 0); |
CartPt pt2 = new CartPt(3, 4); |
CartPt pt3 = new CartPt(7, 1); |
|
IShape c1 = new Circle(new CartPt(50, 50), 10, "red"); |
IShape c2 = new Circle(new CartPt(50, 50), 30, "red"); |
IShape c3 = new Circle(new CartPt(30, 100), 30, "blue"); |
|
IShape s1 = new Square(new CartPt(50, 50), 30, "red"); |
IShape s2 = new Square(new CartPt(50, 50), 50, "red"); |
IShape s3 = new Square(new CartPt(20, 40), 10, "green"); |
|
IShape r1 = new Rect(new CartPt(50, 50), 30, 20, "red"); |
IShape r2 = new Rect(new CartPt(50, 50), 50, 40, "red"); |
IShape r3 = new Rect(new CartPt(20, 40), 10, 20, "green"); |
Our new class diagram becomes:
+--------+ |
| IShape | |
+--------+ |
| |
/ \ |
--- |
| |
+--------------+ |
| AShape | |
+--------------+ |
+-----------| CartPt loc | |
| | String color | |
| +--------------+ |
| | |
| / \ |
| --- |
| | |
| -------------------------------- |
| | | | |
| +------------+ +----------+ +------------+ |
| | Circle | | Square | | Rect | |
| +------------+ +----------+ +------------+ |
| | int radius | | int size | | int width | |
| +------------+ +----------+ | int height | |
| +------------+ |
+----+ |
| |
v |
+--------+ |
| CartPt | |
+--------+ |
| int x | |
| int y | |
+--------+ |
3 Lifting methods: abstract methods
The complete code for this section and the following section is in the file AShape.java
We now look at what can be done with the method definitions.
The abstract class does not have enough information about its constituent shapes to define the methods area, grow, distTo0 (though we could do this for two out of the three classes), and contains. But the method biggerThan leverages the fact that we already know how to compute the area and the method body is the same in all three classes.
When we cannot define the methods in the abstract class, but want to make sure all subclasses implement the method, we define its header and declare the method to be abstract. So, to comply with the requirements of our original interface we start the abstract class as follows:
// to represent a geometric shape |
abstract class AShape implements IShape { |
CartPt loc; |
String color; |
|
AShape(CartPt loc, String color) { |
this.loc = loc; |
this.color = color; |
} |
|
// to compute the area of this shape |
public abstract double area(); |
|
// to compute the distance form this shape to the origin |
public double distTo0(){ |
return this.loc.distTo0(); |
} |
|
// to increase the size of this shape by the given increment |
public abstract IShape grow(int inc); |
|
// is the area of this shape is bigger than the area of the given shape? |
public boolean biggerThan(IShape that){ |
return this.area() >= that.area(); |
} |
|
// does this shape (including the boundary) contain the given point? |
public abstract boolean contains(CartPt pt); |
} |
The definitions of the methods that are labeled abstract remains the same in all subclasses as they have been before (except for the change of the name of the field that represents the current location of this shape).
4 Concrete methods in the abstract class
The class definition contains two concrete methods: distTo0 and biggerThan. The second method’s body was the same in all three classes and so now it appears here without change. Every subclass of the class AShape can invoke this method.
We have made a small change in the method body for distTo0 —
If we delete the method definitions for these methods from our original code and run our tests most of them will succeed. The only one that fails is the test for distTo0 in the class Circle, because there the computation has been different. Restoring the original method definition in the class Circle fixes the problem.
We say that the method definition of the method distTo0 in the class Circle overrides the definition in its superclass AShape. At runtime, Java looks for a method with the matching signature (matching header) first in the class where the current instance of the object has been defined. If the method is not found, it continues looking in its superclass. So, an instance of a Circle invoking the distTo0 method finds the definition in the class Circle, but an instance of the Rect class will find the concrete method defined in the abstract class AShape.
Of course, we run the tests again and make sure all of them pass.
5 Abstraction by defining a subclass
The complete code for this section and the following section is in the file AShapeSub.java
Looking at the code for the classes Square and Rect we see that they are also very similar. We know from geometry that every square is just a rectangle in which the width and the height are the same. So, we can further refine our design and define the class Square to be a subclass of the class Rect. Sure, it will now have a width and a height but we can make sure the constructor will always define both of these values to be the same.
Here is our new definition of the class Square:
// to represent a square |
class Square extends Rect { |
|
Square(CartPt nw, int size, String color) { |
super(nw, size, size, color); |
} |
|
/* TEMPLATE |
Fields: |
... this.loc ... -- CartPt |
... this.width ... -- int |
... this.height ... -- int |
... this.color ... -- String |
Methods: |
... this.area() ... -- double |
... this.distTo0() ... -- double |
... this.grow(int) ... -- IShape |
... this.biggerThan(IShape) ... -- boolean |
... this.contains(CartPt) ... -- boolean |
Methods for fields: |
... this.loc.distTo0() ... -- double |
... this.loc.distTo(CartPt) ... -- double |
*/ |
|
// to increase the size of this shape by the given increment |
public IShape grow(int inc){ |
return new Square(this.loc, this.width + inc, this.color); |
} |
} |
The constructor invokes the constructor in its superclass, Rect.
Furthermore, we only have one method definition here - the methods distTo0 and contains as defined in the Rect class do exactly what they are supposed to do for squares - the only method we need to override is the method grow as it needs to produce an instance of a Square, not of Rect.
6 Common interface - yes or no
The complete code for this section is in the file AShapeCombo.java
It seems that defining both the interface IShape and the abstract class AShape just repeats the code and is useless. For the classes we have defined so far, it is indeed true. However, we would like to add to our collection of shapes a new shape that is composed from two existing shapes. The class diagram would be:
+--------+ |
| IShape | |
+--------+ |
/ \ |
--- |
| |
+------------+ |
| Combo | |
+------------+ |
| IShape top | |
| IShape top | |
+------------+ |
Here are some examples of Combo shapes:
IShape cb1 = new Combo(this.r1, this.c1); |
IShape cb2 = new Combo(this.r2, this.r3); |
IShape cb3 = new Combo(this.cb1, this.cb2); |
leveraging the earlier examples of shapes.
But the Combo shape does not have just one color, and its location is based on the location of the two contributing shapes. Still, we can compute the area of this shape (though, to simplify our work, we just compute the total area of the pieces of paper if one makes a collage by pasting the shapes onto a canvas). We can compute the distance to origin, and determine which shape has bigger area. Finally, we also can grow this shape.
So, all methods defined earlier make sense for the Combo shape, yet the Combo shape cannot be a subclass of AShape. But if both the class AShape and the class Combo implement the common interface IShape, Java will be able to invoke these five methods for any kind of shape, whether simple or a complex one.
Here is the definition of the class Combo —
// to represent a shape that combines two existing shapes |
class Combo implements IShape { |
IShape top; |
IShape bot; |
|
Combo(IShape top, IShape bot) { |
this.top = top; |
this.bot = bot; |
} |
|
/* TEMPLATE |
FIELDS |
... this.top ... -- IShape |
... this.bot ... -- IShape |
METHODS |
... this.area() ... -- double |
... this.distTo0() ... -- double |
... this.grow(int) ... -- IShape |
... this.biggerThan(IShape) ... -- boolean |
... this.contains(CartPt) ... -- boolean |
METHODS FOR FIELDS: |
... this.top.area() ... -- double |
... this.top.distTo0() ... -- double |
... this.top.grow(int) ... -- IShape |
... this.top.biggerThan(IShape) ... -- boolean |
... this.top.contains(CartPt) ... -- boolean |
|
... this.bot.area() ... -- double |
... this.bot.distTo0() ... -- double |
... this.bot.grow(int) ... -- IShape |
... this.bot.biggerThan(IShape) ... -- boolean |
... this.bot.contains(CartPt) ... -- boolean |
*/ |
// to compute the area of this shape |
public double area() { |
return this.top.area() + this.bot.area(); |
} |
|
// to compute the distance form this shape to the origin |
public double distTo0(){ |
return Math.min(this.top.distTo0(), this.bot.distTo0()); |
} |
|
// to increase the size of this shape by the given increment |
public IShape grow(int inc) { |
return new Combo(this.top.grow(inc), this.bot.grow(inc)); |
} |
|
// is the area of this shape is bigger than the area of the given shape? |
public boolean biggerThan(IShape that){ |
return this.area() >= that.area(); |
} |
|
// does this shape (including the boundary) contain the given point? |
public boolean contains(CartPt pt) { |
return this.top.contains(pt) || this.bot.contains(pt); |
} |
} |
and here are the examples that involve the Combo shapes:
// test the method area in the shape classes |
boolean testShapeArea(Tester t) { |
return |
t.checkInexact(this.cb1.area(), 914.15926, 0.01) && |
t.checkInexact(this.cb2.area(), 2200.0, 0.01) && |
t.checkInexact(this.cb3.area(), 3114.15926, 0.01); |
} |
|
// test the method distTo0 in the shape classes |
boolean testShapeDistTo0(Tester t) { |
return |
t.checkInexact(this.cb1.distTo0(), 60.71, 0.01) && |
t.checkInexact(this.cb2.distTo0(), 44.72, 0.01) && |
t.checkInexact(this.cb3.distTo0(), 44.72, 0.01); |
} |
|
// test the method grow in the shape classes |
boolean testShapeGrow(Tester t) { |
return |
t.checkExpect(this.cb1.grow(20), new Combo(this.r2, this.c2)); |
} |
|
// test the method biggerThan in the shape classes |
boolean testShapeBiggerThan(Tester t) { |
return |
t.checkExpect(this.c1.biggerThan(this.cb1), false) && |
t.checkExpect(this.s2.biggerThan(this.cb1), true) && |
t.checkExpect(this.r2.biggerThan(this.cb1), true) && |
t.checkExpect(this.r3.biggerThan(this.cb1), false) && |
|
t.checkExpect(this.cb2.biggerThan(this.r1), true) && |
t.checkExpect(this.cb1.biggerThan(this.r2), false) && |
t.checkExpect(this.cb1.biggerThan(this.c1), true) && |
t.checkExpect(this.cb1.biggerThan(this.c3), false) && |
t.checkExpect(this.cb1.biggerThan(this.s2), false) && |
t.checkExpect(this.cb2.biggerThan(this.s1), true) && |
t.checkExpect(this.cb1.biggerThan(this.cb3), false) && |
t.checkExpect(this.cb2.biggerThan(this.cb1), true); |
} |
|
// test the method contains in the shape classes |
boolean testShapeContains(Tester t) { |
return |
t.checkExpect(this.cb1.contains(new CartPt(100, 100)), false) && |
t.checkExpect(this.cb2.contains(new CartPt(55, 60)), true); |
} |
Finally, here is the class diagram for the entire collection of classes and interfaces we have designed:
+----------------------------------------+ |
| +------------------------------------+| |
| | || |
v v || |
+----------------------------+ || |
| IShape | || |
+----------------------------+ || |
| double area() | || |
| boolean distTo0() | || |
| IShape grow(int) | || |
| boolean biggerThan(IShape) | || |
| boolean contains(CartPt) | || |
+----------------------------+ || |
| || |
/ \ || |
--- || |
| || |
--------------------------------------------- || |
| | || |
+-----------------------------------+ +----------------------------+ || |
| abstract AShape | | Combo | || |
+-----------------------------------+ +----------------------------+ || |
+--| CartPt loc | | IShape top |-+| |
| | String color | | IShape bot |--+ |
| +-----------------------------------+ +----------------------------+ |
| | abstract double area() | | double area() | |
| | boolean distTo0() | | boolean distTo0() | |
| | abstract IShape grow(int) | | IShape grow(int) | |
| | boolean biggerThan(IShape) | | boolean biggerThan(IShape) | |
| | abstract boolean contains(CartPt) | | boolean contains(CartPt) | |
| +-----------------------------------+ +----------------------------+ |
| | |
| / \ |
| --- |
| | |
| -------------------------------- |
| | | |
| +--------------------------+ +--------------------------+ |
| | Circle | | Rect | |
| +--------------------------+ +--------------------------+ |
| | int radius | | int width | |
| +--------------------------+ | int height | |
| | double area() | +--------------------------+ |
| | boolean distTo0() | | double area() | |
| | IShape grow(int) | | IShape grow(int) | |
| | boolean contains(CartPt) | | boolean contains(CartPt) | |
| +--------------------------+ +--------------------------+ |
| / \ |
| --- |
| | |
| +-----------------------------+ |
| | Square | |
| +-----------------------------+ |
| +-----------------------------+ |
| | IShape grow(int) | |
| +-----------------------------+ |
| |
+-------+ |
| |
v |
+-----------------------+ |
| CartPt | |
+-----------------------+ |
| int x | |
| int y | |
+-----------------------+ |
| double distTo0() | |
| double distTo(CartPt) | |
+-----------------------+ |