Discussion 5: OOP and Subtypes
Solutions
Exercise 1:
Defining a Class
(a)
One thing that we may wish to insure is a house, which we'll model with a
House class. Houses have an address, contain a certain number of bedrooms and bathrooms, and a current market value. Fill in the body of the constructor to initialize these fields with the corresponding parameters.
House.java
Note that we needed to use
this on lines 14 and 17 since these fields are shadowed by the parameters with the same name.
(b)
Draw a memory diagram that depicts the state of the
f() call frame after the initialization of home in the following code snippet.
(c)
To add
Houses to insurance policies, we'll have the House class implement Insurable. To do this, we'll need to add definitions for the three Insurable methods. Suppose that isInsured() and policyExpiration() were taken care of (including adding any additional fields to support these behaviors). Add a definition of premium() to the House class. A house's insurance premium is equal to its current value times some multiplier that the insurance company determines from the house's address (factoring in things such as climate risk, traffic, neighborhood characteristics, etc.). Assume that this multiplier can be obtained by calling a static method rateMultiplier() which accepts the address String as its only argument and returns a double.
House.java
Exercise 2:
Subtype Semantics
(a)
Implement the following
static method totalPolicyCost() to compute the total cost of an insurance policy, which we model as an array of Insurables.
(b)
Why are we allowed to have variables of type
Insurable and Insurable[] even though the interface type Insurable cannot be instantiated? What benefits can this offer in our code?
Every class and/or interface that we write defines a new type that our Java programs can use. In particular, this means that we can declare variables and arrays storing an interface type. Such variables can hold a reference to any subtype object that implements that interface. This enables polymorphism in our code, since the same variable can reference different types of objects, and the interface declares methods that are guaranteed to exist for all of these types.
Consider the following method:
(c)
For each of the following reference type expressions, determine its static type, as well as the dynamic type of the object that it references.
home:
home has static type Insurable and dynamic type House. home, the variable on the left-hand side of the assignment statement, is declared to be Insurable.
car:
car has static type Car and dynamic type Car. car, the variable on the left-hand side of the assignment statement, is declared to be Car.
policy[1]:
policy[1] has static type Insurable and dynamic type Car. policy, the variable on the left hand side of the assignment statement, is declared to be Insurable[], so the static type of every element in that array must be Insurable.
(d)
Explain why we are allowed to assign (an alias reference to the same object as)
car to an entry of policy, even though their static types are different.
At runtime,
car references an object with (dynamic) type Car. Car is a subtype of Insurable because the Car class implements the Insurable interface. The subtype substitution rules tell us that we can use a subtype reference anywhere where we were anticipating a reference to its supertype.
(e)
Which method body would we enter if we execute
policy[0].premium()? Which method body would we enter if we execute policy[1].premium()? How does Java make this determination?
Executing
policy[0].premium() runs House.premium(), the version of premium() implemented by House. Executing policy[1].premium() runs Car.premium(), the version of premium() implemented by Car. Java makes this determination using dynamic dispatch, the principle that at runtime, an object’s dynamic type is used to determine what actual behavior it performs.
(f)
What will happen if we try to execute
policy[1].mileage() within the simulate() method? Explain your answer.
This is a compilation error. This is due to the compile-time reference rule, where at compile time, all types, behaviors, and usages of all variables must be declared for their static type. Even though, in this instance,
mileage() should be valid to call at runtime, in general, not every Insurable object has a mileage() method, and that method is being called on an array that is statically-typed to contain Insurables, so Java’s compiler throws an error here.
(g)
Draw a memory diagram that depicts the state of the program immediately after entering a
premium() method for the first time. The bottom call frame in your diagram should correspond to the simulate() method.
Note: Your answer here (particularly the totalPolicyCost() call frame) may vary slightly, depending on your answer to Exercise 2(a).
Exercise 3:
More Complicated Type Hierarchies
(a)
Draw the hierarchy diagram for the types
House, Car, Boat, Insurable, and Vehicle.
Now, suppose that we have (correctly) initialized the following variables:
(b)
(1) This code will fail to compile. Java cannot guarantee that the
Insurable object referenced by i is able to be assigned to the House variable h since Insurable is not a subtype of House. (In other words, implicit down-casting does not work.)
(c)
(2) This code will compile. The compiler knows that it is possible for an
Insurable variable to reference a House object (since House is a subtype of Insurable), so it allows this cast. (In other words, explicit down-casting is allowed.) The cast may fail at runtime if i does not reference a House (or a subtype of House).
(d)
(4) This code will always compile and run successfully.
Car is a subtype of Insurable, so we can always assign a Car reference to an Insurable variable; this is the assignment subtype substitution rule. (In other words, upcasting, either implicit or explicit, always works.)
(e)
(4) This code will always compile and run successfully, by the same reasoning as part (d). The only difference is that the upcast is explicit instead of implicit.
(f)
(1) This code will fail to compile. Any object that is referenced by
c must be a subtype of Car, which therefore precludes it from being a subtype of House (by Java’s single inheritance rule). Therefore, the compiler can deduce that this cast cannot possibly succeed at runtime, so it throws an error and stops the compilation. (Contrast this with part (c), where there is a possibility of the cast succeeding.)
(g)
(3) This code will compile and always encounter an exception at runtime. The compiler sees this as a chained order of casts:
(Insurable) c is a valid cast because Car implements Insurable, and the (House) cast sees the argument to its right as an Insurable. Since each of these casts could possibly succeed, the compiler allows them. However, this line will always fail at runtime because you can’t cast a Car into a House.
(h)
(2) This code will compile and may or may not work at runtime. Both casts here can be valid (since an
Insurable could be a Car, and a Car definitely is a Vehicle), so the compiler will allow them. At runtime, this cast will succeed if i references a Car object, but it will fail (for example) if i references a House object.
(i)
(2) This code will compile and may or may not work at runtime.
i could reference a Car object, which can be up-cast to a Vehicle. This possibility means that the compiler can’t prove that this is an invalid cast, so it must allow this to compile. However, it will fail if i is not a Car or a subtype of one.
(j)
(3) This code will compile but always throw a runtime exception (with the given information). The first cast is the same as part (i). The second cast is a down-cast, which would succeed for a Vehicle expression referencing a Boat. In the type hierarchy from part (a), there is no class implementing Insurable that is a subclass of Boat, so this cast will always fail at runtime.
Note that we could add another class (e.g., class Yacht extends Boat implements Insurable) that would allow this cast to succeed at runtime.