Kestrel Exams kestrel logo KESTREL EXAMSPRACTICE TO PASS
Java11 min readCode verified

Polymorphism: one call, the right method, and is-a vs has-a

Polymorphism is what makes inheritance pay off: one line of code that does the right thing for every kind of object you hand it. This guide covers superclass variables holding subclass objects, how Java chooses which version of a method runs, what the compiler lets you call, casting, and the is-a versus has-a test for designing classes. Every listing was compiled and run on OpenJDK 21.

Practice this topic in the free app →All Java guides

One play button, many devices#

A universal remote has one Play button. Point it at the TV and a show starts. Point it at the speaker and music starts. You pressed the same button; each device decided what “play” means for itself.

Polymorphism (Greek for “many forms”) is that idea in Java. You write one method call, such as p.speak(), and the object it lands on decides which version runs. The overriding methods from the inheritance guide are the devices; polymorphism is the remote.

A parent variable can hold a child object#

Because a cat is a pet, Java lets a Pet variable refer to a Cat object. It never works the other way round: not every pet is a cat.

class Pet {
    public String speak() {
        return "...";
    }
}

class Cat extends Pet {
    @Override
    public String speak() {
        return "Meow";
    }
}

public class Upcast {
    public static void main(String[] args) {
        Pet p = new Cat();
        System.out.println(p.speak());
        Cat c = new Pet();
    }
}
Upcast.java:18: error: incompatible types: Pet cannot be converted to Cat
        Cat c = new Pet();
                ^
1 error

The compiler was happy with Pet p = new Cat(); on line 16. Its only complaint is line 18, the attempt to put a plain pet into a Cat variable. The rule: a variable of a superclass type can refer to an object of any of its subclasses. Two words help keep it straight. The declared type is the type written on the variable, here Pet. The actual type is the class of the object it currently points at, here Cat.

The object picks the method#

When you call an overridden method, Java looks at the actual object, not at the variable’s type, and runs that object’s version. This happens while the program runs, so it is called dynamic binding (or late binding). Here is a small payroll, the kind of example your course builds:

class Employee {
    private String name;

    public Employee(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public double pay() {
        return 0;
    }
}

class Cashier extends Employee {
    private double hours, rate;

    public Cashier(String name, double hours, double rate) {
        super(name);
        this.hours = hours;
        this.rate = rate;
    }

    @Override
    public double pay() {
        return hours * rate;
    }
}

class Manager extends Employee {
    private double salary, bonus;

    public Manager(String name, double salary, double bonus) {
        super(name);
        this.salary = salary;
        this.bonus = bonus;
    }

    @Override
    public double pay() {
        return salary + bonus;
    }
}

public class Payroll {
    public static void main(String[] args) {
        Employee[] staff = {
            new Cashier("Ana", 30, 12.50),
            new Manager("Ben", 1800, 250),
            new Cashier("Cy", 12, 14.00)
        };
        double total = 0;
        for (Employee e : staff) {
            System.out.println(e.getName() + ": " + e.pay());
            total += e.pay();
        }
        System.out.println("Total: " + total);
    }
}
Ana: 375.0
Ben: 2050.0
Cy: 168.0
Total: 2593.0

Every element of the array is declared as Employee, and the loop only ever says e.pay(). Yet the cashier was paid by the hour and the manager got a bonus, because each object ran its own override. The loop does not know or care what kinds of employees exist.

The variable decides what you may call#

Here is the half of the rule that catches people. The compiler only knows the declared type. It lets you call a method only if the declared type has it, whatever the object really is.

class Pet {
}

class Dog extends Pet {
    public String fetch() {
        return "Got it!";
    }
}

public class NoFetch {
    public static void main(String[] args) {
        Pet p = new Dog();
        System.out.println(p.fetch());
    }
}
NoFetch.java:13: error: cannot find symbol
        System.out.println(p.fetch());
                            ^
  symbol:   method fetch()
  location: variable p of type Pet
1 error

The object is a Dog and can fetch perfectly well. But the variable is a Pet, and Pet has no fetch method, so the compiler stops you. Put both halves together and you have the rule to carry into the exam:

QuestionDecided byWhen
May I call this method at all?the declared type of the variablecompile time
Which version runs?the actual type of the objectrun time

To reach a subclass-only method, cast the variable down: ((Dog) p).fetch(). A cast is a promise to the compiler that the object really is a Dog. Break the promise and the program stops while it runs. Checking with instanceof first keeps the promise safe:

class Pet {
}

class Dog extends Pet {
    public String fetch() {
        return "Got it!";
    }
}

class Cat extends Pet {
}

public class Cast {
    public static void main(String[] args) {
        Pet[] pets = { new Dog(), new Cat() };
        for (Pet p : pets) {
            if (p instanceof Dog) {
                System.out.println(((Dog) p).fetch());
            } else {
                System.out.println("not a dog, skipped");
            }
        }
        Dog d = (Dog) pets[1];
    }
}
Got it!
not a dog, skipped
Exception in thread "main" java.lang.ClassCastException: class Cat cannot be cast to class Dog (Cat and Dog are in unnamed module of loader 'app')
	at Cast.main(Cast.java:23)

Inside the loop, instanceof kept every cast safe. The last line casts without checking. It compiled, because the compiler cannot know which object sits in pets[1], and then it failed while running, because that object is a Cat.

Why bother: the benefits#

Without polymorphism, the payroll loop would need to ask every object what it is:

if (e is a Cashier) { pay by the hour }
else if (e is a Manager) { pay salary plus bonus }
else if (e is a Driver) { ... }   // edit this every time a new job is invented

With polymorphism, that whole chain becomes e.pay(). The benefits your course lists come straight from that:

