Lec 03 - Polymorphism
Slides:
Overloading
Method overloading is when we have two or more methods:
in the same class
with the same method name but a different method signature.
In other words, we create an overloaded method by changing the type, order, and number of parameters of the method but keeping the method name identical.
Overloading vs. Overriding
Override
must have same method descriptor (the return type can be subtype also!)
Overload
must have same method name, in the same class and different method signature.
Method Invocation
"Thanks to" polymorphism and the overriding, we may find it hard to determine which method we invocate will be executed. To make this problem clear, let's introduce the mechanism of method invocation in Java.
Basically, the process can be divided into two parts:
During Compile Time
Goal: To get the method descriptor of the function we are going to invocate. Otherwise generate a compilation-error.
Find the compile-time type target
"Target" is an object. More specifically, with the invocation curr.equals(obj), the "target" should be curr. Then the compile-time type of curr, let's denote it as , is what we are interested in.
This compile-time type is actually a class, in which we need to search for the methods that have the same name with method name we want to invocate. In our case, we should find the method with name equals() in the class .
Find the most specific method
We may have several matching methods, and how do we decide which one's descriptor to get? Here, we can use the principle of "the most specific one".
Basically, we should first pass in the arguments from our invocated method to the method we found during step 1. If the arguments cannot be passed into the method we found, we move to the next available option. (If no more option is left, we will generate a compilation error).
After trying all the options, if there is only one matching method, then we have found and we are done for the compile time!
Otherwise, if there are more than one methods that satisfy our requirements, we can use the principle of "the most specific one" to return the method descriptor of the most specific one.
The principle of "the most specific one": a method is more specific than method if the arguments to can be passed to without compilation error.
Basically, this means that the type of the parameter in is the subtype of the type of the parameter in . For example, equals(Circle) is more specific than equals(Object).
Pass the method descriptor we found to run-time step
As the name suggests, we will pass the all the information in the method descriptor to the next step.
During Run Time
Goal: Find the only method to execute.
Retrieve the method descriptor
As the name suggests, this step will retrieve the information of the method descriptor that is passed from the compile process above.
Find the run-time type target
Similar to the compile process, now we should find the run-time type of the target, let's say it's (If using the same example, should be the subtype of . Otherwise, a compilation error will be generated because this is considered as a narrowing type conversion without explicit casting)
This run-time type information tells us the class that we should start to search from. Basically, it is to search from this class all the way up to the root class Object.
Find a matching method descriptor
Using the retrieved method descriptor, now we should find a method from the run-time class all the way up to the root class Object which has the same descriptor with our retrieved method descriptor.
Till now, we have seen that a variable obj with type Object can have many "similar" methods. In Java, which method is invoked is decided during run-time, depending on the run-time type of the obj. This is called dynamic binding or late binding or dynamic dispatch.
Class methods
The description above only applies to instance methods. Class methods, on the other hand, do not support dynamic binding. The method to invoke is resolved statically during compile time. The same process in compile-time is taken, but the corresponding method implementation in class will always be executed during run-time, without considering the run-time type of the target.
Polymorphism
Methods overriding enables polymorphism, which is the last pillar of OOP, and arguably the most powerful one. It basically states that we should use the base-type class as much as possible.
Since polymorphism will dynamically decide which method implementation to execute during run-time, so that to change how our existing code behaves, we don't have to change a single line of our existing code. Instead, we can just create a new derived/sub "type" and then use polymorphism to achieve what we want to achieve.
Let's use an example to have a glimpse of the power of polymorphism.
At Line 3, depending on the run-time type of curr, the corresponding, customized version of equals is called to compare against obj. So if the run-time type of curr is Circle, then we will invoke Circle::equals(Object) and if the run-time type of curr is Point, then we will invoke Point::equals(Object). This, of course, assumes that Object::equals(Object) is overridden in both classes.
Liskov Substitution Principle
Goal: To provide a way to decide when we misuse overriding and inheritance (a.k.a polymorphism).
Soul/Main content: A subclass should not break the expectations / specifications set by the superclass. In other words, the test cases that are passed in superclass should also be passed in the subclass.
Pure substitution vs. Impure substitution
Pure substitution can be thought of as inheritance should override only parent-class methods (and not add new methods that aren't in the parent class). In this case, the relationship between the derived-class and the base-class (a.k.a parent-class) can be viewed as a "is-a" relationship.
Pure substitution is the ideal way to treat inheritance. However, there are times we may need to add new method elements to the derived-class. In this case, the relationship becomes "is-like-a" relationship and it is known as impure substitution.
The final keyword
final keywordThe final keyword can help prevent a class to be inherited from and a method to be overriden. So, till now, the use of final keyword is as follows:
In a field declaration to prevent re-assignment.
In a class declaration to prevent inheritance.
In a method declartion to prevent overriding.
Abstract class
Goal: To fully take the advantage of polymorphism, we want to make our method (a.k.a a kind of abstraction) as general as possible.
One way to do so is to keep defining the object from the root class Object. For example, suppose we want to generalize equals() to check if two objects are equal or not (and extend it to all the other objects, like Circle, Bicycle etc), we can write the code as follows:
However, this style of code has a prerequisite: the Object class must have the method called equals(). So, what if we want to generalize other functions, like a function getArea() to get the area of a circle, or rectangle? Now the all-time Object method doesn't work because there is no such method called getArea() inside the root class Object!
So, seems that now we want to create something more specific than Object that supports the function we want, yet more general than Circle or Rectangle. Here it comes the other way — The use of abstract class and abstract methods.
An abstract class in Java is a class that has been made into something so general that it cannot and should not be instantiated! Otherwise, a compile error will be generated!
In our example, we may want to create an abstract class called Shape. To do so, we can use the keyword abstract.
The abstract keyword
abstract keywordGoal: Basically, the abstract keyword is used to creat an abstract class and an abstract method.
For example, we can implement our abstract class Shape and the abstract method getArea() as follows:
An abstract method cannot be implemented and therefore should not have any method body.
Concrete class
We call a class that is not abstract as a concrete class. A concrete class cannot have any abstract method. Thus, any concrete subclass of Shape must override getArea() to supply its own implementation.
Interface
Goal: To make a method even more generalisable!
The abstraction that models what an entity can do is called interface.
An interface is also a type and is declared with the keyword interface. Since an interface models what an entity can do, the name usually ends with the "-able" suffix.
For example, let's make the getArea() even more generalisable so that it can not only get the area of a shape, but also the area of a real estate property, etc
Now, for every class that we wish to be able call getArea() on, we tell Java that the class implements that particular interface.
For an abstract class
For example, we want our abstract class Shape to be able to call getArea, we can do as follows:
For a concrete class
For a concrete class, it can also implement the interface. For example,
Interface as Supertype
If a class implements an interface , then . This definition implies that a type can have multiple supertypes because a type can implement multiple interfaces.
Type Casting using an interface
This is a knowledge point that almost all students in the past cohort fail, so it must be very valuable! See more here.
As we have seen in the previous lecture, in Java, two types without a subtype relationship cannot be casted. However, let's consider the code as follows,
Will the Line 14 compile? The answer is yes. The compiling principle for the Java compiler is that:
it does not let us cast when it is provable that it will not work. i.e. casting between two classes that have no subtype relationship. However, for
interface, there is the possibility that a subclass could implement the interface. Therefore, the Java compiler trusts that the programmer knows what they are doing, and allows it to compile.
So, in our example, one such possibility is as follows,
Here, the subtype relationship we can get is and . This cannot tell us the is the subtype of . And based solely on this code, it can compile but it will generate a run-time error.
So, given that we are pretty sure that is not the subtype of , why this code still compiles? That is because the compiler doesn't know whether there is a possibility that has a subclass that implements . If so, the code will run perfectly without any compile-time error and run-time error.
For example, let's assume class extends from and implements . Now, we run the following code
Type casting can work in this case because even though compile-time type of a is A , its actual run-time type is C , which implements I . Since compiler can't foresee what is the actual run-time type of a , it would just be lenient and allow your code to compile.
So, coming back to our problem about this line of code I i2 = (I) new A();. Since the compiler has no idea about what kinds of subclasses A has, so even if class C does not exist in the code. The line I i2 = (I) new A(); would still compile.
However, if you actually declare class A with final keyword which prevents it from being extended with any subclasses. Compiler can be 100% sure that class A cannot hold any instance whose type is a subtype of I in any situation and simply give you compile-time error.
Tips
The essence of the original
equalsmethod inObjectis that it will compare whether two objects are referenced to the same memory address or not.The use of
instanceOfoperator: for example,obj instanceOf Circlewill check if the run-time type ofobjis a subtype ofCircle. (Note that it is the run-time ofobj)In polymorphism, remember to do explicit type casting since sometimes the object from the root class
Objectdoes not have the fields we want.Override the functions that should not be overriden will generate a compilation error.
Include the behavior of the compiler regarding the typecasting containing
Interfaceinto cheatsheet!
Last updated