instanceof and casts
In a game, an ArrayList<Enemy> holds goblins, trolls and bats, and a loop over it can call anything every Enemy has. But some enemies have more. A troll can regenerate, and a bat flies at some altitude, which the hero's weapon may or may not reach. Through an Enemy variable, those methods can't be called: the variable's type decides what you may call, and not every Enemy is a troll or a bat. This step shows how a program asks an object what it is, and then gets a variable of the more specific type.
Here's the game's hierarchy. Enemy is abstract, since every enemy is some particular kind of enemy (module 34), and Flying is an interface with one method, altitude():
Object
Enemy (abstract)
Goblin
Troll
Bat implements Flying
instanceof
instanceof asks whether an object has a type:
public class Main {
public static void main(String[] args) {
Enemy squeak = new Bat("Squeak", 3);
System.out.println(squeak instanceof Bat);
System.out.println(squeak instanceof Enemy);
System.out.println(squeak instanceof Flying);
System.out.println(squeak instanceof Object);
System.out.println(squeak instanceof Troll);
Enemy nobody = null;
System.out.println(nobody instanceof Enemy);
}
}public abstract class Enemy {
private String name;
private int health;
public Enemy(String name, int health) {
this.name = name;
this.health = health;
}
public String getName() {
return this.name;
}
public int getHealth() {
return this.health;
}
public void heal(int amount) {
this.health += amount;
}
@Override
public String toString() {
return this.name + " (" + this.health + ")";
}
}public class Troll extends Enemy {
public Troll(String name, int health) {
super(name, health);
}
public void regenerate() {
this.heal(5);
}
}public class Bat extends Enemy implements Flying {
private int altitude;
public Bat(String name, int altitude) {
super(name, 8);
this.altitude = altitude;
}
@Override
public int altitude() {
return this.altitude;
}
}public interface Flying {
int altitude();
}true
true
true
true
false
falsevalue instanceof Typeistruewhen the object thatvaluerefers to has that type. That's any of its types from step 1, not only its own class: a Bat is a Bat, an Enemy, a Flying and an Object.- It looks at the object, not at the variable.
squeakis an Enemy variable, andsqueak instanceof Flyingis stilltrue. - It's
falsefornull, which isn't an object at all. Soinstanceofnever crashes. - Module 23's
equalsused it in the same way, to check that the compared object is of the right class.
Casts
When instanceof says yes, a cast gets a variable of that type. Here the flying enemies report their altitude, and the troll regenerates:
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Enemy> enemies = new ArrayList<>();
enemies.add(new Troll("Grak", 30));
enemies.add(new Bat("Squeak", 3));
enemies.add(new Goblin("Snip", 20));
enemies.add(new Bat("Flit", 12));
for (Enemy enemy : enemies) {
if (enemy instanceof Flying) {
Flying flyer = (Flying) enemy;
System.out.println(enemy.getName() + " flies at " + flyer.altitude() + " meters");
} else {
System.out.println(enemy.getName() + " is on the ground");
}
}
Enemy first = enemies.get(0);
if (first instanceof Troll) {
Troll troll = (Troll) first;
troll.regenerate();
}
System.out.println(first);
}
}public abstract class Enemy {
private String name;
private int health;
public Enemy(String name, int health) {
this.name = name;
this.health = health;
}
public String getName() {
return this.name;
}
public int getHealth() {
return this.health;
}
public void heal(int amount) {
this.health += amount;
}
@Override
public String toString() {
return this.name + " (" + this.health + ")";
}
}public class Goblin extends Enemy {
public Goblin(String name, int health) {
super(name, health);
}
}public class Troll extends Enemy {
public Troll(String name, int health) {
super(name, health);
}
public void regenerate() {
this.heal(5);
}
}public class Bat extends Enemy implements Flying {
private int altitude;
public Bat(String name, int altitude) {
super(name, 8);
this.altitude = altitude;
}
@Override
public int altitude() {
return this.altitude;
}
}public interface Flying {
int altitude();
}Grak is on the ground
Squeak flies at 3 meters
Snip is on the ground
Flit flies at 12 meters
Grak (35)(Flying) enemyis a cast, as in module 23'sequals. It doesn't create an object, and it doesn't change one. It gives a reference to the same object, with the type Flying, so javac letsflyer.altitude()through.enemyandflyerrefer to the same bat. Throughenemythe loop calls Enemy'sgetName(). Throughflyerit calls thealtitude()that Flying lists, and the bat's own version runs.- The
ifmakes the cast safe: it runs only for objects that are Flying. - A cast to a class works the same way.
(Troll) firstgives a reference of type Troll to the troll at index 0.trollandfirstrefer to the same object, the one in the list, so afterregenerate(), printingfirstshows 35.
A wrong cast crashes
javac accepts a cast whenever it could work. Whether it does is only known when the program runs:
public class Main {
public static void main(String[] args) {
Enemy snip = new Goblin("Snip", 20);
Troll troll = (Troll) snip;
troll.regenerate();
}
}public abstract class Enemy {
private String name;
private int health;
public Enemy(String name, int health) {
this.name = name;
this.health = health;
}
public void heal(int amount) {
this.health += amount;
}
}public class Goblin extends Enemy {
public Goblin(String name, int health) {
super(name, health);
}
}public class Troll extends Enemy {
public Troll(String name, int health) {
super(name, health);
}
public void regenerate() {
this.heal(5);
}
}Exception in thread "main" java.lang.ClassCastException: class Goblin cannot be cast to class Troll (Goblin and Troll are in unnamed module of loader 'app')
at Main.main(Main.java:4)snipis an Enemy variable, and an Enemy might be a troll, so javac lets(Troll) snipthrough. When the line runs, Java sees that the object is a goblin, and stops the program with a ClassCastException.- A cast never turns an object into another class. A goblin stays a goblin. The cast works only when the object already has the type.
- So check with
instanceofbefore a cast, unless the code can be sure of the type in some other way. - A cast that could never work doesn't compile at all.
(String) snipis an error, since no Enemy can be a String: javac saysincompatible types: Enemy cannot be converted to String.
instanceof, or a method of its own?
instanceof fits when only some objects have what the code needs: the flying enemies' altitude, or a troll's regenerate(). When every kind has its own answer to the same question, it's the wrong tool. Say each enemy is worth some points when it's defeated. This works:
public static int points(Enemy enemy) {
if (enemy instanceof Goblin) {
return 10;
} else if (enemy instanceof Troll) {
return 50;
} else if (enemy instanceof Bat) {
return 5;
}
return 0;
}
But every new kind of enemy needs another else if, here and in every method written this way, and a forgotten one quietly gives 0 points. Instead, give Enemy an abstract method points(), and let each class write its own (module 34). Then the code asks enemy.points(), and javac makes every new kind of enemy say what it's worth. The next step builds a whole program in that way.