CSE143 Notes for Monday, 10/30/23

I discussed the concept of inheritance and mentioned that we're going to have a midterm question about this. I started by showing a non-programming example of a hierarchy of different kinds of employees:

                     Employee
                   /         \
                 /             \
            Clerical        Professional
               |             /       \
               |            /         \
           Secretary    Lawyers    Engineers
               |
               |
        Legal Secretary
Everyone in the company is an employee, but we divide them into clerical versus professional employees. Among the professionals, we have lawyers and engineers. Among the clericals we have secretaries and a variation of a secretary known as a legal secretary. The idea is that this is part of a company's hierarchy of employees. Obviously an actual company would have many other kinds of employees as well.

I mentioned that most companies have an employee orientation that all employees attend. Suppose that at that orientation there is a 20-page booklet given out that describes general employee procedures (insurance, retirement, vacations, etc). By default, an employee would assume that those are the policies that apply to that employee. But what tends to happen is that the day after the employee orientation they're told some more details. For example, the lawyers might have their own 3-page booklet. That booklet can have two kinds of things. It might have additional procedures that aren't part of the 20-page booklet. And it might have replacement procedures (what we call "overriding"). For example, they might tell a new lawyer, "I'm sure they told you yesterday at the orientation that when you want to take a vacation to fill out the yellow form. We don't use the yellow form here. We have our own form that's pink."

This is similar to what happens in Java with inheritance. You establish an inheritance relationship between two classes with the "extends" keyword in the class header:

        public class TypeA {
            ...
        }

        public class TypeB extends TypeA {
            ...
        }
In our hierarchy diagram, we'd put TypeB below TypeA because it extends it:

        TypeA
          |
        TypeB
We refer to TypeB as a "subclass" of TypeA and TypeA as the "superclass" of TypeB. This use of sub and super is somewhat unfortunate because it has the opposite meaning to how we use the words in English. With these inheritance hierarchies, we put the more specialized version below (as the "sub") and the more generic one above (as the "super"). For example, if we had a hierarchy for burgers and cheeseburgers, we'd put the burger on top because it's the more generic one:

           burger
             |
        cheeseburger
But think about this. Someone says, "You can have a cheeseburger or you can have a super cheeseburger." I really like cheese, so I'd tend to ask for a super cheeseburger. So imagine my surprise when I find that the super cheeseburger has no cheese! Yet in the standard terminology we'd refer to "burger" as the superclass of cheeseburger.

Another pair of words that people sometimes use are to refer to the superclass as the "base" class and the subclass as the "derived" class. This is a little clearer in terms of the superclass being simpler (base). In fact, the C# programming language has a keyword "base" that has the same meaning as a Java keyword "super".

When you don't include an "extends" clause in a class header, the default is that the class extends a generic class known as Object. So in the hierarchy above, to be complete, we really should show that TypeA exends Object:

        Object
          |
        TypeA
          |
        TypeB
The Object class is something like the Employee or Vehicle terms that appear at the top of the nonprogramming hiearchies we looked at. Every class that you define extends Object one way or another, either directly, as in the A class, or indirectly, as in the TypeB class.

Going back to our employee analogy, we talked about the idea of a 20-page manual that would apply to all employees. In an inheritance hierarchy, any state and behavior (i.e., any data fields and methods) in the superclass are automatically included in every subclass. In other words, saying "extends Foo" automatically gives a class all of the data fields and methods of the Foo class.

A subclass can do two things:

In our analogy, this is like the 3-page booklet that the lawyers have that defines new procedures that apply only to lawyers and that overrides inherited behavior ("We use our own pink form instead of the standard yellow form").

I mentioned two last points before we left the "employee" analogy. First I wanted to talk about when a substitute is appropriate. For example, you might imagine a hierarchy for vehicles that included bikes and cars. Among the cars you might have Hondas. Among the Hondas you might have Honda Accords. And among the Accords you might have a luxury version called an LX. And among the Accord LX models, you might have a variation that is known as "luxury package 319". We'd draw a hierarchy something like this to capture these variations:

                   Vehicle
                  /       \
               Car         Bike
                |
              Honda
                |
              Accord
                |
            Accord LX
                |
      Accord LX package 319