  • Write once, use for every subclass. A method that takes an Employee parameter works for every kind of employee, including ones invented next year.
  • Add a class without touching old code. A new Driver extends Employee with its own pay() drops straight into the existing loop. Nothing else changes, so nothing else breaks.
  • Fewer type checks. The long if/else chain on “what kind is this?” disappears, along with the bugs that come from forgetting a case.
  • One collection for many kinds. An Employee[] or ArrayList<Employee> can hold every kind together.

The course’s other examples (database, file handler, graphical interface) are the same pattern: code written against a general type, with the specific class deciding the details.

is-a versus has-a#

Classes relate to each other in two main ways, and choosing the right one is a design question examiners like.

  • is-a means inheritance. A car is a vehicle, so class Car extends Vehicle.
  • has-a means one object holds another as a field. This is called composition. A car has an engine, so Car gets a field private Engine engine;. A car is not a kind of engine.
class Engine {
    private int horsepower;

    public Engine(int horsepower) {
        this.horsepower = horsepower;
    }

    public String start() {
        return horsepower + " hp engine running";
    }
}

class Vehicle {
    public String describe() {
        return "a vehicle";
    }
}

class Car extends Vehicle {
    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    @Override
    public String describe() {
        return "a car: " + engine.start();
    }
}

public class HasA {
    public static void main(String[] args) {
        Vehicle v = new Car(new Engine(150));
        System.out.println(v.describe());
    }
}
a car: 150 hp engine running

Say the sentence out loud, and pick the one that is true:

PairTrue sentenceCode
Manager, Employeea manager is an employeeclass Manager extends Employee
Library, Booka library has booksa field: ArrayList<Book> books
Square, Shapea square is a shapeclass Square extends Shape
Student, Addressa student has an addressa field: Address home

When both sentences sound possible, prefer has-a. A field can be swapped for a different object later; a parent class is fixed for life.

In UML, is-a is the solid line with a hollow triangle pointing at the parent. Has-a is a line between the two boxes, often with a diamond at the owner’s end: a filled diamond when the part cannot exist without the whole (a room in a house), a hollow one when it can (a player on a team).

One sentence to carry into the exam

The variable’s declared type decides what you are allowed to call; the object’s actual type decides which version runs. Use extends only when “is a” is true, and a field when “has a” is.

Where these go wrong#

  • Writing Cat c = new Pet();. A parent object cannot go in a child variable. Only the other direction works.
  • Calling a subclass-only method through a parent variable. The compiler checks the declared type and reports cannot find symbol.
  • Casting without checking. A wrong cast compiles and then throws ClassCastException. Test with instanceof first.
  • Thinking the variable’s type picks the overridden method. The object picks, at run time.
  • Expecting polymorphism from overloading. Which overload runs is fixed at compile time from the declared types of the arguments; only overriding is chosen at run time.
  • Using extends for has-a. class Car extends Engine compiles, but a car is not an engine. Use a field.

Practice#

Answer each one on paper before you look at the solutions. Every answer that involves running code was checked by compiling and running it.

