Suppose we want to implement the game of Cram, which is identical to Domineering except that a player can play each domino either horizontally or vertically. We could write a new program that looks very similar to the old one. A better solution is to use inheritance. We write a new class Cram which extends Domineering (Figure 3-1). This new class specifies only the things that are different.
Cram is called a subclass of Domineering. Conversely, Domineering is a superclass of Cram. The relationship between the two classes is shown in Figure 3-2.
Figure 3-1. With inheritance, the Cram class is surprisingly short. Line 24 is necessary to clear out the input line after reading the column number. The method charAt(), invoked on the String INPUT.nextLine(), returns the character at a particular index.
1 /** The game of Cram. */ 2 public class Cram extends Domineering { 3 4 /** No special initialization is required. */ 5 public Cram() { 6 super(); 7 } 8 9 /** Play until someone wins. */ 10 public void play() { 11 int player = 1; 12 while (true) { 13 System.out.println(" " + this); 14 System.out.println("Player " + player + " to play"); 15 if (!(hasLegalMoveFor(HORIZONTAL) 16 || hasLegalMoveFor(VERTICAL))) { 17 System.out.println("No legal moves -- you lose!"); 18 return; 19 } 20 System.out.print("Row: "); 21 int row = INPUT.nextInt(); 22 System.out.print("Column: "); 23 int column = INPUT.nextInt(); 24 INPUT.nextLine(); // To clear out input 25 System.out.print("Play horizontally (y/n)? "); 26 boolean direction; 27 if (INPUT.nextLine().charAt(0) == 'y') { 28 direction = HORIZONTAL; 29 } else { 30 direction = VERTICAL; 31 } 32 playAt(row, column, direction); 33 player = 3 - player; 34 } 35 } 36 37 /** Create and play the game. */ 38 public static void main(String[] args) { 39 System.out.println("Welcome to Cram."); 40 Cram game = new Cram(); 41 game.play(); 42 } 43 } |
Figure 3-2. UML class diagram showing that Cram is a subclass of Domineering. The arrow for class extension uses the same hollow head as the one for interface implementation, but has a solid instead of a dashed line.
The fields and methods not listed in the code for the Cram class are inherited from the Domineering class. If we invoke a method like playAt() or hasLegalMoveFor() on an instance of Cram, the method from the Domineering class is used. Inherited fields and methods make a Cram instance similar to a Domineering instance.
We can provide additional fields and methods, although the Cram class does not do so. The Cram class does override two methods, play() and main(). When we invoke the play() method on an instance of Cram, the new version is used. Additional fields and methods, along with overridden methods, make a Cram instance different from a Domineering instance.
The difference between overloading and overriding is a subtle one. When we overload a method name, Java decides which version to use based on the arguments which are passed to the method. When we override a method, Java decides which version to use based on the object on which the method is invoked. If two methods with the same name are in the same class but have different signatures, the method name is overloaded. If two methods with the same name and signature are in different classes (one a subclass of the other), the method in the subclass overrides the one in the superclass.
The invocation
super();
on line 6 in the constructor says, "Do whatever you would do to set up an instance of Domineering." A constructor in a subclass must always begin by invoking a constructor from the class it extends, although, as we will see later in this chapter, this invocation can often be implicit. In this case, the constructor from Domineering initializes the field that holds the board. Notice that it doesn't matter if we've forgotten how that field was initialized or even what it was called. Inheritance allows us to extend an encapsulated class without thinking about its inner workings. This allows us to develop correct software much more rapidly.
Extending a class is similar to implementing an interface. The key difference is that a superclass provides functionality, while an interface merely makes promises. A class can implement many interfaces, but it can only have one superclass. If a subclass had two superclasses, there would be problems if both of them provided some method which the subclass did notit would not be clear which version should be inherited.
Polymorphism and Inheritance
As a second example of inheritance, consider the class Light (Figure 3-3). This very simple class has only one field: a boolean indicating whether it is on or off. The toString() method returns the String "O" if the Light is on and "." if it is off.
Figure 3-3. The class Light models a light bulb.
1 /** A light bulb. */ 2 public class Light { 3 4 /** Whether the Light is on. */ 5 private boolean on; 6 7 /** A Light is off by default. */ 8 public Light() { 9 on = false; 10 } 11 12 /** Return true if the Light is on. */ 13 public boolean isOn() { 14 return on; 15 } 16 17 /** Set whether the Light is on. */ 18 public void setOn(boolean on) { 19 this.on = on; 20 } 21 22 public String toString() { 23 if (on) { 24 return "O"; 25 } else { 26 return "."; 27 } 28 } 29 30 } |
The Light class is extended by the ColoredLight class (Figure 3-4), which also has a char indicating its color. The color is determined randomly in the constructor. A ColoredLight looks the same as a Light when it is off, but toString() returns "R", "G", or "B", respectively, for red, green, or blue ColoredLights.
Figure 3-4. ColoredLight is a subclass of Light.
1 /** A colored light bulb. */ 2 public class ColoredLight extends Light { 3 4 /** Color of the ColoredLight. */ 5 private char color; 6 7 /** Set the color randomly to one of 'R', 'G', or 'B'. */ 8 public ColoredLight() { 9 super(); 10 int x = (int)(Math.random() * 3); 11 switch (x) { 12 case 0: 13 color = 'R'; 14 break; 15 case 1: 16 color = 'G'; 17 break; 18 default: 19 color = 'B'; 20 } 21 } 22 23 /** Return the color of this ColoredLight. */ 24 public char getColor() { 25 return color; 26 } 27 28 public String toString() { 29 if (isOn()) { 30 return "" + color; 31 } else { 32 return "."; 33 } 34 } 35 36 } |
ColoredLight inherits the field on and adds a new field color. It inherits the methods isOn() and setOn(). It overrides the toString() method and adds a new method, getColor(). This is illustrated in Figure 3-5.
Figure 3-5. UML class diagram showing that ColoredLight extends Light. Only new fields and methods are shown in the subclass. Inherited or overridden fields can be read from the superclass.
(This item is displayed on page 72 in the print version)
Although ColoredLight inherits the field on, it does not have direct access to the field. On line 29, the toString() method must work through the inherited method isOn() because the field on is private in Light. The private status of this field means that no other class, not even a subclass of Light, has direct access to on. This is information hiding enforcing encapsulation.
A variable of type Light can hold an instance of Light or of any subclass of Light. It is therefore a polymorphic type. Thus, it is perfectly legal to say:
Light bulb = new ColoredLight();
Why not simply cut to the chase and declare bulb to be of type Object? By declaring it to be of type Light, we guarantee that all of the methods defined in the Light class are available. Every method in Light has to be either inherited or overridden, so it is safe to call such a method on bulb. We can turn bulb off without knowing its exact class:
bulb.setOn(false);
If bulb were of type Object, we would have to cast it in order to do this.
If we invoke toString() on bulb, Java uses the class of the instance (ColoredLight) instead of the type of the variable (Light) to determine which version of the method to use. This process is called dynamic dispatch, because the decision is made dynamically at run time rather than once and for all at compile time.
Chains of Inheritance
Can we make a subclass of a subclass? Sure! The class FlashingColoredLight (Figure 3-6) extends ColoredLight. It turns itself on or off every time toString() is invoked. (This is slightly bad style, because we normally don't expect toString() to change an object's state.)
Figure 3-6. FlashingColoredLight extends ColoredLight.
1 /** A flashing, colored light bulb. */ 2 public class FlashingColoredLight extends ColoredLight { 3 4 /** No special initialization is required. */ 5 public FlashingColoredLight() { 6 super(); 7 } 8 9 /** Toggle the light's on status after returning a String. */ 10 public String toString() { 11 String result; 12 if (isOn()) { 13 result = "" + getColor(); 14 } else { 15 result = "."; 16 } 17 setOn(!isOn()); 18 return result; 19 } 20 21 } |
Inheritance is transitive, so FlashingColoredLight inherits every field and method from Light except for those methods overridden by ColoredLight.
The relationship between the three classes is shown in Figure 3-7. We can say that ColoredLight and FlashingColoredLight are proper descendants of Light. Strictly speaking, the descendants of Light are itself plus its proper descendants. This is consistent with the concepts of subset and proper subset from set theory. Conversely, Light and ColoredLight are proper ancestors of FlashingColoredLight. All three classes are ancestors of FlashingColoredLight.
Figure 3-7. A chain of inheritance.
(This item is displayed on page 74 in the print version)
Alternately, we can say that ColoredLight and FlashingColoredLight are both subclasses of Light, but only ColoredLight is a direct subclass. Conversely, both Light and ColoredLight are superclasses of FlashingColoredLight, but ColoredLight is the direct superclass.
We will omit the words "proper" and "direct" when there is no danger of confusion.
Is-a vs Has-a
It takes some experience to know when to extend a class. For example, suppose we want to model a string of Christmas lights. Should we extend the Light class?
To resolve this question, we should think about the relation between the new class and the one we're considering extending. If an instance of the new class is just like an instance of the old class, with a few modifications, we should extend. If an instance of the new class merely has an instance of the old class as a component, we should not. For example, a ColoredLight is a Light, but with the added feature of color, so extension is appropriate. On the other hand, an instance of BeetleGame merely has two beetles, so it is not appropriate for it to extend Beetle. Object-oriented programmers refer to these as is-a and has-a relationships.
A string of Christmas lights has several lights, so it should not extend Light. Instead, it should contain an array of Lights in a field. The LightString class is shown in Figure 3-8.
Figure 3-8. A LightString contains some Lights, but it is not a special kind of Light, so extension is not appropriate. The enhanced for loop used in lines 2123 and 2830 is explained in Appendix A.
1 /** A string of Lights, as used in Christmas decorating. */ 2 public class LightString { 3 4 /** The Lights in this LightString. */ 5 private Light[] bulbs; 6 7 /** Every other Light is a ColoredLight. */ 8 public LightString(int size) { 9 bulbs = new Light[size]; 10 for (int i = 0; i < size; i++) { 11 if (i % 2 == 0) { 12 bulbs[i] = new Light(); 13 } else { 14 bulbs[i] = new ColoredLight(); 15 } 16 } 17 } 1819 /** Turn all of the Lights in the LightString on or off. */ 20 public void setOn(boolean on) { 21 for (Light b : bulbs) { 22 b.setOn(on); 23 } 24 } 25 26 public String toString() { 27 String result = ""; 28 for (Light b : bulbs) { 29 result += b; 30 } 31 return result; 32 } 33 34 /** 35 * Create a LightString, print it, turn it on, and print it 36 * again. 37 */ 38 public static void main(String[] args) { 39 LightString lights = new LightString(20); 40 System.out.println(lights); 41 lights.setOn(true); 42 System.out.println(lights); 43 } 44 45 } |
Even though the LightString class has a method isOn(), it does not make sense to say this method overrides the one in Light, because LightString is not a subclass of Light.
The LightString class uses polymorphism to store both Lights and ColoredLights in the same array. The constructor uses the % operator to put Lights in even-numbered positions and ColoredLights in odd-numbered ones. (The first index is 0, which is even.) The keyword new is used both to allocate the array bulb and to create each individual object within that array.
When we run the LightString class, the output looks like this (colors will vary on each run):
.................... OROBOGOGOGOGOBOGOROR
The relationship between all four of the Light-related classes is shown in Figure 3-9.
Figure 3-9. A LightString contains 0 or more Lights, some of which may actually be ColoredLights or FlashingColoredLights.
(This item is displayed on page 76 in the print version)
Beginning object-oriented programmers often overuse inheritance. Inheritance should be used only when an instance of the subclass can stand in for an instance of the superclass. For example, suppose we have a class Bicycle with a method pedal(). We should not define Motorcycle to extend Bicycle, because the pedal() method wouldn't make sense for a Motorcycle. Since Bicycle is a polymorphic type, any method that accepts a Bicycle might receive an instance of a subclass of Bicycle instead. To prevent such code from breaking, an instance of any subclass should work in place of a regular Bicycle. We might reasonably extend Bicycle with ElectricBicycle (the kind that can be pedaled or powered with an electric motor), but not with Motorcycle.
Exercises
3.1 |
Draw a detailed UML class diagram showing the relationship between the Domineering and Cram classes, showing fields and methods. |
3.2 |
Recall your answer to Problem 2.29. Does Java use dynamic dispatch when deciding which version of an overloaded method to use? |
3.3 |
Discuss what it would mean for an interface to extend another interface. |
3.4 |
Discuss whether each pair below has an is-a or a has-a relationship. bicycle, vehicle bicycle, tire triangle, polygon rutabaga, vegetable person, bank account general, soldier |
3.5 |
Is it possible for an is-a relationship to be symmetric, so that every A is a B and vice versa? What about a has-a relationship? Explain. |
The Object Class |
Part I: Object-Oriented Programming
Encapsulation
Polymorphism
Inheritance
Part II: Linear Structures
Stacks and Queues
Array-Based Structures
Linked Structures
Part III: Algorithms
Analysis of Algorithms
Searching and Sorting
Recursion
Part IV: Trees and Sets
Trees
Sets
Part V: Advanced Topics
Advanced Linear Structures
Strings
Advanced Trees
Graphs
Memory Management
Out to the Disk
Part VI: Appendices
A. Review of Java
B. Unified Modeling Language
C. Summation Formulae
D. Further Reading
Index