The point is that the most generic or simple description appears high in the hierarchy. The more complex, more sophisticated objects appear low in the hierarchy. That's because at each level we are adding potentially more and more state and behavior.

I used this vehicle hierarchy to talk about the notion of "substitutability". The question is, when can one object substitute for another? Inheritance should be used only when there is an "is-a" relationship where the more specialized object can substitute for the less specialized one. So in this hierarchy, the Accord LX luxury package 319 can take the place of anything above it but the things above it can't take its place. This matches our intuition in most cases. If we were expecting a generic Accord and we instead got a luxury Accord, we're not going to complain. But if we paid for the luxury car and instead got the generic car, then we wouldn't be happy.

We also can't substitute across. If we were expecting a luxury Accord, we aren't going to be happy with a bike. Of course, the analogy isn't perfect. In real life if we were expecting a bike, we might be satisfied with a luxury Accord instead, even though with inheritance hierarchies, that wouldn't be allowed.

I said that in programming we think of each of these different entries as "roles". The idea is that an object can fill many roles. An Accord LX luxury package 319 can fill the role of an Accord LX luxury package 319 because it is one. But it can also fill the role of an ordinary Accord LX and it can also fill the role of a generic Accord and it can also fill the role of a Honda and it can fill the role of a car and it can fill the role of vehicle. In general, an object call fill every role that appears as you go up the inheritance chain to the top.

In terms of our simple inheritance hierarchy with class B that extends class A, think about the following situations. The simple cases are where we have variables and objects of the same type. Obviously we can say:

        TypeA x = new TypeA();
        TypeB y = new TypeB();
But what happens if the variable type and the object type don't match?

        TypeA x = new TypeB();
        TypeB y = new TypeA();
One of these is legal and one is not and it comes from the inheritance relationship. Remember that TypeB extends TypeA:

        TypeA
          |
        TypeB
That means that TypeA is the simpler, more generic object and TypeB is the more sophisticated, more complex object. In particular, a TypeB object can substitute for a TypeA object. In other words, a TypeB object can fill the role of a TypeB object or the role of a TypeA object. But the opposite is not true. A TypeA object cannot fill the role of a TypeB object. This is like our generic Accord versus our luxury Accord. The luxury Accord is a reasonable substitute for the generic, but not the other way around. So given our two lines of code, the first is okay but the second is illegal:

        TypeA x = new TypeB();  // okay, TypeB can fill TypeA role
        TypeB y = new TypeA();  // not okay, TypeA cannot fill TypeB role
Sometimes inheritance is described as an "isA" relationship, as in, "A legal secretary is a secretary, a secretary is a clerical worker, a clerical worker is an employee." We would expect that the "isA" relationships extend all the way up the hierarchy.

Another way to think about inheritance is to think of it as, "can substitute for." This idea was championed by a computer scientist named Barbara Liskov who teaches at MIT and we refer to it as the Liskov Substitution Principle.. Many of the potential problems you have to reason about arise because of this idea of a possible substitution.

Getting back to our simple example, we know that we can say:

        TypeA x = new TypeB();
In other words, a TypeB object can substitute when we were expecting a TypeA object. But what about the variable x? It is defined in terms of the TypeA class. As a result, Java expects that we will only request "TypeA type" behaviors when we use this variable. If there is a particular method that TypeB objects have that isn't include in the TypeA class, then we can't make a call like this:

        x.bOnlyMethod();
Even though the actual object has that behavior, if it is not included in the TypeA class, then we can't use the variable x to call the method. It is useful to think of the variable x as having a "contract" with Java that it will be used for TypeA-like behavior only.

There is a way to get around this, but it requires the use of a cast. We can in effect renegotiate our contract with Java and let it know that we expect the actual object to be of a different type than what the variable would indicate:

        ((TypeB)x).bOnlyMethod();
This line of code tells Java that you are substituting TypeB-like behavior in place of the original contract. If that includes the given method, then this line of code looks okay. Keep in mind that the cast does not affect anything other than this one specific method call. The variable isn't somehow changed. For example, if you wanted to make five such calls, you would have to cast each of the five times or introduce a new variable that stores the result of casting.

There are two stages of error checking that Java performs. First the compiler sees whether the method you are calling is included as part of the contract for the variable you are working with. If there is no cast, the compiler looks at the type of the variable. If there is a cast, it looks at the type you are casting to.

If you pass the compiler error checking, there is still a potential error when there is a cast. You are claiming that the object will actually be of a different type. Java is willing to let you get past the compiler, but it will check to see if you told the truth when the program is actually run. If the actual object turns out not to be something that can fill the role you have described, then you get a different kind of error known as a runtime error.

I gave a nonprogramming example to understand this idea of contracts and renegotiating. I said suppose that you are running a temp agency and you charge people $20 per hour for a secretary and $30 an hour for a legal secretary. One day a customer asks you to send over a secretary and you find that you have no generic secretaries to send over, but you have a legal secretary that otherwise wouldn't be working that day. So you decide to send the legal secretary even though the request was for a generic secretary. This works because of the notion of substituting. A legal secretary can substitute for a secretary. But suppose that during a coffee break the employer figures out that the person you sent over is actually a legal secretary and the employer says, "Great, I have some legal secretarial work I want you to do."

I asked if this is okay and people said no. Why not? That customer asked for a secretary and is paying $20 an hour. I happened to send over someone who can do more, but that doesn't mean that customer has the right to change the contract and ask the person to do more than the contract is for. If that customer wants the person to do legal secretary work, then we need to renegotiate the contract and that employer needs to pay $30 an hour for that work.

I said that this is exactly what is happening in Java when you use a class cast. You are renegotiating the contract for what you can ask that object to do. I said we'd see examples of this in the sample problems we were about to do.

I then showed people how these ideas apply to actual code by looking at handout #11. By looking at the class headers and the "extends" clauses, we were able to figure out that the inheritance hierarchy looks like this:

            One
           /   \
        Two    Three
                 |
                Four
I then suggested that we make a table that keeps track of what definition (if any) each class has for method1, method2 and method3. Starting with the class One, we find that it defines method1 as producing the output "One1". It has no definition for method2 and method3. That means that the "One" role does not include a method2 or method3. This will be important later in solving this problem. So our table starts out like this:

                 method1          method2          method3

        One       One1              ---              ---
The Two class provides a definition for method3 that prints out "Two3". It has no other definitions, but it inherits a method1 from One that prints "One1". It has no method2. So now the table looks like this:

                 method1          method2          method3

        One       One1              ---              ---

        Two       One1              ---             Two3
The Three class provides a definition for method2 that prints "Three2" and then calls method1. There is no definition for method1 in this class, but it inherits one from the One class that prints "One1". Since method2 prints "Three2" and then calls method1, you might be inclined to say that its output is two lines: Three2/One1. But that won't always be the case because of what's known as "polymorphism". Java is a dynamic language where methods can be redefined. So for a Three object, method2 prints those two lines of output. But it won't necessarily behave that way for all objects because method1 might be redefined. The Three class had no definition for method3. So now our table looks like this:

                 method1          method2          method3

        One       One1              ---              ---

        Two       One1              ---             Two3

        Three     One1            Three2             ---
                                  method1()
Finally, the Four class defines a method1 and a method3. In method1 of the Four class we see something new, a use of the keyword "super". In this context, super is being used to call an overridden method. This class is giving a new definition to method1, but in doing so, it can call the original version of the method in the superclass by using the keyword "super". You can think of the keyword "super as an alternative to "this". If you say "super.method1()" you are asking for the version of method1 in the superclass. If you say "this.method1()" or just "method1", you're asking for the version of method1 in this class.

We spent some time talking about the details of super. I mentioned that it is statically bound in that you know exactly what method is being called. A minute ago we were careful not to make assumptions about which method1 would be called because that involved a call on "this.method1" where polymorphism enters into things. Here we know exactly what method is being called, the version of method1 in the superclass of the four class. Actually, there is no definition of method1 in the superclass of Four (class Three), so to find it, we keep looking up the inheritance chain until we find the definition in the class One.

