Classes & OOP: Subtype Polymorphism

Roger PoonBy Roger Poon

JS++ Designer and Project Lead

Subtyping describes type relationships, and subtype polymorphism enables operations defined for supertypes to be safely substituted with subtypes. Concretely, imagine the relation between a 'Cat' class and an 'Animal' class. (Remember: classes create data types in JS++.) In this case, within the context of type relationships, 'Cat' is the subtype of 'Animal' and 'Animal' is the supertype of 'Cat'. In simpler terms, 'Cat' "is a" 'Animal', but 'Animal' is not a 'Cat'. In theory, this means that all operations that apply to the 'Animal' data type should accept operating on data of type 'Cat'; however, the reverse is not true: operations defined for the data type 'Cat' would not safely be able to operate on data of type 'Animal'.

If you remember the code from the previous section, cats and dogs are domesticated animals with names. However, not all animals should be named so our 'Animal' class did not take a name parameter. Thus, while we could have defined and called a 'name' getter on an instance of 'Cat', we could not safely substitute the 'Cat' instance with an 'Animal' instance. In JS++, if you even try to do this, you will get an error.

Subtype polymorphism allows us to write code in a more abstract manner. For example, within the context of primitive types, a 'byte' represents numbers in the range of 0 to 255. Meanwhile, an 'int' represents numbers within a much larger range: -2, 147, 483, 648 to 2, 147, 483, 647. Therefore, we can substitute numbers of type 'byte' where numbers of type 'int' are expected:

    int add(int a, int b) {
        return a + b;
    }

    byte a = 1;
    byte b = 1;
    add(a, b);
    

Thus, we are able to express algorithms and functions more "generally" because we can accept a wider variety of data (categorized by data types) for any given algorithm or function.

It's important not to confuse subtyping with inheritance even though the concepts are closely related within object-oriented programming. Subtyping describes type relationships, whereas inheritance is concerned with implementations (such as extending the implementation of a base class with a derived class). Subtyping can apply to interfaces while "inheritance" cannot. This concept will become clearer in later sections when we cover interfaces.

As a starting point to practically understand subtyping, we can change main.jspp so that the data type of all of our variables becomes 'Animal' but we keep the instantiation of classes as the subtypes:

    import Animals;

    Animal cat1 = new Cat("Kitty");
    cat1.render();
    Animal cat2 = new Cat("Kat");
    cat2.render();
    Animal dog = new Dog("Fido");
    dog.render();
    Animal panda = new Panda();
    panda.render();
    Animal rhino = new Rhino();
    rhino.render();
    

Compile your code. It should compile successfully because, during instantiation, all data that is a subtype of 'Animal' can be assigned to a variable of type 'Animal'. In this case, 'Cat', 'Dog', 'Panda', and 'Rhino' data can all be safely assigned to 'Animal' variables.

However, there's a small "gotcha." Open index.html. You'll see all the animals rendered again, but, if you hover your mouse over any of the animal icons, you won't see any names! To understand why this occurs, we must understand static versus dynamic polymorphism, which will be explained in the next section. (In short, we actually specified we wanted "static" or compile-time polymorphism by using the 'overwrite' keyword on the 'render' method. Thus, we got the specified behavior.)