Earlier this week I wrote a tutorial about inheritance and abstract classes in Processing/Java. I mentioned there were several tools we could use to share functionality between classes; the first tool was the abstract class, the second tool is the interface.
First, let’s do a quick review of abstract classes. An abstract class differs from a concrete class in that it can never be directly instantiated. A concrete class must extend the abstract class, and that concrete class may be instantiated. All concrete classes extending an abstract class inherit functionality from a single abstract class—multiple inheritance, or inheritance from more than one class at the same time, is not allowed. Abstract classes may contain both concrete methods and abstract method signatures. Concrete fields and methods are inherited by subclasses. Abstract method signatures define a contract which subclasses must fulfill. For example, if your abstract class contains the code abstract void move(); then all subclasses must define their own void move() method.
Interfaces are a way of defining this kind of contract, but without specifying any functionality along the way. An object which implements an interface is committing to respond in a predictable way to a set of methods. For example, this is how you might specify an interface for objects that can move, and an object that implements this interface:
interface Moves {
void move(int dx, int dy);
}
class Creature implements Moves {
int x, y;
Creature(int initX, int initY) {
x = initX; y = initY;
}
void move(int dx, int dy) {
x += dx; y += dy;
}
}
Note that when declaring a method signature in an interface (as opposed to an abstract class), it is not necessary to prepend abstract, as Processing/Java will figure out what we’re up to and make the method abstract implicitly.
But why use interfaces at all? Why not just use an abstract class, call it MovingThing, and have Creature extend MovingThing? Because while classes can only extend one superclass, they may implement any number of interfaces. For example:
interface Moves {
void move(int dx, int dy);
}
interface ChangesColor {
void changeColor(int newColor);
}
class Creature implements Moves, ChangesColor {
int x, y, c;
Creature(int initX, int initY) {
x = initX; y = initY;
}
void move(int dx, int dy) {
x += dx; y += dy;
}
void changeColor(int newColor) {
c = newColor;
}
}
This is a way to get something a little like multiple inheritance in Processing/Java. Objects implementing these interfaces may not all do the same thing, as they each have the right to their own implementation, but at least we can be sure that they’ll do something when called forth to serve.
An object of any class which implements a given interface guarantees it will implement the methods described in that interface. Because we can be sure that an object of a class which implements an interface will have a specific set of features, Processing/Java allows us to typecast objects to the directly to the interface type. For example, say we’re writing a space shooter game, and we have an ArrayList called listOfSpaceStuff which keeps track of all of our moving objects on the screen. The list will hold objects of types Asteroid, OurHero, and BadGuy. As long as Asteroid, OurHero, and BadGuy all implement the interface Moves, we can do the following:
for(int i = 0; i < listOfSpaceStuff.size(); i++) {
Moves movingObject = (Moves)listOfSpaceStuff.get(i);
movingObject.move(dx, dy);
}
Note that because each of our classes has its own unique implementation of move(), they can all move around the screen in a different way. Also note that we could have created a superclass implementing moves, SpaceThing, made Asteroid, OurHero, and BadGuy subclasses of SpaceThing, and then cast the objects coming out of the list to SpaceThing.
Before the final example, a warning about interfaces. Because interfaces only contain abstract method signatures, they pass along the burden of definition to implementing classes. While changing an abstract class can painlessly enhance the functionality of all its subclasses, changing an interface will more likely break your program, because all of the classes which implement that interface will need to be modified so they fulfill the requirements of the new contract. It pays to do some thinking up-front about how your interfaces are going to work, and whether or not the functionality they describe is likely to change, before using them extensively. (The flip side of this is, of course, that using abstract classes can cause brittle code of an entirely different sort. Interested readers might look to Why extends is evil at JavaWorld for some discussion.)
Here’s an example which uses class inheritance as well as interface inheritance, and typecasts objects to interfaces. Notice that Wall and Chameleon both implement the interface ChangesColor, but in dramatically different ways.
Source code: interfaces
Wall wally = new Wall(50, 150, 0x88000000);
Creature bob = new Creature(150, 150);
Chameleon sally = new Chameleon(250, 150, #FFFFFF);
ArrayList drawList = new ArrayList();
ArrayList colorList = new ArrayList();
ArrayList moveList = new ArrayList();
void setup()
{
size(400,300);
frameRate(30);
drawList.add(wally);
drawList.add(bob);
drawList.add(sally);
colorList.add(wally);
colorList.add(sally);
moveList.add(bob);
moveList.add(sally);
}
void draw()
{
background(222);
for(int i = 0; i < drawList.size(); i++) {
Drawable drawableObject = (Drawable)drawList.get(i);
drawableObject.draw();
}
for(int i = 0; i < colorList.size(); i++) {
ChangesColor colorableObject = (ChangesColor)colorList.get(i);
colorableObject.changeColor(color(random(255), random(255), random(255)));
}
for(int i = 0; i < moveList.size(); i++) {
Moves moveableObject = (Moves)moveList.get(i);
moveableObject.move(int(random(-2,2)), int(random(-2,2)));
}
}
interface Drawable {
void draw();
}
interface Moves {
void move(int dx, int dy);
}
interface ChangesColor {
void changeColor(int newColor);
}
class Creature implements Drawable, Moves {
int x, y;
Creature(int initX, int initY) {
x = initX; y = initY;
}
void move(int dx, int dy) {
x += dx; y += dy;
}
void draw() {
fill(200);
ellipse(x, y, 25, 40);
}
}
class Wall implements Drawable, ChangesColor {
int x, y, c;
Wall(int initX, int initY, int initColor) {
x = initX; y = initY;
c = initColor;
}
void draw() {
fill(c);
rect(x, y, 20, 100);
}
void changeColor(int newColor) {
c = newColor;
}
}
class Chameleon extends Creature implements ChangesColor {
int c, target;
float progress;
Chameleon(int initX, int initY, int initColor) {
super(initX, initY);
c = initColor;
target = initColor;
progress = 1;
}
void changeColor(int newColor) {
if(progress >= 1) {
c = target;
target = newColor;
progress = 0;
}
}
void draw() {
fill(lerpColor(c, target, progress));
ellipse(x, y, 30, 10);
ellipse(x, y, 10, 30);
if(progress < 1) {
progress += 0.05;
}
}
}