Saturday 12 January 2013

Chapter 2: Object Orientation:Reference Variable Casting

It's at the heart of polymorphism
Animal animal = new Dog();

 But what happens when you want to use that animal reference variable to invoke
a method that only class Dog has? You know it's referring to a Dog, and you want to
do a Dog-specific thing? In the following code, we've got an array of Animals, and
whenever we find a Dog in the array, we want to do a special Dog 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 playDead method.

class Animal
{
    void makeNoise()
    {
        System.out.println("generic noise");
    }
}

class Dog extends Animal
{
    void makeNoise()
    {
        System.out.println("bark");
    }

    void playDead()
    {
        System.out.println(" roll over");
    }
}

class CastTest2
{
    public static void main(String[] args)
    {
        Animal[] a =
        { new Animal(), new Dog(), new Animal() };
        for (Animal animal : a)
        {
            animal.makeNoise();
            if (animal instanceof Dog)
            {
                animal.playDead(); // try to do a Dog behavior ?
            }
        }
    }
}

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

The compiler is saying, "Hey, class Animal doesn't have a playDead() method".
Let's modify the if code block:

           if (animal instanceof Dog)
            {
                Dog d = (Dog) animal; // casting the ref. var.
                d.playDead();
            }

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 playDead,
we cast the animal variable to type Dog. What we're saying to the compiler is, "We
know it's really referring to a Dog object, so it's okay to make a new Dog 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.

class Animal { }
class Dog extends Animal { }

class DogTest
{
    public static void main(String[] args)
    {
        Animal animal = new Animal();
        Dog d = (Dog) animal; // 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

Animal animal = new Animal();
Dog d = (Dog) animal;
String s = (String) animal; // animal 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 Animal
{
}

class Dog extends Animal
{
}

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

Both of the previous upcasts will compile and run without exception, because a
Dog IS-A Animal, which means that anything an Animal can do, a Dog can do. A
Dog can do more, of course, but the point is—anyone with an Animal reference can
safely call Animal methods on a Dog instance. The Animal methods may have been
overridden in the Dog class, but all we care about now is that a Dog can always do
at least everything an Animal 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 Dog implements Pet, and Pet defines
beFriendly(), then a Dog can be implicitly cast to a Pet, but the only Dog method
you can invoke then is beFriendly(), which Dog was forced to implement because
Dog implements the Pet interface.

One more thing…if Dog implements Pet, then if Beagle extends Dog, but
Beagle does not declare that it implements Pet, Beagle is still a Pet! Beagle is a Pet
simply because it extends Dog, and Dog's already taken care of the Pet parts of itself,
and all its children. The Beagle class can always override any methods it inherits
from Dog, including methods that Dog implemented to fulfill its interface contract.
And just one more thing…if Beagle does declare it implements Pet, just so that
others looking at the Beagle class API can easily see that Beagle IS-A Pet, without
having to look at Beagle's superclasses, Beagle still doesn't need to implement the
beFriendly() method if the Dog class (Beagle's superclass) has already taken care of
that. In other words, if Beagle IS-A Dog, and Dog IS-A Pet, then Beagle IS-A Pet,
and has already met its Pet obligations for implementing the beFriendly() method
since it inherits the beFriendly() method. The compiler is smart enough to say, "I
know Beagle already IS a Dog, 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 class in its inheritance tree has already
provided concrete (i.e., non-abstract) method implementations, and has declared
that it (the superclass) implements the interface, then the subclass is under no
obligation to re-implement (override) those methods.

Animal a = new Dog();
Dog d = (Dog) a;
a.doDogStuff();
Can be replaced with this easy-to-read bit of fun:
Animal a = new Dog();
((Dog)a).doDogStuff();
In this case the compiler needs all of those parentheses, otherwise it
thinks it’s been handed an incomplete statement.


No comments:

Post a Comment