Subtype polymorphism (providing a single interface to entities of different types) and dynamic type selection (DMS) might be one of the most obscure part for Java beginners like me. Here I am trying to write an article on this topic gathering my solutions and approaches to problems in such topic.

Disclaimer: Most materials and examples are collected from University of California Berkeley CS61b Spring 2021. Copyright belongs to the original author.

What are overriding and overloading

  • Overriding: if a “subclass” has a method with the exact same signatures as in the “superclass”, we say the subclass overrides the method.
  • Overloading: methods with the same name but different signatures are overloaded.

The signature of a method is composed of a name and the number, type, and order of its parameters. However, return types, thrown exceptions, and names of parameters are not included in signature.

A subtle point is that there is no “one method overloads another method”. Methods in an overloading situation are like siblings instead of parent and children for an overriding scenario.

/* abs is overloaded */
public class Math {
   public int    abs(int a) { }
   public double abs(double a) { }
public interface Animal {
   public void makeNoise();
}
/* makeNoise is overloaded */
public class Dog implements Animal {
   public void makeNoise(Dog x) {
			System.out.print("Woof");
	 }
}
/* Pig overrides makeNoise() */
public class Pig implements Animal {
	@Override
  public void makeNoise() {
    System.out.print(“oink”);
   }
}

Static and dynamic type

An variable’s static type is specified when the variable is declared, and is determined at compile time. On the other hand, dynamic type is specified when the variable is instantiated, and is determined at runtime. The static type of an variable can never change along the course of running the program, but its dynamic type could be modified by assigning.

Thing a;
a = new Fox();
Animal b = (Animal) a;
Fox c = (Fox) b;
a = new Squid();

The static and dynamic types of variables after running the above code would be:

Static type Dynamic type
a Thing Squid
b Animal Fox
c Fox Fox

As a reminder, what equal assignment does in Java is just “copying bits”. And any reference type objects is stored in the variable as a pointer to its actual address. This is to say, when assigning primitive type objects, such as Integer, Java copy the actual “thing”. On the other hand, Java only copy the address of the referential object to what is on the left side of the equal sign.

Therefore, variable b is holding the address of a after we assign a to b on line 3. This is originally invalid consider (static type of) a is a Thing but b is an Animal, but the casting (Animal) is making the compiler considering a as an Animal type, thus avoiding a compile error. At runtime, the casting is smoothly carried out given the fact that Fox is an Animal, and the variable b is now holding the address of the same Fox object as a.

Dynamic method selection

The rule of DMS is straight forward: if we have a static type X, and a dynamic type Y, then if Y overrides the method from X, then on runtime, we use the method in Y instead.

We can also think about DMS as a 2 step process, where step 1 happens at compile time, and step 2 happens at runtime.

  1. At compile time: We determine the signature S of the method to be called. S is decided using ONLY static types.
  2. At runtime: The dynamic type of the invoking object uses its method with this exact signature S. By “invoking object”, we mean the object whose method is invoked.

Let’s try it out.

public interface Animal {
  default void greet(Animal a) {
    print("hello animal"); }
  default void sniff(Animal a) {
    print("sniff animal"); }
  default void praise(Animal a) {
    print("u r cool animal"); }
}

public class Dog implements Animal {
  @Override
  void sniff(Animal a) {
    print("dog sniff animal"); }
  void praise(Dog a) {
    print("u r cool dog"); }
	public static void main(String[] args) {
		Animal a = new Dog();
		Dog d = new Dog();
		a.greet(d);
		a.sniff(d);
		d.praise(d);
		a.praise(d);
	}
}

The first step to figure out what is going to be displayed is to write down the types.

Static type Dynamic type
a Animal Dog
d Dog Dog

After that, let's write down the signature of each call at compile time based on the invoking objects’ static types.

a.greet(d);  // greet(Animal a)
a.sniff(d);  // sniff(Animal a)
d.praise(d); // praise(Dog a)
a.praise(d); // praise(Animal a)

Lasty, we call the invoking objects’ dynamic type’s method with the same signature we write down previously.

a.greet(d);  // greet(Animal a)  -> “hello animal”
a.sniff(d);  // sniff(Animal a)  -> “dog sniff animal”
d.praise(d); // praise(Dog a)    -> “u r cool dog”
a.praise(d); // praise(Animal a) -> “u r cool animal”

The tricky part here is that praise(Animal a) in Animal interface and praise(Dog a) in Dog class are overloading, instead of the latter one overrides the other one. In such case, no DMS is happening.

Note that in this example, the type of parameter is trivial, as d has the same static type and dynamic type Dog, and Dog is a “Subclass” of Animal so it can be feed into a Animal type variable.

Just for fun

  1. Bird and Falcon

    What gets printed after running the following code?

    
    public class Bird {
    	public void gulgate(Bird b) {
        	System.out.println("BiGulBi"); 
    	}
    }
    public class Falcon extends Bird {
    	public void gulgate(Falcon f) {
        	System.out.println("FaGulFa");
    	}
    }
    
    
    Bird bird = new Falcon();
    Falcon falcon = (Falcon) bird;
    bird.gulgate(falcon);
    falcon.gulgate(falcon);
    
    Static type Dynamic type
    bird Bird Falcon
    falcon Falcon Falcon

    Note that casting won’t change an object’s type, all it do is making the compiler believe the type of whatever after the casting is as stated inside the brackets.

    
    Bird bird = new Falcon();
    Falcon falcon = (Falcon) bird;
    bird.gulgate(falcon);          // gulgate(Bird b)   -> "BiGulBi"
    falcon.gulgate(falcon);        // gulgate(Falcon f) -> "FaGulFa"
    

    No DMS happens in this example.

  2. Dog and ShowDog

    For each assignment, decide if it causes a compile error. For each call to bark, decide what is printed or Java throw a syntax error.

    public class Dog {
    	public String name;
    	public int height;
    	public Dog(String name, int height) {
    		name = name;
    		height = height;
    	}
    	public static void bark() {
    		System.out.println("Woof!");
    	}
    }
    
    public class ShowDog extends Dog {
    	@Override
    	public static void bark() {
    		System.out.println("Yo! What's up!");
    	}
    }
    
    Static type Dynamic type
    o2 Object ShowDog
    sdx ShowDog ShowDog
    dx Dog ShowDog
    ((Dog) o2) Dog ShowDog
    o3 Object ShowDog
    
    Object o2 = new ShowDog("Mortimer", 25); 
    ShowDog sdx = ((ShowDog) o2);            
    sdx.bark();                              // bark() -> "Yo! What's up!"
    Dog dx = ((Dog) o2);                     
    dx.bark();                               // bark() -> "Yo! What's up!"
    ((Dog) o2).bark();                       // bark() -> "Yo! What's up!"
    Object o3 = (Dog) o2;                    
    o3.bark();                               // compile error
    

    In fact, every time when bark() is invoked, the overriding method in the ShowDog class is used. By the way, o3.bark() won’t compile because the compiler think there is no bark() method for an Object (o3’s static type), despite both you and I know o3 is actually a ShowDog with a bark() method. That is how the compiler works. One way to debug this is to cast o3 to types that have bark() method. For example, we can write ((ShowDog) o3).bark().

    Even more fun

    1. Raining cats and dogs

      Below, four classes are defined. What would Java do after executing the main method in the TestAnimal class? Next to each blank, if something is printed write it down. If there is an error, write whether it is a runtime error or compile time error, and then proceed through the rest of the code as if the erroneous line were not there.

      
      public class Animal {
      	public String name, noise;
      	public int age;
      
      	public Animal(String name, int age) {
      		this.name = name;
      		this.age = age;
      		this.noise = "Huh?";
      	}
      
      	public void greet() {System.out.println("Animal " + name + " says: " + this.noise);}
      	
      	public void play() {System.out.println("Woo it is so much fun being an animal!")}
      }
      
      public class Cat extends Animal {
      	public Cat(String name, int age) {
      		super(name, age);
      		this.noise = "Meow!";
      	}
      
      	@Override
      	public void greet() {System.out.println("Cat " + name + " says: " + this.noise);}
      
      	public void play(String expr) {System.out.println("Woo it is so much fun being a cat!" + expr)}
      }
      
      public class Dog extends Animal {
      	public Dog(String name, int age) {
      		super(name, age);
      		noise = "Woof!";
      	}
      
      	@Override
      	public void greet() {System.out.println("Dog " + name + " says: " + this.noise);}
      
      	public void play(int happiness) {
      		if (happiness > 10) {
      			System.out.println("Woo it is so much fun being a dog!")
      		}
      	}
      }
      
      public class TestAnimal {
      	public static void main(String[] args) {
      		Animal a = new Animal("Pluto", 10);
      		Cat c = new Cat("Garfield", 6);
      		Dog d = new Dog("Fido", 4);
      		a.greet();             // ______________________
      		c.greet();             // ______________________
      		d.greet();             // ______________________
      		c.play();              // ______________________
      		c.play(":)")           // ______________________
      		a = c;
      		((Cat) a).greet();     // ______________________
      		((Cat) a).play(":D");  // ______________________
      		a.play(14);            // ______________________
      		((Dog) a).play(12);    // ______________________
      		a.greet();             // ______________________
      		c = a;                 // ______________________
      	}
      }
      

      After executing the first 3 lines in TestAnimal.main, variables has static types and dynamic types as below:

      Static type Dynamic type
      a Animal Animal
      c Cat Cat
      d Dog Dog

      After all execution, their types are (assuming the last line is fixed):

      Static type Dynamic type
      a Animal Cat
      c Cat Cat
      d Dog Dog
      
      a.greet();             // greet() -> "Animal Pluto says: Huh?"
      c.greet();             // greet() -> "Cat Garfield says: Meow!"
      d.greet();             // greet() -> "Dog Fido says: Woof!"
      c.play();              // play() -> "Woo it is so much fun being an animal!"
      c.play(":)")           // play(String expr) -> "Woo it is so much fun being a cat!:)"
      a = c;
      ((Cat) a).greet();     // greet() -> "Cat Garfield says: Meow!"
      ((Cat) a).play(":D");  // play(String expr) -> "Woo it is so much fun being a cat!:D"
      a.play(14);            // Compile Error
      ((Dog) a).play(12);    // play(int happiness) -> Runtime Error
      a.greet();             // greet() -> "Cat Garfield says: Meow!"
      c = a;                 // Compile Error
      

      One possible fix for the last line c = a; is to perform a cast c = (Cat) a. This line triggers a compile error due to the fact that Animal is a “Superclass” of Cat, and assigning an object from the “Superclass” to its “Subclass” won’t compile, even if we all know that a is actually a Cat, rather than other Animal that can’t fit in a Cat variable. By casting, we tell the compiler just trust that a is a Cat, and everything goes fine.

    2. An exercise in inheritance misery

      Cross out any lines that result in compiler errors, as well as subsequent lines that would fail because of the compiler error. Put an X through runtime errors (if any). Don’t just limit your search to main, there could be errors in classes A,B,C. What does D.main output after removing these lines?

      
      public class A {
      	public int x = 5;
      	public void m1() { System.out.println("Am1-> " + x); }
      	public void m2() { System.out.println("Am2-> " + this.x); }
      	public void update() { x = 99; }
      }
      public class B extends A {
      	public void m2() { System.out.println("Bm2-> " + x); }
      	public void m2(int y) { System.out.println("Bm2y-> " + y); }
      	public void m3() { System.out.println("Bm3-> " + "called"); }
      }
      public class C extends B {
      	public int y = x + 1;
      	public void m2() { System.out.println("Cm2-> " + super.x); }
      	public void m4() { System.out.println("Cm4-> " + super.super.x); } // super.super is not allowed in java
      	public void m5() { System.out.println("Cm5-> " + y); }
      }
      public class D {
      	public static void main (String[] args) {
      		B a0 = new A();
      		a0.m1();
      		a0.m2(16);
      		A b0 = new B();
      		System.out.println(b0.x);
      		b0.m1();
      		b0.m2();
      		b0.m2(61);
      		B b1 = new B();
      		b1.m2(61);
      		b1.m3();
      		A c0 = new C();
      		c0.m2();
      		C c1 = (A) new C();
      		A a1 = (A) c0;
      		C c2 = (C) a1;
      		c2.m3();
      		c2.m4();
      		c2.m5();
      		((C) c0).m3();
      		(C) c0.m2();
      		b0.update();
      		b0.m1();
      	}
      }
      
      Static type Dynamic type
      b0 A B
      b1 B B
      c0 A C
      a1 A C
      c2 C C
      ((C) c0) C C
      
      // B a0 = new A();          Compile Error, can't assign an A object to a B variable
      // a0.m1();                  
      // a0.m2(16);                
      A b0 = new B();           
      System.out.println(b0.x); // 5
      b0.m1();                  // m1() -> "Am1-> 5"
      b0.m2();                  // m2() -> "Bm2-> 5"
      // b0.m2(61);                Compile Error, m2(int y) is not defined in class A
      B b1 = new B();           
      b1.m2(61);                // m2(int y) -> "Bm2y-> 61"
      b1.m3();                  // m3() -> "Bm3-> called"
      A c0 = new C();           
      c0.m2();                  // m2() -> "Cm2-> 5"
      // C c1 = (A) new C();       Compile Error, can't assign an A object to a C variable
      A a1 = (A) c0;            
      C c2 = (C) a1;            
      c2.m3();                  // m3() -> "Bm3-> called"
      // c2.m4();                  C.m4() is invalid
      c2.m5();                  // m5() -> "Cm5-> 6"
      ((C) c0).m3();            // m3() -> "Bm3-> called"
      // (C) c0.m2();              Compile Error, can't cast a void to C class
      b0.update();              // update() -> 
      b0.m1();                  // m1() -> "Am1-> 99"
      

    Ecstasy

    1. Athletes

