Transcript for:
Understanding Class Hierarchies and Polymorphism

So, we saw that we can arrange classes in a hierarchy. We can define a subclass that extends a parent class. So, this was the example we had before.

So, we had an employee class and then we created a class called manager which extended the employee class. So, the subclass inherits the instance variables and the methods in the parent class. So, there are some instance variables for the name and the salary and there are these methods which set the name, set the salary, get the name, get the salary and then there is this double bonus method which calculates the bonus given the percentage of bonus.

So, whenever we create a manager object, all these parts are implicitly there and in addition, The manager object we said can add new instance variables and new methods. So, it is an extension of the employee class. The other thing we said was that these private components of the parent class are not visible to the subclass because in general we said the subclass may be used or implemented by somebody different from the person who created the parent class.

So, the only way that the Child class can get access to these through the public method. So you can use these methods, of course, but also at the time of constructing, you can use a super method to call the constructor of the parent class. So though we have seen that a subclass can add more instance variables and methods, what we did not see was that you can also change some of the methods that are given. So in particular, look at this.

So we have a bonus which is calculated for employees and perhaps managers. have a different type of bonus. So maybe the way we want to compute a bonus for the manager is different from the way we want to compute the bonus for a normal employee. So we should be able to write a different implementation of bonus, which will override, it will override the method which is given by default from the parent class. So here could be a definition of the manager's bonus.

So the manager's bonus has the same signature or this, so it takes a float percentage as its input and it turns a double as the output. And notice now that we are not actually computing the manager's bonus in terms of the salary. If you go back, you will see that for the employee, the bonus was created, computed as a percentage of salary.

But notice that the salary is a private variable of employee. And since salary is a private variable of employee, it is not visible to manager. So, I cannot write here something like 1.5 times percentage by 100 times salary because salary is not available to me.

So, this is not allowed. But what I can do is I can call the bonus of the employee using again this notion of super. So, super in general talks about the function which is developed, which is available one level up in the class header. So, you go to the parents thing, compute the bonus there, take whatever result you get from that and add 50 percent to it. So, you multiply it by 1.5.

So, this way we can get around the problem that a manager has no access to the primary variable salary which is required to compute the bonus and yet because employee has provided a way to do it, you can kind of piggyback the new overridden function bonus in the manager class by using super to call the parent owner. So, now let us look at this definition. So, we say employee E is a new manager and this is legal we said because every time we expect an employee we are okay with a manager because manager is a subtype, it is a subclass.

So, think of set theory. So, every manager is an employee. So, now what about the instance variables?

So, we know that manager has this extra instance variable which is a secretary and it has an extra method to set the secretary name and to get the secretary name. So, can we take this employee which we know to be a manager and invoke setSecretary on it. Now, the problem with this is that e in general is declared to be an employee. So, as far as Java's type checking system goes, the employee class has no setSecretary method.

So, if I call e dot setSecretary statically, it will be illegal, because e as a type does not have that method. So, static type checking will not allow this. But of course, we know that manager is actually the type of the object at runtime because that is how we have assigned it.

So, how do we get that? Now, here what happens for instance, if we take the same e and we call bonus on it, remember now that there are two types of bonus available to us, we had the original bonus which comes with an employee and we have overridden this by defining a bonus available with a manager. So, we could potentially think of two ways. So, if we follow the static route that we use for disallowing said secretary, then in the static sense, an employee variable should invoke the bonus which is defined in the employee class. However, if we are able to go past this and look at the dynamic status of the object, then we would rather use the bonus in the manager.

So which of these do you think we will use? So Java actually uses this notion of dynamic dispatch. So dynamic dispatch, which is also called dynamic binding or late method binding.

it waits to see what the runtime of the object is like. And depending on the type of that runtime object, it chooses the most appropriate function with respect to that runtime type. So, in this case, at runtime, it will see that E is actually a manager and manager has an overridden definition of bonus.

And therefore, in this particular case, what we will get is this version. So, notice that there is a subtle difference here. So, when I try to invoke a function which is not defined for the static type, then I cannot define it even though dynamically it is assigned to a more capable object which has that function.

But if I do have that function, and there is a better version of that function, then dynamic dispatch kicks in and I can use that version. So this is the default in Java. So in Java, essentially, whether a function can be called or not depends on the static type, which function is called depends on the dynamic type.

So Java uses dynamic dispatch by default, not all object oriented languages do this. So, in C++ for instance, which is another popular object oriented language, if you want this behavior, you have to define that method or function to be something called a virtual function. If you do not call it a virtual function, then the static function which is associated with the class which you have defined your variable to use will be used for that function, for that variable.

So, virtual functions are explicitly declared in C++. In Java, every function behaves like this. So, every time we have a method, it is dynamically taken based on the runtime type of the object. So this now allows us to do something interesting.

So supposing we define an employee array. So let us just have a simple employee array which has two elements in it. And what we do is because every position in this array should be an employee object, but we know that employees objects can point to manager objects. Employee are Managers are employees remember. So, I can create an explicit employee and an explicit manager and I can make the first element of the array point to the employee and the second element of the array point to the manager.

So, now I have a kind of heterogeneous array, it is notionally an array of employees, but one of them is an employee, the other is manager. Now I run a loop. So, I go from 0 to 1 and I print out the bonus for that employee. given a 5% bonus. So, what will happen here is because of dynamic dispatch, at runtime for this particular employee for array 1, it will pick up the manager bonus, whereas for the emp array 0, because that was already an employee, it will pick up the employee bonus.

So, without doing anything explicit, just by running over a loop and calling this function on each element in the array, Different elements in the array which are at different levels in this class hierarchy will automatically call the correct function using dynamic dispatch. So in some sense, every employee in this array knows how to calculate its bonus correctly. So this is the power of dynamic dispatch, that we can write this kind of a generic loop without having to worry about, you know, specifically looking at the type of each object and then deciding how to invoke it.

Now if you recall. At the beginning when we discussed the motivation for object oriented programming, we said that the language that first introduced object oriented programming was Simula. And the reason why Simula needed objects was because of this event simulation loop.

So you create a queue consisting of the first event in your simulation list. And then you remove the head of the queue, simulate it and then it generates new events and you put it in the queue. And our question was how do we have such a queue first of all, which is well typed. but yet may hold many different types of events. And the second question was, how do we make Sure that the simulate E is not a complicated switch of cases, right?

I want, do not want to say if E is event of type 1 do this, if E is event of type 2 do that. So, I do not want to explicitly try to tell you how to simulate different types of events. So, this is where dynamic dispatch was used in Simula and it has come down as one of the primary benefits of using this object oriented approach. So, we see it in Java also. So, basically Just like that employee array which had different types of employees in it, here we have a queue which have different types of events in it.

So each of the events in the queue will be a subtype of some parent event type. Each of them will have its own simulate method which overrides the parent simulate method. So when I pick up an actual event from this queue and I simulate it, it will run the local version of simulate.

So that's how this dynamic dispatch works. And this was really the motivation of introducing it in a language like Simula. And that is why we use it by default in Java as well. So this type of dynamic dispatch is also known in the object oriented programming literature as polymorphism, more specifically as runtime polymorphism or inheritance polymorphism, because this comes as a result of the runtime behavior of an object and through the inheritance hierarchy.

So a polymorphism. is a word which talks if you have a little bit of understanding of the Greek origin of such words. So, poly means many, right and morph refers to form.

So, this is saying that the same function takes many forms and which form of the function you use is determined at runtime based on the current objects position in the hierarchies, hence runtime polymorphism or inheritance polymorphism, right. So, this is so dynamic dispatch. This is one way of saying technically how the function is chosen.

So dynamically you choose that function which is most appropriate for the object. But at a programming language level in object oriented literature, this is sometimes referred to as inheritance or runtime polymorphism. So runtime polymorphism is one situation where when I call a function, it may behave differently based on the argument. In this case, the context because it is not really the argument. I invoke.

bonus on a manager object, it does something, I invoke bonus on an employee object, it does something else. The other situation is when actually it depends on the argument in the sense of the signature. So, remember that the signature of a function is the types of its arguments, the list of its input parameters and their types and the output type.

So, we already said that we can have different functions which have the same name. but with different signatures and we have seen this in the context of constructors. So we said you can have different constructors which take different numbers of arguments.

