Polymorphism - 2020
Any Java object that can pass is-a test can be considered polymorphic. Other than objects of type Object, all Java objects are polymorphic in that they pass the is-a test for the class Objects.
Before we move on, let's check the key things about references:
- A reference variable can be of only one type. Once declared that type can never be changed. Note that the object it references can be changed.
- Since a reference is a variable, it can be reassigned to other object unless the reference is declared as final.
- A reference variable's type determines the method that can be invoked on the object the variable is referencing.
- A reference variable can refer to any object of the same type as the declared reference.
- It can refer to any subtype of the declared type.
- A reference variable can be declared as a class type or an interface type. If the variable is declared as an interface type, it can reference any object of any class that implements the interface.
Now, let's look at the code:
public class Food { void eat() { System.out.println("This food is delicious."); } } public class Kiwi extends Food{ void eat() { System.out.println("This Kiwi is delicious."); } } public class Avocado extends Food{ void eat() { System.out.println("This Avocado is delicious."); } } class Poly { public static void main(String[] args){ Food myFood[] = new Food[2]; myFood[0] = new Kiwi(); myFood[1] = new Avocado(); for(Food f: myFood) f.eat(); } }
We defined a Food class as superclass, and we have two subclasses: Kiwi and Avocado. These subclasses will inherit the variables and methods of the superclass such as eat().
Food myFood[] = new Food[2]; myFood[0] = new Kiwi(); myFood[1] = new Avocado();
Here, we declared two reference variables of Food type as an array. Then we assigned Kiwi object to myFood[0] reference variable and Avocado object to myFood[1] reference variable. Note that because myFood is Food type, it can hold the subclass objects, Kiwi and Avocado. When we invoke eat() method, it will override the eat() method of Food class and invoke their own eat() methods as we see from the following output:
This Kiwi is delicious. This Avocado is delicious.
Here we're going to see when we're passing in an argument of superclass type, we can also pass the subtype reference as an argument.
We have a new class Vegetarian with a method called digest(Food f) which takes Food type reference as an argument and invokes eat() method:
public class Vegetarian { public void digest(Food f) { f.eat(); } }
We redefined the main():
class Poly { public static void main(String[] args){ Vegetarian vege = new Vegetarian(); Food f = new Food(); Food kiwi = new Kiwi(); Food avocado = new Avocado(); vege.digest(f); vege.digest(kiwi); vege.digest(avocado); } }
Note that we're passing in three Food reference variables as an argument of the digest() method: The first one, referencing Food object:
vege.digest(f);
The second and third are referencing Kiwi and Avocado objects, respectively:
vege.digest(kiwi); vege.digest(avocado);
Because the digest() takes Food type, it can also take object of subclasses of Food type.
Here is the output:
This food is delicious. This Kiwi is delicious. This Avocado is delicious.
Any time we have a class that inherits a method from a superclass, we have the opportunity to override the method unless it's marked final. The primary benefit of overriding is the ability to define behavior that's specific to a particular subclass type.
In the examples of the previous sections, we overrode the eat() method in the subclasses. The reason we were able to overrode the the eat() method is that we had the same arguments and return types. The eat() method of the Food class does not take any argument and return type is void and the eat() methods of the subclasses do not take any argument either and the return types are all void.
In the preceding codes, we use a Food reference to invoke a method on a Kiwi object. However, the compiler will allow only methods in class Food to be invoked when using a reference to a Food. The following would not be legal given the preceding code:
public class Kiwi extends Food{ void eat() { System.out.println("This Kiwi is delicious."); } void cut() { System.out.println("Cut the Kiwi."); } } class Poly { public static void main(String[] args){ Kiwi kiwi = new Kiwi(); Food kiwiFood = new Kiwi(); kiwi.cut(); kiwiFood.cut(); // Food class doesn't have the cut() method } }
Remember, the compiler looks only at the reference type, not the instance type. Polymorphism lets us use a more abstract supertype reference to refer to one of its subtypes.
The overriding method cannot have a more restrictive access modifier that the method being overridden. For example, we can't override a method marked public and make it private. So, the following won't compile:
private class Kiwi extends Food{ void eat() { System.out.println("This Kiwi is delicious."); } void cut() { System.out.println("Cut the Kiwi."); } }
Think about it: if the Food class advertises a public eat() method and someone has a Food reference (a reference declared as type Food), that someone will assume it's safe to call eat() on the Food reference regardless of the actual instance that the Food reference is referring to. If a subclass were allowed to sneak in and change the access modifier on the overriding method, then suddenly at runtime - when the JVM invokes the true object's(Kiwi) version of the method rather than the reference type's (Food) version - the program would crash or die.
public class Kiwi extends Food{ private void eat() { System.out.println("This Kiwi is delicious."); } } class Poly { public static void main(String[] args){ Food f = new Kiwi(); f.eat(); } }
The variable kiwiFood is of type Food, which has a public eat() method. But remember that at runtime, Java uses virtual method invocation to dynamically select the actual version of the method that will run, based on the actual instance. A Food reference can always refer to a Kiwi instance, because Kiwi is-a Food. What makes that superclass reference to a subclass instance possible is that the subclass is guaranteed to be able to do everything that superclass can do. Whether the a Kiwi instance overrides the inherited methods of a Food or simply inherits them, anyone with a Food reference to a Kiwi instance is free to call al accessible Food methods. For that reason, an overriding method must fulfill the contract of the superclass. Here are the rules for overriding a method:
- The argument list must exactly match that of the overridden method. If they don't match, we can end up with an overloaded method we didn't intend.
- The return type must be the same as, or a subtype of, the return type declared in the original overridden method in the superclass.
- The access level can't be more restrictive that the overridden method's.
- The access level can be less restrictive than that of the overridden method.
- Instance methods can be overridden only if they are inherited by the subclass. A subclass within the same package as the instance's superclass can override any superclass method that is not marked private or final. A subclass in a different package can override only those non-final methods marked public or protected (since protected methods are inherited by the subclass).
- The overriding method can throw any unchecked (runtime) exception, regardless of whether the overridden method declares the exception.
- The overriding method must not throw checked exceptions that are new or broader than those declared by the overridden method.
- The overriding method can throw narrower or fewer exceptions.
- We cannot override a method marked final.
- We cannot override a method marked static.
- If a method can't be inherited, we cannot override since overriding implies that we're reimplementing a method we inherited.
Let's look at another code similar to the previous sample:
package com.bogotobogo; public class Overriding { public static void main(String[] args) { Shape shapes[] = new Shape[2]; Circle c = new Circle(10); Rectangle r = new Rectangle(5, 10); shapes[0] = c; shapes[1] = r; for(Shape s : shapes) System.out.println(s.area()); } } abstract class Shape { public abstract String area(); } class Circle extends Shape { public Circle(double a) { radius = a; } public String area() { return "Circle: Area = " + PI*radius*radius; } private double radius; private final double PI = 3.14; } class Rectangle extends Shape { Rectangle(double w, double h){ width = w; height = h; } public String area() { return "Rectangle: Area = " + width*height; } private double width; private double height; }
One thing about the highlighted line:
Shape shapes[] = new Shape[2];
Here, we're not instantiating Shape objects from the abstract class. Though we cannot make a new instance of an abstract type, we can make an array object declared to hold that type.
The output:
Circle: Area = 314.0 Rectangle: Area = 50.0
- | Overloaded Methods | Overridden Methods |
---|---|---|
Argument(s) | Can change | Must not change. |
Return type | Can change | Can't change except for covariant returns. |
Exceptions | Can change | Can reduce or eliminate. Must not throw new or broader checked exceptions. |
Access | Can change | Must not make more restrictive (can be less restrictive). |
Invocation | Reference type determines which overloaded version (based on declared argument types) is selected. Happens at compile time. The actual method that's invoked is still a virtual method invocation that happens at runtime, but the compiler will already know the signature of the method to be invoked. So, at runtime, the argument match will already Java been nailed down, just not the class in which the method lives. | Object type (in other words, the type of the actual instance on the heap) determines which method is selected. Happens at runtime. |
Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization