Section: Inheritance Heck
“Inheritance Heck” (previously named slightly differently) refers to seemingly complex and contradictory behavior by the compiler and runtime system when resolving types and methods in Java programs. However, through a proper understanding of how the compiler does type checking and how the runtime system works, it is possible to be able to correctly predict the behavior of even the most complex examples. First, some rules:
Compile Time Rules:
You can assign up the inheritance hierarchy
Foo b = newBar();
Object o = new String("abc");
but not down the inheritance hierarchy
Goo g = new Bar(); // Type mismatch: cannot convert from Bar to Goo
String str = newObject(); // Type mismatch: cannot convert from Object to String
Casting up or casting down is okay
((Object) anInt).toString();
((Integer) obj).toString();
((Foo) c).one();
((Bar) b).one();
However, casting sideways is notokay Object
Integer Double
((Integer) aDouble).toString(); // Cannot cast from Double to Integer
((Double) anInt).toString(); // Cannot cast from Integer to Double
Runtime Rule
Casting below object's runtime type is not okay. Foo
Bar Baz
Goo
Foo f = new Bar();
((Goo)f).one(); // Bar cannot be cast to Goo (it does compile)
Recommended Process
Here is a recommended process that should help derive the correct answers.
1) First consider how the compiler will react
- If, based on the compile-time type of the variable, can we cast that type to that type (if there is a cast involved)?
- And, once we do that cast, does that class type have a .method() method, either directly, or inherited from one of its superclasses, found by traversing up the inheritance tree?
If yes to both, the code compiles. If either fails write CE for CompileTime Error
2) If the message compiles consider the following that could happen at runtime
- Based on the run-time type, which is the type of the actual object rather than the type into which it is stored, can that object be cast to the variable's class type?
If yes, some message will be sent (code will run). Otherwise write RE for Runtime Error. The only time the answer would be "no" at runtime would be if you try to cast too far down the tree.
3) If there is no CE or RE, determine the output (answers can be selected from the table):
The Java runtime starts with the run-time type. It finds which version of the method it should use by traversing back up the inheritance tree. It will always find the method, eventually.
For static methods, the Java runtime starts with the compile-time type. Thus, the specific static method is figured out entirely by the compiler (there is no dynamic binding needed). Examples: Math.sqrt(double) or Integer.parseInt(String)
Here is an example hierarchy with many example problems. Here each method simply prints out its name.
Object+toString()
Class1
+ method 1( ) / "C1 M1"
+ method 2( ) / "C1 M2"
+ staticmethod( ) / "C1 Static"
Class2
+method2( ) / "C2 M2"
+method1()
+method3( ) / Super.method2()
+"C2 M3"
+toString( ) / "C2 tS"
+staticmethod( ) / "C2 Static"
Class4
+method2( ) / "C4 M2"
+method3( ) / method1()+"C4 M3"
Class3
+method1( ) / "C3 M1"+method3( ) / "C3 M3"
public class InheritanceHeckExample {
public static void main(String[] args){
Class1 var1 = new Class2();
Class1 var2 = new Class4();
Class2 var3 = new Class3();
Object var4 = new Class2();
/*1*/ var1.method1();// C1 M1
Answer: First, we determine var1 is of type Class1 (its compile-time type) and Class1 has a method1(), so it compiles. Then it executes at runtime (no casting errors). It looks at the run-time type (Class2) and traverses up the tree looking for the first class with the method and calls the first one it finds. In this case, it looks at Class2 and there is no method1() defined in Class2, so it traverses up the tree (from Class2 to Class1) and looks at Class1 to see if it has a method1() which it does, so it executes that method.
Recall the class hierarchy: Class1
Class2Class4
Class3
/*2*/ var2.method1(); // C1 M1
/*3*/ var3.method1(); // C3 M1
/*4*/ var4.method1(); // COMPILER ERROR
Answer: First, we determine if var4 is of type Object (the compiler treats it as such), and since that class does not have a method1() it does not compile, and is considered a COMPILER ERROR.
/*5*/ var1.method2(); // C2 M2 / C1 M1
/*6*/ var2.method2(); // C4 M2
/*7*/ var4.method2(); // COMPILER ERROR
/*8*/ var1.method3(); // COMPILER ERROR
/*9*/ var2.method3(); // COMPILER ERROR
/*10*/ var3.method3(); // C3 M3
/*11*/ var4.method3(); // COMPILER ERROR
/*12*/ System.out.println(var1); // C2 tS
/*13*/ System.out.println(var2); // Class4@20c10f (or another hex#)
First looks at Class4 to see if there is a toString( ) method. Then, looks at Class1 to see if it has a toString( ). It finds that it does not and looks in the Object class finds that it has one and executes the method.
Object
Class1
Class2Class4
Class3
/*14*/ (Class2)var1.method3();// COMPILER ERROR
Answer: First, we determine var1 is of type Class1 which does not have a method3() so it does not compile. The cast is not explicit, so the compiler still treats var1 as a Class1 object.
/*15*/ ((Class2)var2).method2(); // RUNTIME ERROR
Answer: First, we determine the compile-time type of var2 (Class1) and then determine that this cast to this type to Class2 is legal. Class2 has a method2( ), so it compiles. Then considering the run-time type of var2 (Class4), can we cast to Class2? Since we cannot cast a Class4 to a Class2, this is a RUNTIME ERROR and the method is not called on the object.
/*16*/ ((Class4)var2).method2(); // C4 M2
Answer: First, we determine the compile-time type of the expression to the left of the period: Class4. Class4 has a method2( ), so it compiles. Then at runtime, there are no casting errors because a Class4 run-time type can be cast to Class4. It then starts from the run-time type, traversing up the tree looking for the first class with the method and calls the first one it finds. In this case, it looks at Class4 and there is a method2( ) defined there.
Class1
Class2 Class4
Class3
/*17*/ ((Class3)var4).method1(); // RUNTIME ERROR
Answer: var4 has a run-time type of Class2. So this cast requests a cast downward, which is not allowed.
/*18*/ ((Class3)var3).method2(); // C2 M2 / C3 M1
/*19*/ ((Class2)var1).method3(); // C1 M2 / C2 M3
/*20*/ System.out.println((Class2)var1);// C2 tS
Answer: This one compiles because it looks at var1, determines it is a legal cast and treats the argument to println as a Class2 object. Since Class2 has a toString() method, it compiles. At runtime, the expression has a run-time type of Class2, so it uses its method.
/*21*/ System.out.println((Class4)var1);// RUNTIME ERROR
Answer: This one fails at runtime because it looks at var1, determines it is a legal cast (the compiler treats var1 as a Class1 object). The cast treats the argument as a Class4 object. Since Class4 inherits a toString( ) method (all objects do), this line compiles. At runtime, it then determines that var1 has a run-time type of Class2, which cannot be cast to Class4 (this is called a cross cast, and is disallowed in Java).
/*22*/ var1.staticmethod(); // C1 Static
Answer: Static methods (and fields) are treated quite differently in Java. The compiler figures out everything at compile time. Here, var1 has a compile-time type of Class1, so its static method is used. Note that the run-time type of var1 (Class2) is not important, nor is it relevant that Class2 has its own staticmethod().
/*23*/ var2.staticmethod(); // C1 Static
Answer: var3 has a compiler type of Class1, so the same staticmethod() is used here.
/*24*/ var3.staticmethod(); // C2 Static
Answer: var3 has a compile-time type of Class2, which has its own staticmethod().
/*25*/ var4.staticmethod(); // COMPILER ERROR
Answer: var4 has a compile-time type of Object, which has no staticmethod(). (The fact that its run-time type is Class2, which does have a staticmethod() is not relevant.)
/*26*/ ((Class2)var1).staticmethod(); // C2 Static
/*27*/ ((Class4)var1).staticmethod(); //RUNTIME ERROR
Answer: var1 has a compile-time type of Class1, which can sometimes be cast to Class4, which has a staticmethod(), so everything is fine at compile-time. In fact, the compiler notes that Class4’s staticmethod() is to be used. At runtime, var1 has a run-time type of Class2, which cannot be cast to Class4 (as it would be a cross-cast). So the problem is with the cast, not the call to the static method.
/*28*/ ((Object)var1).staticmethod(); // COMPILER ERROR
Answer: You tell me! var1 has a type of Class2, which has a staticmethod(). So why does this generate a compiler error. (Hint: run-time or compile-time type? Does that even matter?)