So you just write each as a separate function and the one that matches the invocation will be used. And this kind of overloading of functions is not possible in a language like Python, but it is available in Java. So in particular, if you look at the built-in class that handles arrays in Java, it has a static sort method.

which can sort arbitrary arrays of basic scalar types, you know, int, double, float and all that. So, for instance, you can create a double array of size 100 and an int array of size 500. And then you can use the same sort function in arrays, it is a static function, so you can call it without having to do anything. And you pass it in one case, the double array and in one case, the integer array.

So, you call the same function with two different types of arguments. Now, the way that this is working is not through inheritance polyphosphism, but overloading. So if you look inside the arrays class, then you will find that there are actually multiple definitions of sort. So there is a sort with a signature double a, there is another sort with a signature int array a.

So depending on which one you call, so this one will come and invoke this definition, and this one will come and invoke this definition. So depending on which one you call, the type of the argument, the signature of the call will match the signature of the function definition. So, this is overloading. So, this is a very different way in which the same function can apparently take on different forms.

And it is nothing to do with polymorphism. They are two completely different things which have the same effect you may imagine, which is that the behavior of the function at runtime is determined by the types of the arguments or the context in the case of runtime polymorphism. The main thing to note about overloading is that these, the choice between the methods is static based on the signature.

So we know in advance that this D array was of this type and therefore, it will pick this type. So it is not a runtime decision at all. On the other hand, when we override a function, as we saw before, then we have different methods with the same signature, but the choice is static.

So, if we have an employee and we call it, then it is employee bonus. And if we have a manager and we call it, it is a manager bonus. But the interesting case is when we have an employee variable pointing to a manager object. So, you have a more specialized object.

being pointed to by a less specialized type variable. At this point, we have this polymorphism of dynamic mismatch. So, this is now a case where it is not based on the static type of the object, but the dynamic type of the object. So, the choice of which function to call is made at runtime.

So, you should be clear about all these things. So, if you have no confusion, that is the type of the declaration matches the type of the object assigned to it, then overriding will determine whether you are using the function which is defined in the current class. or in the parent class. If there is nothing in the current class, you will inherit.

If you have something in the current class or if manager has redefined bonus, then every manager object will call its own bonus. This is the case where we have employeeE equal to some new manager. So then we have to do this.

So now let us get back to the earlier case where we were asking about Calling functions which we know are available dynamically, but are statically not available. So, we said if we have employee e is equal to new manager, and we want to use the set secretary function, then static type checking we said will disallow this. But supposing we are confident that this actually is available, because we know that at this point, this e is actually pointing to a manager.

So, how can we get Java to accept this? So, the way to get Java to accept this is to explicitly promote the type e. So, this is called type casting.

So, type casting takes a variable of one type and converts it to another type. In this case, if you are converting something and it is now possible to do it, it will work. So, I take e and then this, so I, the way you cast is to put the type that you want in brackets before the thing. So, this manager in brackets before the e, so this part.

So, this converts the e, which is an employee statically into a manager type. And then now that is a manager type, Java is willing to accept that you can call this function setSecretly. What if it is not?

So, if you happen to invoke this cast at a time when it is not possible, for example, e is actually an employee. Then it is illegal to, you cannot convert an employee to a manager because there are extra instance fields which have not been set. So, an employee, remember that every manager is an employee, but not every employee is a manager. So, this task is not legal if you do not have this kind of an assignment before this.

So, this would give you a runtime error, which is of course a bad thing. So, how can you avoid this runtime error because maybe this is in a complex situation where you are not sure whether this is possible or not. So, what you can do is you can actually query the status of the variable.

So, you can ask whether at runtime, this variable e is an instance of manager. So currently, what is e pointing to? Is it a manager or not? So, this is an, it is like a usual kind of status query, it will tell you true or false. And then you can say that if this is true, if e is an instance of manager, then and only then do you want to make this class and set the secretary, otherwise you will do something else.

So, this is an interesting thing that we can actually query the status. So, in Python also you have this type function, you can say type of x is something and you can find out the type of a variable at runtime. So, Java also allows this and in fact, it allows more as we will see later on. So, this ability of a within the language.

So, when you are running the program to query the status of things which are artifacts of the program. So, remember, we are not asking about the value of x, which is a normal thing. We are not saying is e equal to something. We are saying is the type of So, this is something about the program that is running rather than the state of the program. So, this kind of thing is called reflection.

