polymorphism
One of the most interesting features of OOP is how classes can extend others. The subclasses inherit the properties of their parent by default, and can add their own (override) to further specify their definition. The ability of each class to respond in its own way to the same message is called polymorphism.
The following example illustrates the parent-child inheritance model.
// ----------------------------------------------------------------------
// CLASS DEFINITIONS
// ----------------------------------------------------------------------
class Parent {
// constructor
Parent() {
println("Building a Parent.");
}
void speak() {
println("I am a Parent.");
}
}
// use the keyword "extends" to create a child/subclass
class Child extends Parent {
void speak() {
println("I am a Child.");
}
}
// ----------------------------------------------------------------------
// GLOBAL VARIABLES
// ----------------------------------------------------------------------
// declare a Parent object
Parent p;
// declare a Child object
Child c;
// ----------------------------------------------------------------------
// MAIN
// ----------------------------------------------------------------------
void setup() {
// instantiate the Parent object
p = new Parent();
// instantiate the Child object
c = new Child();
p.speak();
c.speak();
}
The output of that last application would be:
Building a Parent. Building a Parent. I am a Parent. I am a Child.
Note that:
- You use the keyword extends to create a subclass. Child is a subclass of Parent, and inherits all of Parent's methods and properties.
- We do not have to declare a constructor for Child, because there is one already defined in Parent. When the program instantiates a Child object, it first looks to see if there is a constructor defined in it. If not -as is the case here- it looks to the Parent object.
- If we wanted to, we could override the Parent's constructor by defining a constructor in the Child object.
- We can override any function of the Parent. In the example above, Parent's draw() function is overriden by a draw() function in the Child object.
Overriding is quite useful since it allows a child to inherit most of the functionality of one class, but also to change some of it by replacing one or more of the parent's methods. The overridden methods will have the same signature (i.e. the same name, parameter count, parameter types, and return type), but their functionality will be different. This allows the programmer to keep the same interface, the outward definition of the class, while changing its inner workings.
abstract classes
If there ever is a need to prevent a child class from overriding one of its parent's methods, the final modifier must appear in the parent's method declaration.
class Parent {
// constructor
Parent() {
println("Building a Parent.");
}
final void speak() {
println("I am a Parent.");
}
}
On the other hand, we can also do the opposite, and force the child to override one of its parent's methods. This is done with the abstract modifier and means that the method is incomplete and must be overridden. Note that if a class contains an abstract method, it must also be defined as an abstract class. Note that an abstract class cannot be instantiated.
abstract class Parent {
// constructor
Parent() {
println("Building a Parent.");
}
abstract void speak();
}
strength in numbers revisited
Let's modify the strength in numbers applet so that we can create three types of Soldiers: Circles, Squares, and Triangles. All three types will have the same attributes and will behave the same way. The only difference is their appearance, so the only method that will need modification is draw(). Since a general Soldier does not have enough information to know how to draw itself, that method is a good candidate to convert to abstract.
abstract class Soldier {
// ...
// --------------------------------------------------------------------
// METHODS
// --------------------------------------------------------------------
// ...
/* draw() has to be implemented by any class extending Soldier */
abstract void draw();
// ...
}
class Circle extends Soldier {
Circle(float x, float y) {
super(x, y);
}
/* draws the soldier */
void draw() {
// draw the shape first
fill(colour);
ellipse(xPos, yPos, strength+MIN_SIZE, strength+MIN_SIZE);
// draw the number over it
fill(0);
text(strength, xPos, yPos);
}
}
class Square extends Soldier {
Square(float x, float y) {
super(x, y);
}
/* draws the soldier */
void draw() {
// draw the shape first
fill(colour);
rectMode(CENTER);
rect(xPos, yPos, strength+MIN_SIZE, strength+MIN_SIZE);
rectMode(CORNER);
// draw the number over it
fill(0);
text(strength, xPos, yPos);
}
}
class Triangle extends Soldier {
Triangle(float x, float y) {
super(x, y);
}
/* draws the soldier */
void draw() {
// draw the shape first
fill(colour);
float half = (strength+MIN_SIZE)/2.0;
triangle(xPos, yPos-half, xPos-half, yPos+half, xPos+half, yPos+half);
// draw the number over it
fill(0);
text(strength, xPos, yPos);
}
}
Note that:
- Since Soldier contains the abstract method draw(), it must also be defined as an abstract class.
- The abstract method draw() has no body.
- All the subclass constructors contain a call to super(), meaning call the constructor of the parent with the given parameters.
- Each subclass implements its own version of the draw() method.
- Circle's draw() is identical to the old Soldier's draw().
We can now update the main application. Since all objects we are creating are still Soldiers, we don't need to modify the array declaration. However, since we can't instantiate a Soldier (because it is an abstract class), we need to specify which kind of Soldier to create in addSoldier().
// ...
// ----------------------------------------------------------------------
// USER FUNCTIONS
// ----------------------------------------------------------------------
/* adds a new soldier to the display */
void addSoldier(int newX, int newY) {
if (numSoldiers < MAX_SOLDIERS) {
// randomly pick the type of Soldier to create
float coin = random(1);
if (coin < .33) {
army[numSoldiers] = new Circle(newX, newY);
} else if (coin < .66) {
army[numSoldiers] = new Square(newX, newY);
} else {
army[numSoldiers] = new Triangle(newX, newY);
}
numSoldiers++;
}
}
// ...
We can even use this new class hierarchy to make the game more interesting. Let's add a rule stating that a Soldier can only fight with a Soldier of a different type, i.e. a Circle can only fight Squares and Triangles, a Square can only fight Circles and Triangles, and a Triangle can only fight Circles and Squares. We'll implement this by overriding the collidesWith() method for each subclass to check the type of the passed Soldier before actually checking for collisions. If the types are different, we proceed as usual, using the collidesWith() definition from Soldier; if they are the same, we'll just assume they don't collide.
class Circle extends Soldier {
// ...
/* if the other Soldier is not of the same type, makes them collide */
boolean collidesWith(Soldier other) {
if (!(other instanceof Circle)) {
return super.collidesWith(other);
}
return false;
}
}
class Square extends Soldier {
// ...
/* if the other Soldier is not of the same type, makes them collide */
boolean collidesWith(Soldier other) {
if (!(other instanceof Square)) {
return super.collidesWith(other);
}
return false;
}
}
class Triangle extends Soldier {
// ...
/* if the other Soldier is not of the same type, makes them collide */
boolean collidesWith(Soldier other) {
if (!(other instanceof Triangle)) {
return super.collidesWith(other);
}
return false;
}
}