| Home | Resources | Papers | Essays | Short Fiction | About |

Polymorphism

Table of Contents

Introduction

In biology, polymorphism is defined as the phenomenom where different phenotypes can appear within the population of a species—i.e., the difference between jaguars and panthers (panthers being melanated jaguars). It is a combination of the greak πολύ (polý), or "many", and μορφή (morfí), or "form". Computer scientists eventually repurposed the phrase to fit the phenomena in complex type systems whereby a single variable can take on many different type forms. Polymorphism is deeply embedded in the Java type system and is something that every Java programmer must be intimately familiar with.

The Object Class

It turns out that every single object in Java is a subclass of the Object class. If we take a look at the standard library documentation for Object we'll find that there are a handful of methods which all objects get for free: clone(), equals(...), hashCode(), etc. This means that every single non-Object object in Java is inherently a polymorphic type. It contains within it the type Object and whatever else derived type was defined via the class definition. For example, let's go back to our Animal classes from the previous chapter:

 1: class Animal{
 2:     String name;
 3:     Animal(String n){
 4:         name = new String(n);
 5:     }
 6:     void eat(){
 7:         System.out.printf("%s eats!%n", name);
 8:     }
 9: }
10: class Mammal extends Animal{
11:     Mammal(String n){
12:         super(n);
13:     }
14:     void breathe(){
15:         System.out.printf("%s breathes!%n", name);
16:     }
17: }
18: final class Dog extends Mammal{
19:     Dog(String n){
20:         super(n);
21:     }
22:     void bark(){
23:         System.out.printf("%s barks!%n", name);
24:     }
25: }

We can also throw in a few more:

 1: final class Cow extends Mammal{
 2:     Cow(String n){
 3:         super(n);
 4:     }
 5:     void moo(){
 6:         System.out.printf("%s moos!%n", name);
 7:     }
 8: }
 9: final class Pig extends Mammal{
10:     Pig(String n){
11:         super(n);
12:     }
13:     void oink(){
14:         System.out.printf("%s oinks!%n", name);
15:     }
16: }

Because Java allows for subtyping polymorphism, I can store three distinct Animal-derived instances in a single Object-based container:

1: public class Farm{
2:     public static void main(String[] args){
3:         Object[] arr = {new Dog("Lassie"), new Cow("Bessie"), new Pig("Porky")};        
4:     }
5: }

If you send this code through the javac compiler you'll find no errors are thrown. This is because Dog, Cow, and Pig are ultimately all derived from Object and can therefore be stored as Object​s. We'll investigate this a bit more in the section on polymorphic subtyping.

Method Overriding

One of the most significant advantages of polymorphism in object-oriented programming language is the ability to override ancestral methods. Let's alter the example shown in Listing 3 to try to have each animal make their trademark sound:

 1: public class Farm{
 2:     public static void main(String[] args){
 3:         Animal[] arr = {new Dog("Lassie"), new Cow("Bessie"), new Pig("Porky")};
 4:         for(Animal a : arr){
 5:             if(a instanceof Dog){
 6:                 ((Dog) a).bark();
 7:             }else if(a instanceof Cow){
 8:                 ((Cow) a).moo();
 9:             }else if(a instanceof Pig){
10:                 ((Pig) a).oink();
11:             }
12:         }
13:     }
14: }

Yuck! But if we compile and run it we will find that it does the job just fine:

josephraskind@stargazer:/tmp/polymorphism$ java Farm.java 
Lassie barks!
Bessie moos!
Porky oinks!

We can see that Listing 4 works well enough, but it makes for some rather tiresome coding. Furthermore, anytime we define a new Animal we'll need to adjust Farm to check for another possible derived instance. A way to skirt that is to utilize method overriding. We can reconfigure our class definitions to allow for the technique:

 1: class Animal{
 2:     String name;
 3:     Animal(String n){
 4:         name = new String(n);
 5:     }
 6:     void eat(){
 7:         System.out.printf("%s eats!%n", name);
 8:     }
 9:     void makeSound(){
10:         System.out.printf("%s makes a noise!%n", name);
11:     }
12: }
13: class Mammal extends Animal{
14:     Mammal(String n){
15:         super(n);
16:     }
17:     void breathe(){
18:         System.out.printf("%s breathes!%n", name);
19:     }
20: }
21: final class Dog extends Mammal{
22:     Dog(String n){
23:         super(n);
24:     }
25:     void makeSound(){
26:         System.out.printf("%s barks!%n", name);
27:     }
28: }
29: final class Cow extends Mammal{
30:     Cow(String n){
31:         super(n);
32:     }
33:     void makeSound(){
34:         System.out.printf("%s moos!%n", name);
35:     }
36: }
37: final class Pig extends Mammal{
38:     Pig(String n){
39:         super(n);
40:     }
41:     void makeSound(){
42:         System.out.printf("%s oinks!%n", name);
43:     }
44: }

