Saturday, January 8, 2011

Chapter 9: Reference Variable Casting

In the previous chapter we took a detailed look at polymorphism, overloading and overriding. While going through contents about Inheritance & overloading you would have seen that objects of one type were being considered or used as another. Java has a feature called Casting, wherein an object of one type can be cast into an object of another type (As long as its valid)

For example, this line of code should be second nature by now:

Car Car = new Ferrari();

But what happens when you want to use that Car reference variable to invoke a method that only class Ferrari has? You know it’s referring to a Ferrari, and you want to do a Ferrari-specific thing? In the following code, we’ve got an array of Cars, and whenever we find a Ferrari in the array, we want to do a special Ferrari thing. Let’s agree for now that all of this code is OK, except that we’re not sure about the line of code that invokes the brake method.

class Car {
void drive() {System.out.println("generic noise"); }
}
class Ferrari extends Car {
void drive() {System.out.println("drive"); }
void brake() { System.out.println("brake hard"); }
}

class CastTest2 {
public static void main(String [] args) {
Car [] a = {new Car(), new Ferrari(), new Car() };
for(Car Car : a) {
Car.drive();
if(Car instanceof Ferrari) {
Car.brake(); // try to do a Ferrari behavior ?
}
}
}
}

When we try to compile this code, the compiler says something like this:

cannot find symbol

The compiler is saying, “Hey, class Car doesn’t have a brake() method”. Let’s modify the if code block:
if(Car instanceof Ferrari) {
Ferrari d = (Ferrari) Car; // casting the ref. var.
d.brake();
}

The new and improved code block contains a cast, which in this case is sometimes called a downcast, because we’re casting down the inheritance tree to a more specific class. Now, the compiler is happy. Before we try to invoke brake, we cast the Car variable to type Ferrari. What we’re saying to the compiler is, “We know it’s really referring to a Ferrari object, so it’s okay to make a new Ferrari reference variable to refer to that object.” In this case we’re safe because before we ever try the cast, we do an instanceof test to make sure.

It’s important to know that the compiler is forced to trust us when we do a downcast, even when we screw up:

class Car { }
class Ferrari extends Car { }
class FerrariTest {
public static void main(String [] args) {
Car Car = new Car();
Ferrari d = (Ferrari) Car; // compiles but fails later
}
}

It can be maddening! This code compiles! When we try to run it, we’ll get an exception something like this:
java.lang.ClassCastException

Why can’t we trust the compiler to help us out here? Can’t it see that Car is of type Car? All the compiler can do is verify that the two types are in the same inheritance tree, so that depending on whatever code might have come before the downcast, it’s possible that Car is of type Ferrari. The compiler must allow things that might possibly work at runtime. However, if the compiler knows with certainty that the cast could not possibly work, compilation will fail.

The following replacement code block will NOT compile:
Car Car = new Car();
Ferrari d = (Ferrari) Car;
String s = (String) Car; // Car can't EVER be a String

In this case, you’ll get an error something like this:
inconvertible types

Unlike downcasting, upcasting (casting up the inheritance tree to a more general type) works implicitly (i.e., you don’t have to type in the cast) because when you upcast you’re implicitly restricting the number of methods you can invoke, as opposed to downcasting, which implies that later on, you might want to invoke a more specific method. For instance:

class Car { }
class Ferrari extends Car { }

class FerrariTest {
public static void main(String [] args) {
Ferrari d = new Ferrari();
Car a1 = d; // upcast ok with no explicit cast
Car a2 = (Car) d; // upcast ok with an explicit cast
}
}

Both of the previous upcasts will compile and run without exception, because a Ferrari IS-A Car, which means that anything a Car can do, a Ferrari can do. A Ferrari can do more, of course, but the point is—anyone with a Car reference can safely call Car methods on a Ferrari instance. The Car methods may have been overridden in the Ferrari class, but all we care about now is that a Ferrari can always do at least everything a Car can do. The compiler and JVM know it too, so the implicit upcast is always legal for assigning an object of a subtype to a reference of one of its supertype classes (or interfaces). If Ferrari implements Convertible, and Convertible defines openHood(), then a Ferrari can be implicitly cast to a Convertible, but the only Ferrari method you can invoke then is openHood(), which Ferrari was forced to implement because Ferrari implements the Convertible interface.

One more thing...if Ferrari implements Convertible, then if FerrariF12011 extends Ferrari, but FerrariF12011 does not declare that it implements Convertible, FerrariF12011 is still a Convertible! FerrariF12011 is a Convertible simply because it extends Ferrari, and Ferrari’s already taken care of the Convertible parts of itself, and all its children. The FerrariF12011 class can always override any methods it inherits from Ferrari, including methods that Ferrari implemented to fulfill its interface contract.

And just one more thing...if FerrariF12011 does declare it implements Convertible, just so that others looking at the FerrariF12011 class API can easily see that FerrariF12011 IS-A Convertible, without having to look at FerrariF12011’s superclasses, FerrariF12011 still doesn’t need to implement the openHood() method if the Ferrari class (FerrariF12011’s superclass) has already taken care of that. In other words, if FerrariF12011 IS-A Ferrari, and Ferrari IS-A Convertible, then FerrariF12011 IS-A Convertible, and has already met its Convertible obligations for implementing the openHood() method since it inherits the openHood() method. The compiler is smart enough to say, “I know FerrariF12011 already IS a Ferrari, but it’s OK to make it more obvious.”

So don’t be fooled by code that shows a concrete class that declares that it implements an interface, but doesn’t implement the methods of the interface. Before you can tell whether the code is legal, you must know what the superclasses of this implementing class have declared. If any superclass in its inheritance tree has already provided concrete (i.e., non-abstract) method implementations, then, regardless of whether the superclass declares that it implements the interface, the subclass is under no obligation to re-implement (override) those methods.

Tip: Sometimes, the exam questions are jammed into little spaces and that might add to a little confusion to the exam taker (YOU). So be on the watch out for stuff like this:
Car a = new Ferrari();
Ferrari d = (Ferrari) a;
d.doFerrariStuff();

Can be replaced with this easy-to-read bit of fun:
Car a = new Ferrari();
((Ferrari)a).doFerrariStuff();

In this case the compiler needs all of those parentheses, otherwise it thinks it’s been handed an incomplete statement.

Previous Chapter: Chapter 8: Object Oriented Concepts - Polymorphism

Next Chapter: Chapter 10: Implementing An Interface

No comments:

Post a Comment

© 2013 by www.inheritingjava.blogspot.com. All rights reserved. No part of this blog or its contents may be reproduced or transmitted in any form or by any means, electronic, mechanical, photocopying, recording, or otherwise, without prior written permission of the Author.

ShareThis

Google+ Followers

Followers