Lesson 18Phase 2: Objects
Polymorphism
One reference type, many runtime shapes, and why a single loop can drive different behavior per object.
By BasithUpdated 60 min
- polymorphism
- upcasting
- dynamic dispatch
- runtime type
- instanceof
- downcasting
You can now build a class on top of another and replace a method the parent gave you. Here is the idea that makes all of that pay off: you can write one piece of code that talks to the general kind of thing, and at runtime each specific thing does its own version of the work. One word in your code, many behaviors when it runs. That is polymorphism, and it is the quiet engine under most real Java programs.
- Hold different kinds of objects in one array and drive them with a single loop.
- Explain what
Animal a = new Dog();means, and why theDogbehavior still runs. - Add a new kind of object to a program without editing the code that uses it.
- Use
instanceofto ask an object what it really is at runtime. - Downcast with
(Dog) ato reach a subclass member, and say when it is safe.
Dog is-a Animal) and method overriding (a Dog can replace the speak() it inherited, marked with @Override). If those words feel shaky, reread them first. Polymorphism is what those two ideas were building toward.A word that means “many shapes”#
Polymorphismcomes from Greek: “poly” means many, “morph” means form or shape. In Java it means one thing in your code, the general type, can take many forms when the program actually runs. You will see exactly what that buys you in a moment. It is easiest to see with a concrete problem.
Suppose we are modelling a small zoo. We already wrote an Animal class, and from it we built Dog and Cat, each overriding speak() to make its own noise. Now the keeper wants every animal in the zoo to speak, one after another. With what you know so far, you might keep each animal in its own variable and call each one by hand.
public class Main { public static void main(String[] args) { Dog d = new Dog(); Cat c = new Cat(); d.speak(); c.speak(); }} class Animal { void speak() { System.out.println("Some animal sound"); }} class Dog extends Animal { @Override void speak() { System.out.println("Woof"); }} class Cat extends Animal { @Override void speak() { System.out.println("Meow"); }}That works for two animals. But a zoo has hundreds, and they come and go. Writing one named variable and one hand-written call per animal does not scale, the same ache you felt when one customer became many in the first OOP lesson. What we want is to put all the animals in an array and walk it with a loop. But an array holds one type. Can a single array hold a Dog and a Cat at the same time?
One array, many kinds of object#
It can, and this is the heart of the lesson. Because a Dog is-a Animal and a Cat is-a Animal, an Animal[]array can hold either of them. The array's declared element type is Animal, the general type, and any object that is-a Animal is welcome in it. Now watch what one loop does.
public class Main { public static void main(String[] args) { Animal[] zoo = { new Dog(), new Cat(), new Animal() }; for (int i = 0; i < zoo.length; i++) { zoo[i].speak(); } }} class Animal { void speak() { System.out.println("Some animal sound"); }} class Dog extends Animal { @Override void speak() { System.out.println("Woof"); }} class Cat extends Animal { @Override void speak() { System.out.println("Meow"); }}Step through it and watch the one line that matters: zoo[i].speak(). It is the same line every time around the loop. The code never mentions Dog or Cat. Yet the output is Woof, then Meow, then Some animal sound. Each object did its own version of speak(). The loop asked “animal, speak” and each animal answered in its own voice.
Key ideaOne line of code,
zoo[i].speak(), produced three different behaviors. The code is written against the general typeAnimal, and the specific object decides what actually happens. That is polymorphism in one sentence.
speak() on each works as long as each object happens to have that method. Java asks for a shared type up front. The array is an Animal[], so the compiler checks, before the program runs, that every element is an Animal and that every Animal has a speak().Upcasting: a Dog standing in an Animal-shaped slot#
Slow the array down to a single variable and the mechanism is easier to see. Look closely at this line: Animal a = new Dog();. The left side says the variable a has type Animal. The right side builds an actual Dog. So the variable's declared type and the object's real type are different. Storing a Dog in an Animal variable like this is called upcasting: you are looking at a specific thing through the lens of its more general type.
public class Main { public static void main(String[] args) { Animal a = new Dog(); a.speak(); Animal b = new Cat(); b.speak(); }} class Animal { void speak() { System.out.println("Some animal sound"); }} class Dog extends Animal { @Override void speak() { System.out.println("Woof"); }} class Cat extends Animal { @Override void speak() { System.out.println("Meow"); }}Here is the part that surprises people. The variable a is typed Animal, so you might expect a.speak() to run Animal's version and print Some animal sound. It does not. It prints Woof. The object inside a is a real Dog, and the Dog's overridden speak() is the one that runs. The type on the variable did not change what the object is.
=. Here it is Animal.new on the right. Here it is Dog.Read the middle row twice, because it is the whole trick. The declared type controls what is allowed. The real object controls what happens. Upcasting is always safe and automatic: every Dog truly is an Animal, so no information is lost and Java never complains. You are just choosing to forget the extra Dog details for a while.
The name for “the right version runs”
That behavior, where the object's own overridden method is the one that runs even though the variable is a more general type, is called dynamic dispatch (also called late binding or runtime polymorphism). Dispatchjust means “deciding which method to call”. It is dynamic because the decision is made while the program runs, based on the real object, not while it is being compiled. You met this idea in method overriding; here is the why behind it.
That upward search decides what happens when a class does not override a method at all. Below, Cat and Puppy are both empty: neither overrides speak(). Puppy extends Dog, and Cat extends Animal directly.
public class Main { public static void main(String[] args) { Animal[] zoo = { new Dog(), new Cat(), new Puppy() }; for (int i = 0; i < zoo.length; i++) { zoo[i].speak(); } }} class Animal { void speak() { System.out.println("Some animal sound"); }} class Dog extends Animal { @Override void speak() { System.out.println("Woof"); }} class Cat extends Animal {} class Puppy extends Dog {}Neither Cat nor Puppy has a speak() of its own. What do the three calls print?
It prints Woof, Some animal sound, Woof. For the cat, the search starts in Cat, finds no speak(), and moves up to Animal. For the puppy, it starts in Puppy, finds nothing, and moves up one class to Dog, which has a speak(), so the search stops there. A puppy barks because its nearest ancestor with a speak() is Dog.
Animal a = new Cat(); stores a cat in an Animal variable. Cat overrides speak() and also has a scratch() method that Animal lacks. Which statement is true?Adding a new kind without touching the loop#
It is fair to ask what all this buys you beyond a tidy loop. The payoff is that your code stops caring about the specific kinds. Go back to the zoo loop. It says zoo[i].speak() and nothing more. Now suppose the zoo gets a Cow. You write a new Cow class that extends Animal and overrides speak(), drop a new Cow() into the array, and the loop prints Moo in the right place. You did not touch the loop. You did not touch Dog or Cat. The general code kept working because it only ever spoke to the general type.
Key ideaCode written against the general type does not need to change when you add new specific types. You extend the program by adding classes, not by editing the code that uses them.
This is the difference between a program that fights you as it grows and one that welcomes new cases. A loop with a long chain of ifchecks, “if it is a dog do this, else if it is a cat do that”, has to be hunted down and edited every single time a new animal appears, and it is easy to miss one. Polymorphism deletes that chain. Each class already knows how to behave; the loop just asks.
main is finished and must not change: it asks every ticket for its name() and price(). Make ChildTicket answer Child and 6.0, and VipTicket answer VIP and 30.0, by overriding both methods in each class.speak() or not. Later you will meet tools that let you require every animal to define its own speak(), so you can never forget one: that is what abstract classes and interfaces are for. You do not need them yet. Notice only that polymorphism is the reason they will matter.Asking an object what it really is#
Treating everything as an Animal is exactly what we wanted for speak(). But sometimes a specific kind can do something the general type cannot. A Dog might be able to fetch(), an action no plain Animal has. If your variable is typed Animal, the compiler will not let you call a.fetch(), because not every animal can fetch. The declared type only allows what is guaranteed for all animals.
So first you need a way to ask, at runtime, “is this particular object actually a Dog?” The operator instanceof answers exactly that. The expression a instanceof Dog evaluates to a boolean: true if the real object is a Dog or a subclass of Dog, and false otherwise. It is also false when a holds null, so the check itself never crashes.
public class Main { public static void main(String[] args) { Animal a = new Dog(); System.out.println(a instanceof Dog); System.out.println(a instanceof Animal); System.out.println(a instanceof Cat); }} class Animal { }class Dog extends Animal { }class Cat extends Animal { }The variable a holds a Dog. What do the three checks print?
The answer is true, true, false. The object is a Dog, so the first check passes. A Dog is also an Animal, so the second passes too: instanceofasks “is it this type, or some kind of this type?”. The object is not a Cat, so the last one is false. To get the name of the real class, for example in a debugging message, call a.getClass().getSimpleName(), which returns the String "Dog".
Downcasting: reaching the specific again#
Once instanceof has confirmed the object really is a Dog, you can convert the Animal view back into a Dog view so the compiler lets you call fetch(). That conversion is written (Dog) a and is called downcasting: moving from the general type back down to the specific one. The cast does not change the object at all (it was always a Dog); it only changes the type you are looking at it through. Here is the full pattern, the one you will reach for again and again.
public class Main { public static void main(String[] args) { Animal a = new Dog(); a.speak(); if (a instanceof Dog) { Dog d = (Dog) a; d.fetch(); } Animal c = new Cat(); if (c instanceof Dog) { System.out.println("c is a Dog"); } else { System.out.println("c is not a Dog"); } }} class Animal { void speak() { System.out.println("Some animal sound"); }} class Dog extends Animal { @Override void speak() { System.out.println("Woof"); } void fetch() { System.out.println("Fetching the ball"); }} class Cat extends Animal { @Override void speak() { System.out.println("Meow"); }}Trace it and the shape becomes clear. a holds a Dog, so a.speak() prints Woof by dynamic dispatch. Then a instanceof Dog is true, so we step inside the if, downcast with (Dog) a into d, and now d.fetch() is allowed and prints Fetching the ball. For c, which holds a Cat, c instanceof Dog is false, so we take the else branch and print c is not a Dog. The check protected us from doing something a cat cannot do.
pets fetches, and the cat gets the “not a dog” line. Check first, then cast, then call. An order that casts the cat will crash.Tap a line, then tap where it should go. Or use the arrows.
(int) truncates a double. If you write Dog d = (Dog) c; when c actually holds a Cat, Java cannot pretend a cat is a dog. It throws a ClassCastException and the program stops. The cure is the habit you just saw: check with instanceof first, and only downcast inside the branch where the check was true.ClassCastException: class Cat cannot be cast to class Dog. Fix it so that every animal still speaks, only the dogs fetch, and the last line counts the fetches. Do not change the classes.| Move | Example | What it does |
|---|---|---|
| Upcast | Animal a = new Dog(); | Specific to general. Always safe, and automatic. |
| instanceof | a instanceof Dog | Asks the real object what it is. Gives a boolean. |
| Downcast | Dog d = (Dog) a; | General to specific. Safe only after an instanceof check. |
if (a instanceof Dog d) { d.fetch(); }. If a is a Dog, the new variable d is ready to use inside the if, with no separate cast. You will use this form, and learn exactly where d exists, in sealed types and pattern matching.Try it
Answer each one in your head or on paper first, then open the answer.
zoo[i].speak() printed three different things. In your own words, what decided which speak() ran each time?Animal a = new Cat();, what does a.speak() print, and why is it not Some animal sound?Animal a = new Cat(); then a.fetch();, where only Dog has fetch(). Does this fail when you compile or when you run, and why?Animal a = new Dog(); always safe, but Dog d = (Dog) someAnimal; sometimes crashes?Cow class that extends Animal and overrides speak() to print Moo, then drops a new Cow() into the zoo array. How many lines of the loop must change for it to print Moo in the right place?Practice
Work through these on your own. Each one builds on this page.
Bird, that extends Animal and overrides speak() to print Tweet. Add a new Bird() to the array and run it. Confirm the loop prints the bird's sound without you editing the loop at all.Cat a method scratch() that only cats have. Write a loop over the Animal[] zoo that calls speak() on every animal and also calls scratch(), but only on the ones that are really cats. Use instanceof and a downcast.Animal a = new Dog(); as your example. Say which of the two decides what you are allowed to call, and which decides what actually runs.if on the kind of shape. Shape has String name() returning "shape" and double area() returning 0.0. Circle (with a radius) and Rectangle (with a width and a height) extend it and override both methods. Put two circles and a rectangle in a Shape[], then in one loop print each shape's name and area, the total area, and the name of the largest shape.Further reading#
- Polymorphism, the dev.java tutorial, which calls this behavior virtual method invocation
- The instanceof operator, section 15.20.2 of the Java Language Specification
- ClassCastException in the Java SE 25 API documentation
Short guides on this topic
Quick answers to what people search for about it, with code to run.
Spotted a mistake, or a part that did not make sense? Report it by email