We have seen that we can use inheritance to specify that one class should be a subclass of another. When we do this, the subclass gets all of the instance variables and methods of the base class.
One benefit of this is that it can reduce duplication in code. If two or more classes need to be similar, we can put the things that they share in a base class. Then we can have them extend the base class and add in the things that make them unique.
As another example, let's say we are making a game with different types of enemies. All of the enemies could contain a position and health and contain a method to attack the player. But different enemies could have extra things they do as well. We could model that with inheritance using something like this:
public class Enemy {
protected double x, y;
protected int health;
public Enemy(double x, double y, int health) {
this.x = x;
this.y = y;
this.health = health;
}
public void attack(Player p) {
// let's the basic attack does 3 damage
System.out.println("Enemy does 3 damage.");
p.adjustHealth(-3);
}
}
public class Wizard extends Enemy {
public Wizard(double x, double y) {
super(x, y, 45);
}
@Override
public void attack(Player p) {
System.out.println("Wizard does 5 damage.");
p.adjustHealth(-5);
}
// the wizard can also teleport someplace else
public void teleport() {
x = Math.Random() * 100;
y = Math.Random() * 100;
}
}
public class Dragon extends Enemy {
private boolean airborne;
public Dragon(double x, double y) {
super(x, y, 70);
airborne = false;
}
@Override
public void attack(Player p) {
if (airborne) {
System.out.println("Dragon does 9 damage.");
p.adjustHealth(-9);
} else {
System.out.println("Dragon does 7 damage.");
p.adjustHealth(-7);
}
}
// the dragon can also fly
public void fly() {
airborne = true;
}
}
Here the base class contains the things that are common to all of the types of enemies. Each subclass can then add on to it in different ways. Here the Wizard enemy adds the ability to teleport around and the dragon can fly.
Using inheritance allows us to organize the code without duplication. However, there's another cool thing about inheritance we will cover today. That's polymorphism, or the ability to treat different kinds of objects the same way.
With the three classes above, we can create objects of either generic enemies, wizards, or dragons. The key Java rule that allows for polymorphism is that we can create any of these three types of objects and put them in a reference that refers to the base class of Enemy:
// make a generic enemy
Enemy e1 = new Enemy(5, 5, 15);
// make a wizard
Enemy e2 = new Wizard(10, 10);
// make a dragon
Enemy e3 = new Dragon(12, 7);
This is different from what we've seen before. Normally the type of thing we make with new exactly matches the type of reference we put it in. That's true for the first enemy, but not the second two. The rule in Java is this:
References can refer to objects of the same type or any subtype.
That means we can refer Enemy references to any type of Enemy object.
When we do that, we are limited to calling the methods in the base class. In this
case, we can only call the attack
method on these objects. We can't call
teleport
or fly
on the objects.
However, when we do call attack
, it will call the right method for the
type it actually is. For example:
e1.attack(player);
e2.attack(player);
e3.attack(player);
Will produce the output:
Enemy does 3 damage. Wizard does 5 damage. Dragon does 7 damage.
This is what polymorphism means. We can write code that deals with Enemy
objects and not need to worry about what sort of enemy it actually is. We can override
the methods in the subclasses to do different things and code it the same way.
In this case, let's say we have code that does a battle between the player and one enemy. Without polymorphism, we might have to do something like this:
public void battle(Player p, Enemy e) {
if (e.getType() == Type.Dragon) {
// do dragon battle
} else if (e.getType() == Type.Wizard) {
// do wizard battle
} else if ....
}
But with polymorphism, we can pass in any type of Enemy and the battle method can call methods that do different things:
public void battle(Player p, Enemy e) {
// will call whatever attack method goes with this type of enemy
e.attack(p);
// we can add a method to move the enemy, and allow the subclasses to do it in different ways
e.move();
}
Then later we can call this method with different sorts of enemies:
battle(player, new Wizard());
battle(player, new Dragon());
When pointing an Enemy reference at a Dragon or Wizard object, we lose the ability to call methods that are specific to Dragon (like fly) or Wizard (like teleport). That's the price of treating all the objects the same. However, in this case, we might be able to get around this by calling the method "specialAbility" and giving all of the enemies a special ability. That way we can call this method on any Enemy object and they would do different things depending on the subclass.
But what should be the special ability of the generic Enemy base class? We might decide that the right answer is none. But to call the method polymorphically, the Enemy class needs to have it. To get around this, we can make it an abstract method in the base class. This means it has no code, we just say it is a method. When we give a class at least one abstract method, the whole class needs to be marked abstract too:
public abstract class Enemy {
protected double x, y;
protected int health;
public Enemy(double x, double y, int health) {
this.x = x;
this.y = y;
this.health = health;
}
public void attack(Player p) {
// let's the basic attack does 3 damage
System.out.println("Enemy does 3 damage.");
p.adjustHealth(-3);
}
abstract public void specialAbility();
}
Here, we added a method called specialAbility. This method has no actual method body, so it's marked abstract. This is only useful when doing inheritance. It says that all enemies have a special ability, but the subclasses will fill in how it actually works.
Abstract classes can't be used to make objects, so we can't create basic Enemy objects any more. The only point of the Enemy class is now to serve as the base class for the subclasses we'll make.
Each of those subclasses must override the specialAbility method. If they don't, they too will have to be abstract classes. Now we can the Wizard class such that it overrides the specialAbility to do teleportation:
public class Wizard extends Enemy {
public Wizard(double x, double y) {
super(x, y, 45);
}
@Override
public void attack(Player p) {
System.out.println("Wizard does 5 damage.");
p.adjustHealth(-5);
}
@Override
public void specialAbility() {
// our special ability is teleportation
x = Math.Random() * 100;
y = Math.Random() * 100;
}
}
We can do the same thing with the Dragon class so that its special ability is flight. Then we can ask any Enemy object to do its special ability and it will choose based on what type of Enemy it actually is:
Enemy e1 = new Wizard(20, 30);
Enemy e2 = new Dragon(40, 15);
e1.specialAbility(); // will teleport
e2.specialAbility(); // will fly
Another thing we can do with polymorphism is make lists of enemies where each item in the list can be a different sort of enemy. This allows us to loop through all of the objects and treat them the same. In this case, we can have all of the enemies attack the player, and then use their special ability:
ArrayList<Enemy> enemies = new ArrayList<>();
enemies.add(new Wizard());
enemies.add(new Dragon());
enemies.add(new Skeleton());
// ... we can have whatever subclasses we want...
for (Enemy e : enemies) {
e.attack(p);
e.specialAbility();
}
Even better, if we want to add new types of enemies, that attack differently and have different special abilities, we won't need to rewrite this code. We can just add the new subclass and override methods to make it behave the way we want. The code which calls those methods will automatically take advantage of the new behavior.
As another example of abstract classes and polymorphism, consider this version of
our Pig game. Here we are using an abstract class called PigPlayer
. There are two subclasses: one for
the human player and one for the AI. There are two benefits of this. The first is we don't duplicate code they both
share (keeping track of the score). The second, and bigger benefit is that we can write the code in the main class
so that it doesn't care which player it's dealing with. That's polymorphism. We can call rollOrStay
without needing to worry about whether it's a human or computer that's deciding.
Copyright © 2024 Ian Finlayson | Licensed under a Creative Commons BY-NC-SA 4.0 License.