• Tidak ada hasil yang ditemukan

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 inter‐

faces, 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 meth‐

ods 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 5-6.

Figure 5-6. A class hierarchy

A subclass inherits all members of its superclass not designated as private. As we’ll discuss shortly, other levels of visibility affect which 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;

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 Ani mal 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.

Shadowed variables

We have seen 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 5-7.

We’re going to cover the details of this variable hiding now for completeness and in preparation for more advanced topics, but in practice you should almost never do this. It is much better in practice to structure your code to clearly differentiate vari‐

ables using different names or naming conventions.

In Figure 5-7, the variable weight is declared in three places: as a local variable in the

6Note that a better way to design our calculators would be to have an abstract Calculator class with two sub‐

classes: IntegerCalculator and DecimalCalculator.

when you reference it in the code would depend on the scope in which we are work‐

ing and how you qualify the reference to it.

Figure 5-7. The scope of shadowed variables

In the previous example, all variables were of the same type. A slightly more plausible use of shadowed variables would involve 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 vari‐

able, 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.6 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 DecimalCalcula tor, and they can have independent values. In fact, any methods that DecimalCalcu lator inherits from IntegerCalculator actually see the integer variable sum.

Because both variables exist in DecimalCalculator, we need a way to reference the variable inherited from IntegerCalculator. We do that using the super keyword as a qualifier on the 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 parent type). For example, we can refer to a DecimalCalculator object as an IntegerCalculator by using it via a variable of type 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

The same would be true if we accessed the object using an explicit cast to the Integer Calculator type or when passing an instance into a method that accepts that parent type.

To reiterate, 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 behav‐

ior when methods shadow other methods, or to use the correct terminology, override other methods.

Overriding methods

We have seen that we can 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

7The Platypus is a highly unusual egg-laying Mammal. We could override the reproduce() behavior again for it in its own subclass of Mammal.

8An overridden method in Java acts like a virtual method in C++.

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 5-8. Method overriding

In Figure 5-8, Mammal overrides the reproduce() method of Animal, perhaps to spe‐

cialize the method for the behavior of mammals giving birth to live young.7 The Cat object’s sleeping behavior is also overridden to be different from that of a general Ani mal, perhaps to accommodate catnaps. The Cat class also adds the more unique behaviors of purring and hunting mice.

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 fur‐

thest down the hierarchy) always overrides the others, even if we refer to the object through a reference of one of the superclass types.8

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 imple‐

mented in the Cat class, not the one in Animal:

Cat simon = new Cat();

Animal creature = simon;

...

creature.sleep(); // accesses Cat sleep();

In other words, for purposes of behavior (invoking methods), a Cat acts like a Cat, regardless of whether you refer to it as such. In other respects, the variable creature here may behave like an Animal reference. As we explained earlier, access to a shad‐

owed variable through an Animal reference would find an implementation in the Ani mal 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 treating it more generally 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 and still take advantage of their specialized implementations of behavior.

Interfaces

Java expands on the concept of abstract methods with interfaces. It’s often desirable to specify a group of abstract methods defining some behavior for an object without tying it to any implementation at all. In Java, this is called an interface. An interface defines a set of methods that a class must implement. A class in Java can declare that it implements an interface if it implements the required methods. Unlike extending an abstract class, a class implementing an interface doesn’t have to inherit from any par‐

ticular part of the inheritance hierarchy or use a particular implementation.

Interfaces are kind of like Boy Scout or Girl Scout merit badges. A scout who has learned to build a birdhouse can walk around wearing a little sleeve patch with a pic‐

ture of one. This says to the world, “I know how to build a birdhouse.” Similarly, an interface is a list of methods that define some set of behavior for an object. Any class that implements each method listed in the interface can declare at compile time that it implements the interface and wear, as its merit badge, an extra type—the interface’s type.

Interface types act like class types. You can declare variables to be of an interface type, you can declare arguments of methods to accept interface types, and you can specify that the return type of a method is an interface type. In each case, what is meant is that any object that implements the interface (i.e., wears the right merit badge) can fill that role. In this sense, interfaces are orthogonal to the class hierarchy. They cut across the boundaries of what kind of object an item is and deal with it only in terms of what it can do. A class can implement as many interfaces as it desires. In this way, interfaces in Java replace much of the need for multiple inheritance in other lan‐

guages (and all its messy complications).

An interface looks, essentially, like a purely abstract class (i.e., a class with only abstract methods). You define an interface with the interface keyword, and list its

void stopEngine();

float accelerate( float acc );

boolean turn( Direction dir );

}

The previous example defines an interface called Driveable with four methods. It’s acceptable, but not necessary, to declare the methods in an interface with the abstract modifier; we haven’t done that here. More importantly, the methods of an interface are always considered public, and you can optionally declare them as so.

Why public? Well, the user of the interface wouldn’t necessarily be able to see them otherwise, and interfaces are generally intended to describe the behavior of an object, not its implementation.

Interfaces define capabilities, so it’s common to name interfaces after their capabili‐

ties. Driveable, Runnable, and Updateable are good interface names. Any class that implements all the methods can then declare that it implements the interface by using a special implements clause in its class definition. For example:

class Automobile implements Driveable { ...

public boolean startEngine() { if ( notTooCold )

engineRunning = true;

...

}

public void stopEngine() { engineRunning = false;

}

public float accelerate( float acc ) { ...

}

public boolean turn( Direction dir ) { ...

} ...

}

Here, the class Automobile implements the methods of the Driveable interface and declares itself a type of Driveable using the implements keyword.

As shown in Figure 5-9, another class, such as Lawnmower, can also implement the Driveable interface. The figure illustrates the Driveable interface being imple‐

mented by two different classes. While it’s possible that both Automobile and Lawn mower could derive from some primitive kind of vehicle, they don’t have to in this scenario.

Figure 5-9. Implementing the Driveable interface

After declaring the interface, we have a new type, Driveable. We can declare vari‐

ables of type Driveable and assign them any instance of a Driveable object:

Automobile auto = new Automobile();

Lawnmower mower = new Lawnmower();

Driveable vehicle;

vehicle = auto;

vehicle.startEngine();

vehicle.stopEngine();

vehicle = mower;

vehicle.startEngine();

vehicle.stopEngine();

Both Automobile and Lawnmower implement Driveable, so they can be considered interchangeable objects of that type.

Dokumen terkait