Method overriding occurs when the exact same method (return type, name, and arguments) is defined in a subclass. The Java runtime will invoke whicher is the most recent definition of the method within the inheritance hierarchy. We can see how this plays out with an updated Farm class:

1: public class Farm{
2:     public static void main(String[] args){
3:         Animal[] arr = {new Dog("Lassie"), new Cow("Bessie"), new Pig("Porky")};
4:         for(Animal a : arr)
5:             a.makeSound();
6:     }
7: }

After compiling and running we get the exact same results as Listing 5! How can that be the case? Whenever a method is invoked in Java, it will be checked to see if it has been overridden. If it has, then the "closest" definition relative to the instance type will be used. So even though the compile-time type of a on Line 5 is Animal the actual, underlying runtime-type is Dog, Cow, or Pig. This does not mean that Animal can at all times be a stand-in for any derived type or treated as such. For example, if Dog contained an instance method called fetch then a could not reference it as it would not also be shared by Animal.

Overriding vs Overloading

Students often get confused between method overriding and method overloading. Overriding occurs when a subtype reimplements a method defined above it in its class hierarchy—this requires declaring a method with the exact same method header. Overloading occurs whenever a method is declared with the same name, but a different set of arguments. It is always best practice to add an @Override annotation before functions you are trying to override. If the annotation is used and the function is not being overridden, but rather overloaded, the compiler will throw an error. Otherwise, the compiler will let you make the mistake of overloading instead of overriding a function! Below is a classic example taken from Effective Java (3rd Edition):

 1: public class Bigram {
 2:     private final char first;
 3:     private final char second;
 4:     public Bigram(char first, char second) {
 5:         this.first = first;
 6:         this.second = second;
 7:     }
 8:     public boolean equals(Bigram b) {
 9:         return b.first == first && b.second == second;
10:     }
11:     public int hashCode() {
12:         return 31 * first + second;
13:     }
14:     public static void main(String[] args) {
15:         Set<Bigram> s = new HashSet<>();
16:         for (int i = 0; i < 10; i++)
17:             for (char ch = 'a'; ch <= 'z'; ch++)
18:                 s.add(new Bigram(ch, ch));
19:         System.out.println(s.size());
20:     }
21: }

Although it looks like equals is overriding Object's equals method, it is not. This is because equals should take an Object type not a Bigram type. Java will have no problem letting this mistake be made. The solution? adding an @Override tag will prevent the above code from compiling. I leave it as an exercise to the reader to test that out.

Polymorphic Subtyping

As mentioned above, the ability for objects in Java to shift between types in the class hierarchy is called polymorphic subtyping. It is often a difficult topic for students to conceptualize as it appears to break the strong type system that has be presented heretofore. This sort of polymorphism is not magic neither is it completely lawless. You can follow a simple algorithm to determine whether or not a class can be treated like another class:

  • A class may be treated as a superclass
  • A superclass may be treated as a subclass

Examples and Oddities

Below are a few exampels of explicit conversions between different object types:

 1: public class Farm{
 2:     public static void main(String[] args){
 3:         Dog d = new Dog("Lassie");
 4:         Cow c = new Cow("Bessie");
 5: 
 6:         Animal a = (Animal) c;                //   Legal conversion: subclass to superclass
 7:         a = (Animal) d;                       //   Legal conversion: subclass to superclass
 8:         d = (Dog) a;                          //   Legal conversion: superclass to subclass (same instance)
 9: 
10:         try{
11:             Dog fake_d = (Dog) ((Animal) c);  // Illegal conversion: superclass to subclass (different instance)
12:         }catch(ClassCastException e){         // (Runtime Error)
13:             System.out.println(e);
14:         }
15:         try{
16:             Cow fake_c = (Cow) a;             // Illegal conversion: superclass to subclass (different instance)
17:         }catch(ClassCastException e){         // (Runtime Error)
18:             System.out.println(e);
19:         }
20:         try{
21:             Cow fake_c = (Cow) d;             // Illegal conversion: subclass to unrelated subclass
22:         }catch(ClassCastException e){         // (Compile-time Error)
23:             System.out.println(e);
24:         }
25: 
26:     }
27: }