  1. Which of these compile? Pet a = new Dog();   Dog b = new Pet();   Object c = new Dog(); (Dog extends Pet.)
  2. What does this print?
    class Shape {
        public String name() { return "shape"; }
        public String info() { return "I am a " + name(); }
    }
    class Circle extends Shape {
        @Override
        public String name() { return "circle"; }
    }
    // in main:
    Shape s = new Circle();
    System.out.println(s.name());
    System.out.println(s.info());
    Shape t = new Shape();
    System.out.println(t.info());
  3. Pet p = new Dog(); and Dog has a method fetch() that Pet does not. Does p.fetch() compile? If not, give the safe way to call it.
  4. Add a Driver kind of employee, paid $4.00 per delivery, to the payroll example. What has to change in the loop that totals the pay?
  5. For each pair, choose is-a (extends) or has-a (field): Teacher and Person; Classroom and Desk; Phone and Battery; SavingsAccount and Account.
  6. In one sentence each, what does the compiler decide and what does the running program decide when you write e.pay()?

Solutions#

  1. The first and third. A variable can hold an object of its own class or of any subclass. Object is everyone’s ancestor, so it can hold anything. Dog b = new Pet(); fails with incompatible types: Pet cannot be converted to Dog.
  2. Dynamic binding applies even inside the parent’s own method: info() calls name() on the actual object, which is a Circle.
    class Shape {
        public String name() {
            return "shape";
        }
    
        public String info() {
            return "I am a " + name();
        }
    }
    
    class Circle extends Shape {
        @Override
        public String name() {
            return "circle";
        }
    }
    
    public class QTrace {
        public static void main(String[] args) {
            Shape s = new Circle();
            System.out.println(s.name());
            System.out.println(s.info());
            Shape t = new Shape();
            System.out.println(t.info());
        }
    }
    circle
    I am a circle
    I am a shape
  3. No. The compiler checks the declared type, Pet, which has no fetch, and reports cannot find symbol. Safe version: if (p instanceof Dog) { ((Dog) p).fetch(); }.
  4. Nothing. Write the subclass with its own pay() override and add Driver objects to the array; the existing e.pay() loop handles them. That is the main benefit of polymorphism.
    class Employee {
        public double pay() {
            return 0;
        }
    }
    
    class Driver extends Employee {
        private int deliveries;
    
        public Driver(int deliveries) {
            this.deliveries = deliveries;
        }
    
        @Override
        public double pay() {
            return deliveries * 4.0;
        }
    }
    
    public class QDriver {
        public static void main(String[] args) {
            Employee e = new Driver(35);
            System.out.println(e.pay());
        }
    }
    140.0
  5. Teacher is a Person: extends. A Classroom has desks: field. A Phone has a battery: field. A SavingsAccount is an Account: extends.
  6. The compiler decides whether pay() may be called at all, by checking the declared type of e. The running program decides which pay() runs, by looking at the actual object e refers to.

Test-readiness checklist#

These are the items from the course’s Exam 2 topic list that this guide covers. Tick each one only when you can do it without looking.

  • ☐  I can explain polymorphism and dynamic binding with an example.
  • ☐  I can list the benefits of polymorphism.
  • ☐  I can say which assignments between parent and child variables compile, and when a cast is needed.
  • ☐  I can explain class relationships, and pick between an “is-a” relationship (extends) and a “has-a” relationship (a field).

Test yourself in the free Kestrel Exams app

40 practice questions on polymorphism, casting, is-a and has-a, with every code answer checked by compiling and running it. Free, offline, no account.

Practise polymorphism →

Frequently asked questions#

What is polymorphism in Java?

Polymorphism means one method call can run different code depending on the object it is called on. A superclass variable can refer to any subclass object, and when an overridden method is called, the version belonging to the actual object runs. This lets one loop or method work with every subclass.

What is dynamic binding?

Dynamic binding, also called late binding, means Java chooses which overridden method to run while the program is running, by looking at the actual class of the object rather than the declared type of the variable.

Why can I not call a subclass method through a superclass variable?

Because the compiler only knows the declared type of the variable. If that type does not have the method, the call does not compile, even if the object happens to be a subclass that does. Check with instanceof and cast to the subclass to call it.

What causes a ClassCastException?

Casting a reference to a class that the object is not actually an instance of, for example casting a Cat object to Dog. The cast compiles because the compiler cannot know the object's real type, then fails at run time. Use instanceof before casting.

What is the difference between is-a and has-a relationships?

An is-a relationship is inheritance: a Manager is an Employee, so Manager extends Employee. A has-a relationship is composition: a Car has an Engine, so Car has an Engine field. If saying "is a" sounds wrong, use a field instead of extends.

What are the benefits of polymorphism?

Code written once against a superclass works for every subclass, new subclasses can be added without changing existing code, long if-else chains that test an object's type disappear, and objects of different subclasses can be stored and processed together in one array or list.

Suggest a change

Something here not clear? A topic you wish we covered? Tell us. We read every message, and a request is the fastest way to get a guide written — several of these exist because somebody asked.