Section 6.1. Subclassing and Inheritance

6.1. Subclassing and Inheritance

Classes in Java exist in a hierarchy. A class in Java can be declared as a subclass of another class using the extends keyword. A subclass inherits variables and methods from its superclass and can use them as if they were declared within the subclass itself:

     class Animal {         float weight;         ...         void eat( ) {             ...         }         ...     }     class Mammal extends Animal {         // inherits weight         int heartRate;         ...         // inherits eat( )         void breathe( ) {             ...         }     }

In this example, an object of type Mammal has both the instance variable weight and the method eat( ). They are inherited from Animal.

A class can extend only one other class. To use the proper terminology, Java allows single inheritance of class implementation. Later in this chapter, we'll talk about interfaces, which take the place of multiple inheritance as it's primarily used in other languages.

A subclass can be further subclassed. Normally, subclassing specializes or refines a class by adding variables and methods (you cannot remove or hide variables or methods by subclassing). For example:

     class Cat extends Mammal {         // inherits weight and heartRate         boolean longHair;         ...         // inherits eat( ) and breathe( )         void purr( ) {             ...         }     } 

The Cat class is a type of Mammal that is ultimately a type of Animal. Cat objects inherit all the characteristics of Mammal objects and, in turn, Animal objects. Cat also provides additional behavior in the form of the purr( ) method and the longHair variable. We can denote the class relationship in a diagram, as shown in Figure 6-1.

A subclass inherits all members of its superclass not designated as private. As we'll discuss shortly, other levels of visibility affect what inherited members of the class can be seen from outside of the class and its subclasses, but at a minimum, a subclass always has the same set of visible members as its parent. For this reason, the type of a subclass can be considered a subtype of its parent, and instances of the subtype can be used anywhere instances of the supertype are allowed. Consider the following example:

     Cat simon = new Cat( );     Animal creature = simon; 

Figure 6.1. A class hierarchy

The Cat instance simon in this example can be assigned to the Animal type variable creature because Cat is a subtype of Animal. Similarly, any method accepting an Animal object would accept an instance of a Cat or any Mammal type as well. This is an important aspect of polymorphism in an object-oriented language such as Java. We'll see how it can be used to refine a class's behavior, as well as add new capabilities to it.

6.1.1. Shadowed Variables

In Chapter 5, we saw that a local variable of the same name as an instance variable shadows (hides) the instance variable. Similarly, an instance variable in a subclass can shadow an instance variable of the same name in its parent class, as shown in Figure 6-2.

Figure 6.2. The scope of shadowed variables

In Figure 6-2, the variable weight is declared in three places: as a local variable in the method foodConsumption( ) of the class Mammal, as an instance variable of the class Mammal, and as an instance variable of the class Animal. The actual variable selected depends on the scope in which we are working.

In the previous example, all variables were of the same type. Just about the only reason to declare a variable with the same type in a subclass is to provide an alternate initializer.

A slightly more plausible use of shadowed variables involves changing their types. We could, for example, shadow an int variable with a double variable in a subclass that needs decimal values instead of integer values. We can do this without changing the existing code because, as its name suggests, when we shadow variables, we don't replace them but instead mask them. Both variables still exist; methods of the superclass see the original variable, and methods of the subclass see the new version. The determination of what variables the various methods see occurs at compile time.

Here's a simple example:

     class IntegerCalculator {         int sum;         ...     }     class DecimalCalculator extends IntegerCalculator {         double sum;         ...     } 

In this example, we shadow the instance variable sum to change its type from int to double.[*] Methods defined in the class IntegerCalculator see the integer variable sum while methods defined in DecimalCalculator see the floating-point variable sum. However, both variables actually exist for a given instance of DecimalCalculator, and they can have independent values. In fact, any methods that DecimalCalculator inherits from IntegerCalculator actually see the integer variable sum.

[*] Note that a better way to design our calculators would be to have an abstract Calculator class with two subclasses: IntegerCalculator and DecimalCalculator.

Since both variables exist in DecimalCalculator, we need a way to reference the variable inherited from IntegerCalculator. We do that using the super reference:

     int s = super.sum; 

Inside of DecimalCalculator, the super keyword used in this manner selects the sum variable defined in the superclass. We'll explain the use of super more fully in a bit.

Another important point about shadowed variables has to do with how they work when we refer to an object by way of a less derived type (a supertype). For example, we can refer to a DecimalCalculator object as an IntegerCalculator. If we do so and then access the variable sum, we get the integer variable, not the decimal one:

     DecimalCalculator dc = new DecimalCalculator( );     IntegerCalculator ic = dc;     int s = ic.sum;       // accesses IntegerCalculator sum

After this detailed explanation, you may still wonder what shadowed variables are good for. To be honest, the usefulness of shadowed variables is limited. It's much better to abstract the use of variables like this in other ways than to use tricky scoping rules. However, it's important to understand the concepts here before we talk about doing the same thing with methods. We'll see a different and more dynamic type of behavior when methods shadow other methods, or to use the correct terminology, override other methods.

6.1.2. Overriding Methods

In Chapter 5, we saw that we could declare overloaded methods (i.e., methods with the same name but a different number or type of arguments) within a class. Overloaded method selection works in the way we described on all methods available to a class, including inherited ones. This means that a subclass can define additional overloaded methods that add to the overloaded methods provided by a superclass.

A subclass can do more than that; it can define a method that has exactly the same method signature (name and argument types) as a method in its superclass. In that case, the method in the subclass overrides the method in the superclass and effectively replaces its implementation, as shown in Figure 6-3. Overriding methods to change the behavior of objects is called subtype polymorphism. It's the usage most people think of when they talk about the power of object-oriented languages.

Figure 6.3. Method overriding

In Figure 6-3, Mammal overrides the reproduce( ) method of Animal, perhaps to specialize the method for the peculiar behavior of mammals' giving live birth.[*] The Cat object's sleeping behavior is also overridden to be different from that of a general Animal, perhaps to accommodate cat naps. The Cat class also adds the more unique behaviors of purring and hunting mice.

[*] We'll ignore the platypus, which is an obscure nonovoviviparous mammal.

From what you've seen so far, overridden methods probably look like they shadow methods in superclasses, just as variables do. But overridden methods are actually more powerful than that. When there are multiple implementations of a method in the inheritance hierarchy of an object, the one in the "most derived" class (the lowest one in the hierarchy) always overrides the others, even if we refer to the object through a reference of one of the superclass types.[*]

[*] An overridden method in Java acts like a virtual method in C++.

For example, if we have a Cat instance assigned to a variable of the more general type Animal, and we call its sleep( ) method, we still get the sleep( ) method implemented in the Cat class, not the one in Animal:

     Cat simon = new Cat( );     Animal creature = simon;       ...     creature.sleep( );       // accesses Cat sleep( );

In other words, a Cat acts like a Cat, regardless of whether you know that you have one or not. In other respects, the variable creature may look like an Animal. As we explained earlier, access to a variable through an Animal reference would find the implementation in the Animal class, not the Cat class. However, because methods are located dynamically, searching subclasses first, the appropriate method in the Cat class is invoked, even though we are dealing with it as an Animal object. This means that the behavior of objects is dynamic. We can deal with specialized objects as if they were more general types of objects and still take advantage of their specialized implementations of behavior. @Override

A common programming error in Java is to accidentally overload a method when trying to override it. Any difference in the number or type of arguments produces two overloaded methods instead of a single, overridden method. The new annotations syntax in Java 5.0 provides a way to get the compiler to help with this problem. An annotation, as we'll describe in Chapter 7, allows us to add special markers or metadata to source code that can be read by the compiler or runtime tools. One of the standard annotations that Java defines is called @Override and it tells the compiler that the method it marks is intended to override a method in the superclass. The compiler then warns if the method doesn't match. For example, we could specify that the sleep( ) method of our Cat class overrides one in a superclass like so:

     class Cat extends Mammal {         ...         @Override void sleep( ) { ... }     } Overridden methods and dynamic binding

In a previous section, we mentioned that overloaded methods are selected by the compiler at compile time. Overridden methods, on the other hand, are selected dynamically at runtime. Even if we create an instance of a subclass our code has never seen before (perhaps a new class loaded from the network), any overriding methods that it contains are located and used at runtime, replacing those that existed when we last compiled our code.

In contrast, if we load a new class that implements an additional, more specific, overloaded method, our code continues to use the implementation it discovered at compile time. Another effect of this is that casting (i.e., explicitly telling the compiler to treat an object as one of its assignable types) affects the selection of overloaded methods but not overridden methods. Static method binding

Static methods don't belong to any object instance; they are accessed directly through a class name, so they are not dynamically selected at runtime like instance methods. That is why static methods are called "static"; they are always bound at compile time.

A static method in a superclass can be shadowed by another static method in a subclass, as long as the original method was not declared final. However, you can't "override" a static method with an instance method. In other words, you can't have a static method and instance method with the same signature in the same class hierarchy. final methods and performance

In languages like C++, the default is for methods to act like shadowed variables, so you have to declare explicitly the methods you want to be dynamic (or, as C++ terms them, virtual). In Java, instance methods are, by default, dynamic. But you can use the final modifier to declare that an instance method can't be overridden in a subclass, and it won't be subject to dynamic binding.

We have seen final used with variables to effectively make them constants. When applied to a method, final means that its implementation is constantno overriding allowed. final can also be applied to an entire class, which means the class can't be subclassed.

In the old days, dynamic method binding came with a significant performance penalty, and some people are still inclined to use the final modifier to guard against this. Modern Java runtime systems eliminate the need for this kind of fudging. A profiling runtime can determine which methods are not being overridden and "optimistically" inline them, treating them as if they were final until it becomes necessary to do otherwise. As a rule, you should use the final keyword when it is correct for your program's structure, not for performance considerations. Compiler optimizations

In some older versions of Java, the javac compiler can be run with a -O switch, which tells it to perform certain optimizations, like inlining, statically. Most of these optimizations are now done at runtime by smarter VMs, so switches like this are generally not necessary.

Another kind of optimization allows you to include debugging code in your Java source without a size or performance penalty. Java doesn't have a preprocessor to explicitly control what source is included, but you can get some of the same effects by making a block of code conditional on a constant (i.e., static and final) variable. The Java compiler is smart enough to remove this code when it determines that it won't be called. For example:

     static final boolean DEBUG = false;     ...     final void debug (String message) {         if (DEBUG) {             System.err.println(message);             // do other stuff             ...         }     }

In this case, the compiler can recognize that the condition on the DEBUG variable is always false, and the body of the debug( ) method will be optimized away. With a modern compiler the method call would be removed entirely. Of course, with the speed of modern machines, it's hard to imagine that a simple condition such as this would be significant anyway.

Note that this kind of debugging code is useful for purposes other than assertions, which we covered in Chapter 4. Assertions are supposed to be yes/no tests that guarantee the correctness of your program logic. While you could trick them into printing debugging information, it's not what they were intended for. Method selection revisited

By now you should have a good, intuitive feel for how methods are selected from the pool of potentially overloaded and overridden method names of a class. If, however, you are dying for more detail, we'll provide it now.

In a previous section, we offered an inductive rule for overloaded method resolution. It said that a method is considered more specific than another if its arguments are assignable to the arguments of the second method. We can now expand this rule to include the resolution of overridden methods by adding the following condition: to be more specific than another method, the type of the class containing the method must also be assignable to the type of the class holding the second method.

What does that mean? Well, the only classes whose types are assignable are classes in the same inheritance hierarchy, meaning the set of all methods of the same name in a class or any of its parent or child classes. Since subclass types are assignable to superclass types, but not vice versa, the resolution is pushed, in the way that we expect, down the chain, toward the subclasses. This effectively adds a second dimension to the search, in which resolution is pushed down the inheritance tree toward more refined classes and, simultaneously, toward the most specific overloaded method within a given class. Exceptions and overridden methods

An overriding method may change the behavior of an object, but in some ways, it must still fulfill the contract of the original method with the user. Specifically, an overriding method must adhere to the throws clause of the original method. The new method cannot throw new types of checked exceptions. It can only declare that it throws exception types assignable to those thrown by the method in the parent class. That is, it may declare that it throws the same types of exceptions or subtypes of those declared by the original method.

If the new method does not throw any of the checked exceptions of the original, it does not have to declare them and callers of the method via the subclass do not have to guard against them. The new method may also declare exactly the same checked exceptions as the original. The interesting thing is that the new method also has the option to refine those types by declaring that it throws more specific subtypes than the overridden method. This is not the same as just saying that the method can simply throw subtypes of its declared exceptions; any method can do that. The new method can actually redefine the throws clause of the method to be more specific. This technique is called covariant typing of the throws clause, which means that the exception types change to become more refined with the subtype.

Let's quickly review what the throws clause really means. If a method declares that it can throw an IOException, it is really saying that it can throw exceptions of type IOException or its subtypes. For example, FileNotFoundException is a type of IOException. A method declaring that it can throw IOException could actually throw FileNotFoundException or any other subtype of IOException at runtime:

     public void readFile( ) throws IOException {         ...         if ( error ) throw new FileNotFoundException( filename );     }

When we call this method, the compiler will ensure that we allow for the possibility of any kind of IOException, using either a TRy/catch block or by throwing the exception from our own method.

When we override a method in a subclass, we get an opportunity to rewrite the throws clause of the method a bit. The new method must still be backward-compatible with the original, so any checked exceptions it throws must be assignable to those thrown by the overridden method. But we can be more specific if we want, refining the type of exception to go along with the new method's behavior. For example:

     class MeatInedibleException extends InedibleException { ... }     class Animal {         void eat( Food f ) throws InedibleException {             ...         }     }     class Herbivore extends Animal {         void eat( Food f ) throws MeatInedibleException {             if ( f instanceof Meat )                 throw new MeatInedibleException( );             ...         }     }

In this code, Animal specifies that it can throw an InedibleException from its eat( ) method. Herbivore is a subclass of Animal, so its eat( ) method must also be able to throw an InedibleException. However, Herbivore's eat( ) method actually declares that it throws a more specific exception: MeatInedibleException. It can do this because MeatInedibleException is a subtype of InedibleException. If we are working with an Herbivore type directly, the compiler will allow us to catch just the MeatInedibleException and not require us to guard against the more general InedibleException:

     Herbivore creature = ...     try { food );     } catch ( MeatInedibleException ) {         // creature can't eat this food because it's meat     }

On the other hand, if we don't care why the food is inedible, we're free to guard for the more general InedibleException alone and treat it as any other Animal.

To sum up, an overriding method can refine not only the behavior of the parent method but also the type of checked exceptions it throws. Next, we'll talk about overridden methods that change their return type in exactly the same way. Return types and overridden methods

For a method to qualify as an overridden method in a subclass, it must have exactly the same number and types of arguments. It must have the same "inputs," as it were. As we saw in the previous section, overriding methods may refine their "output" to some extent. Namely, they can narrow their throws clause by declaring that they throw subtypes of the original method's exception types. What about the main "output" of a method? Its return value? Can we change the return type of a method by overriding it? The answer used to be "no," but Java 5.0 gives us covariant return types on methods just as it does for exception types.

What this means is that, as of Java 5.0, when you override a method, you may change the return type to a subtype of the original method's return type. For example, if our Animal class has a factory method called create( ) that produces an instance of Animal, our Mammal class could refine the return type to Mammal:

     class Animal {         Animal create( ) { ... }     }     class Mammal extends Animal {         Mammal create( ) { ... }     }

As we'll see later, this coding technique is very helpful because it eliminates runtime casting of objects.

6.1.3. Special References: this and super

The special references this and super allow you to refer to the members of the current object instance or to members of the superclass, respectively. We have seen this used elsewhere to pass a reference to the current object and to refer to shadowed instance variables. The reference super does the same for the parents of a class. You can use it to refer to members of a superclass that have been shadowed or overridden. Being able to invoke the original method of the superclass allows us to use it as part of our new method, delegating to its behavior before or after we perform additional work:

     class Animal {         void eat( Food f ) throws InedibleException {             // consume food         }     }     class Herbivore extends Animal {         void eat( Food f ) throws MeatInedibleException {             // check if edible             ...    f );         }     }

In this example, our Herbivore class overrides the Animal eat( ) method to first do some checking on the food object. After doing its job, it uses ) to call the (otherwise overridden) implementation of eat( ) in its superclass.

super prompts a search for the method or variable to begin in the scope of the immediate superclass rather than the current class. The inherited method or variable found may reside in the immediate superclass or in a more distant one. The usage of the super reference when applied to overridden methods of a superclass is special; it tells the method resolution system to stop the dynamic method search at the superclass instead of at the most derived class (as it otherwise does). Without super, there would be no way to access overridden methods.

6.1.4. Casting

A cast explicitly tells the compiler to change the apparent type of an object reference. The main use for casts is when an object is temporarily assigned to a more general type. For example, if a String were assigned to a variable of type Object, to use it as a String again, we'd have to perform a cast to get it back. The compiler recognizes only the declared types of variables and doesn't know that we actually placed a String into it. In Java, casts are checked both at compile time and at runtime to make sure they are legal. At compile time the Java compiler will stop you from trying to perform a cast that cannot possibly work (such as turning a Date directly into a String). And at runtime Java will check that casts that are plausible (such as our String to String) are actually correct.

Attempting to cast an object to an incompatible type at runtime results in a ClassCastException. Only casts between objects in the same inheritance hierarchy (and, as we'll see later, to appropriate interfaces) are legal in Java and pass the scrutiny of the compiler and the runtime system. Casts in Java affect only the treatment of references; they never change the form of the actual object. This is an important rule to keep in mind. You never change the object pointed to by a reference by casting it; you change only the compiler's (or runtime system's) notion of it.

A cast can be used to narrow or downcast the type of a referenceto make it more specific. Often, we'll do this when we have to retrieve an object from a more general type of collection or when it has been previously used as a less derived type. (The prototypical example is using an object in a collection, as we'll see in Chapter 11.) Continuing with our Cat example:

     Animal creature;     Cat simon;     // ...     creature = simon;        // OK     // simon = creature;     // Compile-time error, incompatible type     simon = (Cat)creature;   // OK

We can't reassign the reference in creature to the variable simon even though we know it holds an instance of a Cat (Simon). We have to perform the indicated cast to narrow the reference. Note that an implicit cast was performed when we went the other way to widen the reference simon to type Animal during the first assignment. In this case, an explicit cast would have been legal but superfluous.

What all this means is that you can't lie or guess about what an object is. If you have a Cat object, you can use it as an Animal or even Object because all Java classes are a subclass of Object. But if you have an Object you think is a Cat, you have to perform a cast to get it back to an Animal or a Cat. If you aren't sure whether the Object is a Cat or a Dog at runtime, you can check it with instanceof before you perform the cast. If you get the cast wrong, the runtime system throws a ClassCastException.

     if ( creature instanceof Cat ) {         Cat cat = (Cat)creature;         cat.meow( );     }

As we mentioned earlier, casting can affect the selection of compile-time items such as variables and overloaded methods, but not the selection of overridden methods. Figure 6-4 shows the difference. As shown in the top half of the diagram, casting the reference simon to type Animal (widening it) affects the selection of the shadowed variable weight within it. However, as the lower half of the diagram indicates, the cast doesn't affect the selection of the overridden method sleep( ).

Figure 6.4. Casting and selection of methods and variables Casting aspersions

Casting in Java is something that programmers strive to avoid. This is not only because it indicates a weakness in the static typing of the code, but because casts can also simply be tedious to use and make code less readable. Unfortunately, a great deal of code written in Java in the past has had no choice but to rely on casting so that it can work with any type of object the user requires. Java 5.0 introduced a major new language feature, generics, partly to address this issue. Generics allow Java code to be "typed" for a particular kind of object by the user, eliminating the need to cast in many situations. We'll cover generics in detail in Chapter 8 and see how they reduce the need for casts in most Java code.

6.1.5. Using Superclass Constructors

When we talked earlier about constructors, we discussed how the special statement this( ) invokes an overloaded constructor upon entry to another constructor. Similarly, the statement super( ) explicitly invokes the constructor of a superclass. Of course, we also talked about how Java makes a chain of constructor calls that includes the superclass's constructor, so why use super( ) explicitly? When Java makes an implicit call to the superclass constructor, it calls the default constructor. If we want to invoke a superclass constructor that takes arguments, we have to do so explicitly using super( ).

If we are going to call a superclass constructor with super( ), it must be the first statement of our constructor, just as this( ) must be the first call we make in an overloaded constructor. Here's a simple example:

     class Person {         Person ( String name ) {             //  setup based on name             ...         }         ...     }     class Doctor extends Person {         Doctor ( String name, String specialty ) {             super( name );             // setup based on specialty             ...         }         ...     }

In this example, we use super( ) to take advantage of the implementation of the superclass constructor and avoid duplicating the code to set up the object based on its name. In fact, because the class Person doesn't define a default (no arguments) constructor, we have no choice but to call super( ) explicitly. Otherwise, the compiler would complain that it couldn't find an appropriate default constructor to call. In other words, if you subclass a class whose constructors all take arguments, you have to invoke one of the superclass's constructors explicitly from at least one of your subclass's constructors.

Instance variables of the class are initialized upon return from the superclass constructor, whether that's due to an explicit call to super( ) or an implicit call to the default superclass constructor.

6.1.6. Full Disclosure: Constructors and Initialization

We can now tell the full story of how constructors are chained together and when instance variable initialization occurs. The rule has three parts and is applied repeatedly for each successive constructor that is invoked:

  • If the first statement of a constructor is an ordinary statementi.e., not a call to this( ) or super( )Java inserts an implicit call to super( ) to invoke the default constructor of the superclass. Upon returning from that call, Java initializes the instance variables of the current class and proceeds to execute the statements of the current constructor.

  • If the first statement of a constructor is a call to a superclass constructor via super( ), Java invokes the selected superclass constructor. Upon its return, Java initializes the current class's instance variables and proceeds with the statements of the current constructor.

  • If the first statement of a constructor is a call to an overloaded constructor via this( ), Java invokes the selected constructor, and upon its return, simply proceeds with the statements of the current constructor. The call to the superclass's constructor has happened within the overloaded constructor, either explicitly or implicitly, so the initialization of instance variables has already occurred.

6.1.7. Abstract Methods and Classes

A method in Java can be declared with the abstract modifier to indicate that it's just a prototype. An abstract method has no body; it's simply a signature declaration followed by a semicolon. You can't directly use a class that contains an abstract method; you must instead create a subclass that implements the abstract method's body:

     abstract void vaporMethod( String name );

In Java, a class that contains one or more abstract methods must be explicitly declared as an abstract class, also using the abstract modifier:

     abstract class vaporClass {         ...         abstract void vaporMethod( String name );         ...     }

An abstract class can contain other nonabstract methods and ordinary variable declarations, but it can't be instantiated. To be used, it must be subclassed and its abstract methods must be overridden with methods that implement a body. Not all abstract methods have to be implemented in a single subclass, but a subclass that doesn't override all its superclass's abstract methods with actual, concrete implementations must also be declared abstract.

Abstract classes provide a framework for classes that is to be "filled in" by the implementer. The class, for example, has a single abstract method called read( ). Various subclasses of InputStream implement read( ) in their own ways to read from their own sources. The rest of the InputStream class, however, provides extended functionality built on the simple read( ) method. A subclass of InputStream inherits these nonabstract methods to provide functionality based on the simple read( ) method that the subclass implements.

    Learning Java
    Learning Java
    ISBN: 0596008732
    EAN: 2147483647
    Year: 2005
    Pages: 262

    Similar book on Amazon © 2008-2017.
    If you may any questions please contact us: