CS 4: Lecture 25 Monday, April 24, 2006 INHERITANCE (continued) =========== Class Hierarchies ----------------- Subclasses can have subclasses. Subclassing is transitive: if Dog is a subclass of Animal, and Beagle is a subclass of Dog, then Beagle is a subclass of Animal. Furthermore, _every_ class is a subclass of the Object class (including Java's built-in classes like String and BufferedReader). Object is at the top of every class hierarchy. Object / \ String Animal / \ Dog Cat Superclasses appear above their subclasses. / \ | PitBull Beagle Siamese That's why the equals() method takes a parameter of type Object: you can use equals() to compare any object to any other object of any class. (You can't use equals() to compare with a primitive type, though.) Inheritance and Constructors ---------------------------- What happens when we construct a Vector3D? As you would expect, Java executes a Vector3D() constructor, but first it executes the code in the Vector() constructor. The Vector3D() constructor should initialize fields unique to Vector3D(), like z; and it can also modify the work done by Vector() if appropriate. public class Vector3D extends Vector { [definitions from Lecture 24 here] public Vector3D() { // Vector() constructor called automatically System.out.println("Making new Vector3D."); } The zero-parameter Vector() constructor is always called by default, regardless of the parameters passed to the Vector3D() constructor. To change this default behavior, the Vector3D constructor can explicitly call any constructor for its superclass by using the "super" keyword. public Vector3D(double xx, double yy, double zz) { super(xx, yy); z = zz; } The call to "super()" must be the first statement in the constructor. If a constructor has no explicit call to "super", and its (nearest) superclass has no zero-parameter constructor, a compilation error occurs. There is no way to tell Java not to call a superclass constructor. You only have the power to choose which of the superclass constructors is called. Invoking Overridden Methods --------------------------- Sometimes you want to override a method, yet still be able to call the method implemented in the superclass. The following example shows how to do this. Suppose there is a method Vector.double() that multiplies a vector by two. We want Vector3D.double() to reuse the code in Vector.double(), but we also need to double the z-coordinate. public void double() { super.double(); // Double the values of x and y. z = 2 * z; // Double the value of z. } } Unlike superclass constructor invocations, ordinary superclass method invocations need not be the first statement in a method. The "protected" Keyword ----------------------- In the Vector class, we'd normally declare the fields "x" and "y" to be private. We usually don't want them to be public, because applications might corrupt or meddle with them, not using the proper interface. But if they're private, the Vector3D class can't read or change them directly, either. So we declare "x" and "y" to be "protected", not "private". public class Vector { protected double x, y; ... "protected" is a level of protection somewhere between "public" and "private". A "protected" field is visible to the declaring class and all its subclasses, but not to other classes. "private" fields aren't even visible to the subclasses. If "x" and "y" are declared private, the method Vector3D.length() [see Lecture 24] can't access them and won't compile. If they're declared protected, Vector3D.length() can access them because Vector3D is a subclass of Vector. When you write a class, if you think somebody might someday want to write a subclass of it, declare its vulnerable fields "protected", unless you have a reason for not wanting subclasses to see them. Helper methods often should be declared "protected" as well. Subtleties of Inheritance ------------------------- (1) Suppose we write a new method in the Cat class called meow(). We can't call meow() on an Animal. We can't even call meow() on a variable of type Animal that references a Cat. Cat c = new Cat(); c.meow(); // Groovy. Animal a = new Cat(); // Groovy--every Cat is an Animal. a.meow(); // COMPILE-TIME ERROR. Why? Because not every object of class Animal has a "meow()" method, so Java can't use dynamic method lookup to find the right "meow()" for the variable a. But if we define meow() in Animal instead, the statements above compile and run without errors, even if no meow() method is defined in class Cat. (Cat inherits meow() from Animal.) (2) You can't assign an Animal object to a Cat variable, because not every animal is a cat. Cat c = new Animal(); // COMPILE-TIME ERROR. The rules are more complicated when you assign one variable to another. Animal a; Cat c = new Cat(); a = c; // Groovy. c = a; // COMPILE-TIME ERROR. c = (Cat) a; // Groovy. a = new Animal(); c = (Cat) a; // RUN-TIME ERROR: ClassCastException. Why does the compiler refuse "c = a", but accept "c = (Cat) a"? The cast in the latter statement is your way of reassuring the compiler that you've written the program to guarantee that the Animal "a" will always be a Cat. If you're wrong, Java will find out when you run the program, and will crash with a "ClassCastException" error message. The error occurs only at run-time because Java cannot tell in advance what class of object "a" will reference. (3) Java has an "instanceof" operator that tells you whether an object is of a specific class. WARNING: The "o" in "instanceof" is not capitalized. if (a instanceof Cat) { c = (Cat) a; } This instanceof operation will return false if "a" is null or doesn't reference a Cat. It returns true if "a" references a Cat object--even if it's a subclass of Cat.