So, reflection in English means not think back and reflect, not reflection in the sense of mirror. but the philosophical kind of reflection, you sit back and reflect on your actions, you reflect on your past. So you reflect, meaning you kind of think internally, you introspect, think about yourself. So Java allows a program to think about itself.

So this is technically called reflection in programming languages. And this is a very simple case of that this instance of. Now, type casting is not only for objects. So, type casting can actually also be used for the basic types that we introduced earlier which are not objects like int and float. So, for instance, you can take a variable of type double and then you can convert it to an int by using the type name int in a cast.

So, casting can be used because you should use it judiciously and you have to look up the Java. programming reference to understand what will happen. For example, when I convert d to an int, presumably it will truncate in the sense that it will cut off this 0.98 and make it 29. But you have to check that this is the intended behavior before you do a cast.

And again if it at runtime this cast is illegal, then you will get a runtime error. So, you better be careful that you are doing it correctly. But here notice that there is no problem about it is a static thing, because for these kind of basic variables, the type is known.

and there is no way to take a double and make it point to a float or make it point to an int. Every point a double is a double and int is an int. So this is really a static type checking thing that Java can do. It can check that this conversion is legal and that it matches the left hand side and then once that is done it will do whatever is the appropriate type conversion of the value and store it.

So, to summarize what we have seen is that not only can a subclass extend a parent class by adding new instance variables and new methods, it can also override existing methods. So, in this case, we saw that the manager class can override the bonus function of the employee class. Then we have this interesting situation that because of this compatibility, you can have a variable of the parent class pointing to an object of the child class.

So, the child has more capabilities. So, now if I call this overridden function in the context of the parent class, if I have an employee which is pointing to a manager object, if I ask for the bonus, then the question is which bonus do I get and dynamic dispatch will actually use the runtime value. So, in this case, we saw that if the current employee e is pointing to a manager, then you will get the manager bonus.

So, this is called runtime or inheritance polymorphism. So, this is different from overloading. So, overloading is based on signatures. So, this is a static thing, it says that you can write the same function name with different signatures different arguments, we saw it for constructors, we saw in the arrays, the sort function which takes different types of basic type arrays, double arrays and int arrays and so on. So, based on the static type of the argument, it will choose the correct definition among all of these which has the same name.

So, as the person using the function, it looks like you have one magic sort function which will sort arrays of any type, but actually you do not have one sort function, you have A separate sort function defined for every type and based on the static type of the argument, Java is picking the right one. So, overloading is a static choice of functions among different signatures, whereas dynamic dispatch is really based on the runtime type of the object. So, there is another type of polymorphism which we shall see, which is very useful. So, imagine that let us get back to the sorting.

So, what is it that is important to sort an array? So, essentially the sorting of an array requires us to put it in some order either ascending order or descending order. So, what is really important when we sort an array is not the values themselves, but how to compare these values. I need to know whether the value at position i in an array is smaller than, bigger than or equal to position j. So, so long as I can compare, if I have a comparison function between objects in an array, it does not really matter whether these objects are integers or strings or you know more, I could have a way of comparing dates for example.

A date can be compared in terms of the day, month and year. Or I could even compare lists in terms of their length. So there could be any different, many different ways of calling an array of objects so long as we have this one common feature that I can compare. So this is something called structural polymorphism. Now I can use a single sort function and this sort function will ask that its argument, have a certain capability, namely that capability of being compared.

And so long as the argument has the capability of being compared, the same sort function will work without having to worry about what the exact object is. So, this is called sometimes called structural polymorphism. And this is quite common in say, for example, functional programming, Java also has this and it uses the term generics. So, we will see this later.

So, there is a third way of doing polymorphism. So, there is one way of doing polymorphism, which is just the kind of trick which is overloading. There is another way which is based on dynamic dispatch, which is based on the class hierarchy.

And the third way is to use actually some property of the type and say that if the type has this property, if it can be compared, if you can do something else, then my function will work and this is something we will see later. And the last thing we saw is that you can use typecasting and some introspection, you can look at the type. to determine whether typecasting will work or not and use this to overcome some static type restriction.

So, for instance, if you want to set an instance variable of a more specific type from a less specific variable name, you can do that by using casting.