The compiler will trust that the subtype conversions being performed are valid knowing that the runtime system will eventually catch any mistakes.

Why Polymorphism?

The largest advantage of polymorphism can be witnessed in Listing 7, we can store many different subtypes in a single container and have each perform a unique action using an overridden method. This behavior simplifies interfaces/APIs and makes it easier to reuse code wherever applicable. Polymorphism also grants the ability to replicate real-world "is-a" relationships: a Student is-a Person, a Book "is-a" Item, a Pig "is-a" Animal, etc.

Why Not Polymorphism?

Again, much like inheritance, polymorphism presents as a double-edged sword. It becomes difficult to trust the types being supplied when they can be absolutely any subtype. It also is unlikely that all subtypes perfectly reflect the is-a relationship that might be given by the parent (a Square "is-a" Rectangle, but you cannot independently set the length and height). Finally, there are major performance overheads associated with managing a runtime typechecking system (which is mandatory in a type-safe polymorphic language)1.

Exercises

  1. The chapter introduces method overriding. Add an eat method to Dog that overrides the one defined in Animal and prints "[name] wolfs down food!". Then add a Cat class that extends Mammal and also overrides eat to print "[name] nibbles delicately!". Write a main that creates an Animal[] containing one instance each of Animal, Mammal, Dog, and Cat, and calls eat on each through the array. Which version of eat is called for each element? What does this confirm about how Java resolves method calls at runtime?
  2. The chapter shows that @Override is optional but recommended. Remove @Override from Dog's eat method and introduce a typo — change it to public void eatt(). Does it compile? Does it behave as expected? Now restore @Override and introduce the same typo. What does the compiler tell you? What does this demonstrate about the value of @Override?
  3. The chapter states that polymorphism allows a single method to operate on any subtype. Write a static method makeEat(Animal a) that calls a.eat(). Call it with instances of Animal, Mammal, and Dog. Which version of eat is called in each case? Explain why in terms of dynamic dispatch.
  4. The chapter shows that a method defined in a parent class is inherited by all subclasses. Add a toString method to Animal that returns a String describing the animal (e.g. "Animal: Rex"). Call System.out.println on instances of Animal, Mammal, and Dog — Java automatically calls toString when an object is passed to println. What prints for each instance? What does this confirm about how inherited methods behave across the hierarchy?
  5. The chapter shows that a subclass can call its parent's constructor using super. Add a second field String sound to Animal and update its constructor to accept both name and sound. Trace the changes needed through Mammal and Dog to keep everything compiling. Then print the sound field from a Dog instance to verify it was correctly passed up the chain.
  6. The chapter states that a reference of a parent type can hold an instance of any subtype. Write a program that assigns a Dog instance to an Animal reference and attempt to call bark through that reference. What does the compiler say? What does this tell you about the tradeoff between generality and specificity when using parent type references?
  7. The chapter shows that overriding allows subclasses to specialize inherited behavior. Add a speak method to Animal that prints "...", override it in Mammal to print "[name] makes a sound!", and override it again in Dog to print "[name] barks!". Create one instance of each class, store them all in an Animal[], and call speak on each. Verify the most specific version is called in each case.
  8. Write a static method describeAnimal(Animal a) that prints the name of the animal and calls eat. Call it with instances of Animal, Mammal, and Dog. Which version of eat is called in each case and why? What does this demonstrate about the value of writing methods that accept parent types rather than specific subtypes?
  9. The chapter introduces polymorphism as a way to write more general code. Without polymorphism, a program that needed to feed every animal on a farm would require a separate method for each type: feedDog, feedCat, feedBird, and so on. Write both versions — one with a separate method per type and one with a single feed(Animal a) method — for at least three animal types. Count the number of lines of code in each version. What happens to the non-polymorphic version when you add a fourth animal type? What happens to the polymorphic version?
  10. The chapter presents polymorphism as resolving the tension between generality and specificity. Revisit the composition-based hierarchy you wrote in the inheritance exercises and consider: could you achieve the same polymorphic behavior (storing different types in a single array and calling a shared method) using composition alone, without inheritance or interfaces? Try it and describe what you find. What does this tell you about when polymorphism through subtyping is genuinely necessary?

Footnotes:

1

C++ skirts some of this overhead by allowing for undefined behavior.

Contact: [email protected] | rss feed | Compiled with org-mode | Licensed under CC BY-NC-SA 4.0