So we know that method1 prints out "Four1/One1". Method2 is the inherited method that prints out "Three2" and then calls method1. And method3 prints out "Four3". So the Four class is the only class that has all three methods defined. So our final version of the table looks like this:

                 method1          method2          method3

        One       One1              ---              ---

        Two       One1              ---             Two3

        Three     One1            Three2             ---
                                  method1()

        Four      Four1           Three2            Four3
                  One1            method1()
There are several things to notice about this table. First of all, think in terms of the four roles: One, Two, Three and Four. The One role defines just a method1; the Two role has a method1 and method3; the Three role has a method1 and method2 and the Four role has all three of method1, method2 and method3. Also notice what a call on method2 will produce for a Three object versus a Four object. Remember that method2 includes a call on method1 and this is determined polymorphically. For a Three object, method1 produces the output "One1", so a call on its method2 would produce two lines of output (Three2/One1). For a Four object, method1 produces two lines of output, so a call on its method2 would produce three lines of output (Three2/Four1/One1). The same method call can end up calling different methods. That's what polymorphism is all about (poly for "many" and morphism for "forms", so a single method call can take "many forms"). We then looked at the rest of the problem. It involves several variables that are defined and a series of calls using those variables. I mentioned that there is a three step process to go through to figure out these calls:

  1. First see if you pass the compiler. If there is no casting involved, the compiler looks at the type of the variable. If there is a cast, then the compiler uses that type instead. The compiler makes sure that the type involved (the role) includes the method you are calling. If not, you get a compiler error.

  2. Next see if there is a possible runtime error. This happens only when a cast is involved. When you cast, you are saying to Java, "Trust me. The object will be of this other type." Java isn't a trusting language. By casting, you can delay the check, but the runtime system will make sure that your claim is true (that the object in question can fill the role that you are casting it into). If the cast is inappropriate for the actual object involved, then you get a runtime error (a ClassCastException). This is like my example of renegotiating the contract from a secretary to a legal secretary.

  3. If you pass both of those tests, then you simply execute the method in question. Here you pay attention to what kind of object you have. The variable type and the cast don't matter at all. All that matters is what kind of object you have. Objects always behave in the same way no matter what the variable type is and no matter what kind of casting you've done.
Then we worked through specific problems. I pointed out that the first six problems involved calling method1 on each of the six variables without any casting going on. The first question you have to consider is whether you pass the compiler check. To figure that out, you have to look at the types of the variables. The types of the objects don't matter to the compiler. The variables determine the contract. The variables of type One and Three are okay because both the One class and the Three class include a method1. But the two variables of type Object are a problem because the Object class does not include a method1. Even though the objects themselves can do this, the contract was for a generic Object, so the compiler is going to complain. This is an exact parallel of our customer who asked for a secretary and got a legal secretary. Even though the legal secretary can do more sophisticated work, the customer isn't allowed to ask for that because the contract is for a $20/hour generic secretary, not for a legal secretary.

So the fifth and six answers are "compiler error". The first four pass the compiler and have no casting, so we don't have to worry about runtime errors. The only thing left is to figure out what the individual objects do when method1 is called.

We then looked at the first casting example:

        ((Two)var1).method2();
The variable var1 is declared to be of type "One", so in the absence of a cast, we'd be looking at the One role to figure out this contract. But there is a cast, so we use that instead. We are casting to Two, which means we have renegotiated the contract. So the question becomes, does the Two role include a method2? The answer is no. So even with this cast, we get a compiler error (the role we have contracted for does not include this method).

But what about the next one:

        ((Three)var1).method2();
