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