Version without JavaScript, read-only: workshops, quizzes and running code are not available here. Full version · Catalogue · Sign in

Lecture notes · chapter 6

The declared type and the actual type

The compiler relies on the declared type, execution on the actual object. This chapter distinguishes the two, a frequent source of errors in the start-of-year audit.

This chapter is a review of COOA: its notion (N1) is checked by the diagnostic of 21/10, and is not in the exam.

Java is both statically typed and dynamically bound. These two properties do not contradict each other, because they come into play at two different moments.

What the compiler checks

CanardEnPlastique a = new CanardEnPlastique("Gérard", 499);
CanardSwag        b = new CanardSwag("Jacqueline", 999, "cape");

a.cancaner();          // coin coin
b.cancaner();          // coin (mais stylé)
b.getAccessoire();     // legal: b is declared CanardSwag

On the other hand:

CanardEnPlastique a = new CanardEnPlastique("Jean", 300);
a.getAccessoire();     // rejected: CanardEnPlastique does not have this method

The call would be rejected even if a contained a CanardSwag at run time. The compiler does not take into account what the variable will contain, only its declared type.

What happens at run time

CanardEnPlastique c = new CanardSwag("Ouarzazate", 799, "chapeau");
c.cancaner();          // coin (mais stylé)

The declared type is CanardEnPlastique, the actual type is CanardSwag. The override of the actual type is the one that runs: this is called polymorphism.

Complete example

Before running the program, try to predict what each of the three lines prints.

Typage.java

class CanardEnPlastique {
    public String crier() { return "coin"; }
}

class CanardSwag extends CanardEnPlastique {
    @Override public String crier() { return "yo"; }
    public String accessoire()      { return "cape"; }
}

class Demo {
    public static void main(String[] args) {

        CanardEnPlastique c = new CanardSwag();

        System.out.println("1. c.crier()       : " + c.crier());
        System.out.println("2. c.getClass()    : " + c.getClass().getSimpleName());

        // 3. Uncomment: the compiler refuses, even though the object KNOWS how to do it.
        // System.out.println("3. c.accessoire()  : " + c.accessoire());

        // The downcast makes the method visible, at your own risk.
        CanardSwag swag = (CanardSwag) c;
        System.out.println("4. après downcast  : " + swag.accessoire());

        // And here is what happens when the object is not what we claim.
        CanardEnPlastique simple = new CanardEnPlastique();
        try {
            CanardSwag rate = (CanardSwag) simple;
            System.out.println(rate.accessoire());
        } catch (ClassCastException e) {
            System.out.println("5. downcast raté   : " + e.getClass().getSimpleName());
        }
    }
}

A workshop goes here: you write and run code in it. It requires JavaScript.

Line 2 shows that the object is indeed a CanardSwag. Line 3, if you uncomment it, shows that the compiler does not take this into account and rejects the call.

Casts

Upwards (upcast). It is implicit and always safe:

CanardSwag swag = new CanardSwag("Jacqueline", 999, "cape");
CanardEnPlastique canard = swag;      // no parentheses needed

Indeed, a CanardSwag is necessarily a CanardEnPlastique.

Downwards (downcast). It must be written explicitly, and the compiler accepts it without checking:

CanardEnPlastique canard = new CanardEnPlastique("Jean", 399);
CanardSwag swag = (CanardSwag) canard;    // compiles
swag.getAccessoire();                     // ClassCastException at run time

The limits of instanceof

if (c instanceof CanardSwag) {
    CanardSwag s = (CanardSwag) c;
    System.out.println("Accessoire : " + s.getAccessoire());
}

This code works, but it is hard to maintain:

  • it is fragile: with each new subclass, every instanceof has to be found;
  • it is closed to extension: each new type requires modifying existing code;
  • it is verbose: three lines where an override needs none at the call site.

In the polymorphic version, a method of the object is called directly instead of testing its type:

class CanardEnPlastique {
    public String description() { return "Canard basique"; }
}

class CanardSwag extends CanardEnPlastique {
    @Override public String description() { return "Swag avec " + accessoire; }
}

void afficher(CanardEnPlastique c) {
    System.out.println(c.description());     // no more type test
}
Exam question

CanardEnPlastique c = new CanardSwag();: which crier() method runs, and why does the compiler reject c.accessoire()?

The one in CanardSwag, because binding is dynamic: the actual type of the object is what counts. On the other hand, c.accessoire() is rejected because the compiler only considers the declared type, CanardEnPlastique, which does not have this method, whatever object the variable contains at run time.