Here we are using a cast to Three to renegotiate the contract. So the question becomes, does the Three role include a method2? The answer is yes. So we pass the compiler (step 1). Then we ask whether the cast is actually legal (step 2). What kind of object do we have? The variable var1 is referring to a Two object. Can a Two object be cast to a Three? In other words, can a Two substitute for a Three? The answer is no. This would be like giving someone a bicycle when they were expecting an Accord (it's a cast across, which is illegal). So even though we pass the compiler, we don't pass the runtime system. So this generates a runtime error.

As a final example, we considered this problem:

        ((Four)var5).method2();
We have a cast, so we look at the Four role to determine whether this is legal as far as the compiler is concerned (step 1). The answer is yes. The Four role includes a method2. So we pass the compiler. But what about the second step? We have to consider whether the cast is legal. The actual object is a Three object. Can a Three object substitute for a Four object? The answer is no. A Four object can substitute for a Three, but not the other way around. So this generates a runtime error because of the illegal cast.

The odd thing about this example is that the actual object would be able to execute method2. If we had instead cast to Three (which is the next problem below this one), it would work:

        ((Three)var5).method2();
So in a sense we have "overcast" in the first case. We didn't need to claim that it will be a Four. We only needed to claim that it was a Three. But if you overclaim, Java will call you on it. It will make sure that all of your casts actually work out.

Below is a list of all of the problems from the section handout with a brief description of each answer.

Answers to handout #16
Call Output Discussion
var1.method1(); One1 variable is of type One, One role includes method1, no cast, actual object is a Two which writes out "One1" when method1 is called
var2.method1(); One1 variable is of type One, One role includes method1, no cast, actual object is a Three which writes out "One1" when method1 is called
var3.method1(); Four1/One1 variable is of type One, One role includes method1, no cast, actual object is a Four which writes out "Four1/One1" when method1 is called
var4.method1(); Four1/One1 variable is of type Three, Three role includes method1, no cast, actual object is a Four which writes out "Four1/One1" when method is called
var5.method1(); compiler error variable is of type Object, Object role does not include method1
var6.method1(); compiler error variable is of type Object, Object role does not include method1
var4.method2(); Three2/Four1/One1 variable is of type Three, Three role includes method2, no cast, actual object is a Four which writes out "Three2/Four1/One1" when method2 is called (note that method2 calls its method1 polymorphically, which is why this output includes "Four1")
var4.method3(); compiler error variable is of type Three, Three role does not include method3 (even though the object itself is a Four that is capable of performing this action)
((Two)var1).method2(); compiler error because of cast we pay attention to it rather than the variable type (because we have renegotiated the contract), cast is to Two, Two role does not include method2
((Three)var1).method2(); runtime error cast is to Three, Three role includes method2 so we pass the compiler, but actual object is a Two which can't fill the Three role (casting across the hierarchy, like asking someone to accept a bike when they were expecting a car), so we get a runtime error
((Two)var1).method3(); Two3 cast is to Two, Two role includes method3, actual object is a Two which cal fill the Two role, so the cast is okay, and a Two object writes "Two3" when its method3 is called
((Four)var2).method1(); runtime error cast is to Four, Four role includes method1, actual object is a Three which can't fill the Four role; this was, in essence, a stupid cast to do because it isn't necessary, but if you tell this kind of lie, Java will complain
((Four)var3).method1(); Four1/One1 cast is to Four, Four role includes method1, actual object is a Four which can fill the Four role, so cast is okay and a Four object writes "Four1/One1" when method1 is called
((Four)var4).method3(); Four3 cast is to Four, Four role includes method3, actual Object is a Four, which can fill the Four role, so cast is okay and a Four object writes "Four3" when method3 is called
((One)var5).method1(); One1 cast is to One, One role includes method1, actual object is a Three, which can fill the One role, so cast is okay and a Three object writes "One1" when method1 is called
((Four)var5).method2(); runtime error cast is to Four, Four role includes method2, actual object is a Three which can't fill the Four role
((Three)var5).method2(); Three2/One1 cast is to Three, Three role includes method2, actual object is a Three which can fill the Three role and a Three object writes "Three2/One1" when method2 is called
((One)var6).method1(); One1 cast is to One, One role includes method1, actual object is a One which can fill the One role, so cast is okay and a One object writes "One1" when method1 is called
((One)var6).method2(); compiler error cast is to One, One role does not include method2
((Two)var6).method3(); runtime error cast is to Two, Two role includes method3, actual object is a One, which can't fill the Two role


Stuart Reges
Last modified: Mon Oct 30 12:43:29 PDT 2023