In class, we will be discussing an extension of the Inheritance/Polymorphism topic that was introduced in CSE 142 (or should have been introduced in your CSE 142 equivalent course). My goal is you go into the next class being able to confidently solve the type of problem that would appear on a CSE 142 exam like this practice problem.

If you do not feel like you remember the material to solve this type of problem, you should read the textbook chapter 9.1 or review the lecture slides or watch the Panopto recording from the CSE 142 website from a previous quarter.

Below is a list of terminology you should be familiar with and my walkthrough of the above problem.

Terminology

Inheritance is a way to form an is-a relationship between two classes. If you have a class called A and you write a new class public class B extends A, you are saying that B is a sub-class of A and that A is a super-class of B. This relationship says that a B is an A, but is a more specialized version. For example you might have SoftwareEngineer extend Engineer since a software engineer is a specialized version of an engineer.

By default, a sub-class gets a copy of all the methods defined in the super-class (we say the sub-class inherits the super-classes methods). The sub-class is able to override the methods to provide it’s own behaviors.

A sub-class is able to access the methods of its super-class. If the sub-class wants to call exampleMethod on the super-class, it writes the code super.exampleMethod();.

A class can only extend up to one super-class. If a class does not extend the class, it extends the class Object by default.

Regular method calls (ones not involving super.method()) are polymorphic (also referred to as dynamically dispatched). This means that the method call is always called on the actual type of the instance of object it is being called on. This means that if methodA calls methodB, you always call methodB on the actual type of the object even if it was a method inherited from the super-class.

Example

Suppose we had the following class declarations

public class Denny extends John {
    public void method1() {
        System.out.print("denny 1 ");
    }
    public String toString() {
        return "denny " + super.toString();
    }
}
public class Cass {
    public void method1() {
        System.out.print("cass 1 ");
    }
    public void method2() {
        System.out.print("cass 2 ");
    }
    public String toString() {
        return "cass";
    }
}
public class Michelle extends John {
    public void method1() {
        System.out.print("michelle 1 ");
    }
}
public class John extends Cass {
    public void method2() {
        method1();
        System.out.print("john 2 ");
    }
    public String toString() {
        return "john";
    }
}

Given these classes, what is the output of the following code

Cass[] elements = {new Cass(), new Denny(), new John(), new Michelle()};
for (int i = 0; i < elements.length; i++) {
    elements[i].method1();
    System.out.println();

    elements[i].method2();
    System.out.println();

    System.out.println(elements[i]);

    System.out.println();
}

To solve this problem, we first draw out the inheritance tree

                     +-------------------------+
                     |                         |
                     |           Cass          |
                     |                         |
                     +------------+------------+
                                  ^
                                  |
                     +------------+------------+
                     |                         |
                     |           John          |
                     |                         |
              +----> +-------------------------+ <--+
              |                                     |
              |                                     |
              |                                     |
+-------------+--------------+       +--------------+--------------+
|                            |       |                             |
|           Denny            |       |           Michelle          |
|                            |       |                             |
+----------------------------+       +-----------------------------+

You may or may not have seen this in CSE 142, but we find that it’s helpful to write all the classes and methods into a table to be a reference for what the output will be. If a class does not define a method, it gets a copy of its super class’ method.

method1 method2 toString
Cass S.o.p("cass 1 "); S.o.p("cass 2 "); "cass"
John S.o.p("cass 1 "); method1(); S.o.p("john 2 "); "john"
Denny S.o.p("denny 1 "); method1(); S.o.p("john 2 "); "denny " + "john"
Michelle S.o.p("michelle 1 "); method1(); S.o.p("john 2 "); "john"

Notice that when method2 calls method1, we do not immediately write down the output in the table. This is because the method calls are polymorphic so we have to call method1 on the actual type of the object. This is not true for super calls though as they are not polymorphic, that is why for the super.toString() call, we just look above and copy the output from that table entry.

Building up the table is normally the hard part, now all we have to do is use the table to answer the questions for the output.

What is the output for the first element when i=0? First thing we have to ask is what is the type of the object stored at index 0. If we look back at the array, we see it’s a Cass. We first call method1 and all we have to do is look at the table to see what Cass‘s method1 does to get the output. This process is repeated for method2 and the call to toString when printing the object, to get the output

cass 1
cass 2
cass

What about for the next index? The type of the object is a Denny so when we call method1, we get

denny 1

When we call method2 the first thing we do is call method1. Who do we call method1 on? Whatever the type of the object we are working with is, so in this case call Denny‘s method1; this is why we didn’t write the output of the method call in the table, because we don’t know the actual type of the object until we actually make the method call. This means the output for the second call will be

denny 1 john 2

What about the toString call? We just use the output in the table since there are no dynamic method calls (recall super calls aren’t dynamic so we know right away what the output is) to get

denny john

Following the process for all the variables we would get the whole output

cass 1
cass 2
cass

denny 1
denny 1 john 2
denny john

cass 1
cass 1 john 2
john

michelle 1
michelle 1 john 2
john