      Suppose we have the Person, Athlete, and SoccerPlayer classes defined below.

      
      class Person {
      	void speakTo(Person other) { System.out.println("kudos"); }
      	void watch(SoccerPlayer other) { System.out.println("wow"); }
      }
      
      class Athlete extends Person {
      	void speakTo(Athlete other) { System.out.println("take notes"); }
      	void watch(Athlete other) { System.out.println("game on"); }
      }
      
      class SoccerPlayer extends Athlete {
      	void speakTo(Athlete other) { System.out.println("respect"); }
      	void speakTo(Person other) { System.out.println("hmph"); }
      }
      

      For each line below, write what, if anything, is printed after its execution.
      Write CE if there is a compiler error and RE if there is a runtime error. If a
      line errors, continue executing the rest of the lines.

      
      Person itai = new Person();                  //
      SoccerPlayer shivani = new Person();         //
      Athlete sohum = new SoccerPlayer();          //
      Person jack = new Athlete();                 //
      Athlete anjali = new Athlete();              //
      SoccerPlayer chirasree = new SoccerPlayer(); //
      itai.watch(chirasree);                       //
      jack.watch(sohum);                           //
      itai.speakTo(sohum);                         //
      jack.speakTo(anjali);                        //
      anjali.speakTo(chirasree);                   //
      sohum.speakTo(itai);                         //
      chirasree.speakTo((SoccerPlayer) sohum);     //
      sohum.watch(itai);                           //
      sohum.watch((Athlete) itai);                 //
      ((Athlete) jack).speakTo(anjali);            //
      ((SoccerPlayer) jack).speakTo(chirasree);    //
      ((Person) chirasree).speakTo(itai);          //
      
      Static type Dynamic type
      itai Person Person
      sohum Athlete SoccerPlayer
      jack Person Athlete
      anjali Athlete Athlete
      chirasree SoccerPlayer SoccerPlayer
      
      Person itai = new Person();                  //
      SoccerPlayer shivani = new Person();         // CE
      Athlete sohum = new SoccerPlayer();          //
      Person jack = new Athlete();                 //
      Athlete anjali = new Athlete();              //
      SoccerPlayer chirasree = new SoccerPlayer(); //
      itai.watch(chirasree);                       // watch(SoccerPlayer other) -> "wow"
      jack.watch(sohum);                           // CE
      itai.speakTo(sohum);                         // speakTo(Person other) -> "kudos"
      jack.speakTo(anjali);                        // speakTo(Person other) -> "kudos"
      anjali.speakTo(chirasree);                   // speakTo(Athlete other) -> "take notes"
      sohum.speakTo(itai);                         // speakTo(Person other) -> "hmph"
      chirasree.speakTo((SoccerPlayer) sohum);     // speakTo(Athlete other) -> "respect"
      sohum.watch(itai);                           // CE
      sohum.watch((Athlete) itai);                 // RE
      ((Athlete) jack).speakTo(anjali);            // speakTo(Athlete other) -> "take notes"
      ((SoccerPlayer) jack).speakTo(chirasree);    // RE
      ((Person) chirasree).speakTo(itai);          // speakTo(Person other) -> "hmph"
      

      Note that, when a method is invoked, the number of actual arguments and the compile-time types of the arguments are used, at compile time, to determine the signature of the method that will be invoked.

    2. Challenge: a puzzle

      Consider the partially filled classes for A and B as defined below:

      
      public class A {
      	public static void main(String[] args) {
      		___ y = new ___();
      		___ z = new ___();
      	}
      	int fish(A other) {
      		return 1;
      	}
      	int fish(B other) {
      		return 2;
      	}
      }
      
      class B extends A {
      	@Override
      	int fish(B other) {
      		return 3;
      	}
      }
      

      Note that the only missing pieces of the classes above are static/dynamic types!
      Fill in the four blanks with the appropriate static/dynamic type — A or B — such
      that the following are true:

      1. y.fish(z) equals z.fish(z)
      2. z.fish(y) equals y.fish(y)
      3. z.fish(z) does not equal y.fish(y)

      To approach this puzzle, we can firstly enumerate all possible combinations of types y and z, in terms of the value returned when calling the fish method.

      Static A Static B
      Static A Dynamic B 1 3
      Static A Dynamic B 1 2
      Static B Dynamic B 1 3

      Where the head column referring to the invoking object types, and the head row referring to the parameter static type. Because static type B dynamic type A will cause a compile error, so there are only three rows. Further, as signature of method to be invoked considers parameter’s static type rather than both static and dynamic type, there are only two columns.

      Given the three statements, z and y should have types shown as follow:

      Static Dynamic
      y A B
      z B B

      Therefore, the answer for this puzzle should be:

      
      public static void main(String[] args) {
      	A y = new B();
      	B z = new B();
      }