Polymorphism
One reference type, many runtime shapes, and why a single loop can drive different behavior per object.
Finished reading?
Mark this session so you can track where you are.
One reference type, many runtime shapes, and why a single loop can drive different behavior per object.
Finished reading?
Mark this session so you can track where you are.
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.
Animal a = new Dog(); means, and why the Dog behavior still runs.instanceof to ask an object what it really is at runtime.(Dog) a to 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). If those words feel shaky, reread them first. Polymorphism is what those two ideas were building toward.Polymorphism comes 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. Let us start, as always, 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 { void speak() { System.out.println("Woof"); }} class Cat extends Animal { void speak() { System.out.println("Meow"); }}Woof 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 back when one customer became many in the first OOP session. 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?
It can, and this is the heart of the session. 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 { void speak() { System.out.println("Woof"); }} class Cat extends Animal { void speak() { System.out.println("Meow"); }}Woof Meow Some animal sound
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.
One 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.
for a in zoo: a.speak() just worked because Python checks at runtime. Java reaches the same place, but it asks you to be explicit: the array is typed Animal[], and the language guarantees every element really is an Animal. You get the flexibility and the safety net at once.Let us slow the array down to a single variable so the mechanism is naked. 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 { void speak() { System.out.println("Woof"); }} class Cat extends Animal { void speak() { System.out.println("Meow"); }}Woof 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.
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 passing when you learned overriding; here is the why behind it.
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.
Code 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.
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.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), false otherwise.
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 { void speak() { System.out.println("Woof"); } void fetch() { System.out.println("Fetching the ball"); }} class Cat extends Animal { void speak() { System.out.println("Meow"); }}Woof Fetching the ball c is not a Dog
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.
(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. Never downcast on a hunch.Animal a = new Dog(); Specific to general. Always safe, automatic.a instanceof Dog Asks the real object what it is. Returns a boolean.Dog d = (Dog) a; General to specific. Safe only after an instanceof check.Try each one yourself 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?Take these away. They continue exactly what we just did.
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 additionally calls scratch() 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.Cat in an Animal variable, then downcast it to Dog without any instanceof check, and call a Dog-only method. Run it, read the error Java gives you, and then fix it by adding the instanceof guard. Write down, in one sentence, what the error was telling you.