Using Subclasses and Inheritance
As you find out in Book III, Chapter 1, a Java class can be based on another class. Then the class becomes like a child to the parent class: It inherits all the characteristics of the parent class, good and bad. All the fields and methods of the parent class are passed on to the child class. The child class can use these fields or methods as is, or it can override them to provide its own versions. In addition, the child class can add fields or methods of its own.
In this chapter, you discover how this magic works, along with the basics of creating and using Java classes that inherit other classes. You also find out a few fancy tricks that help you get the most out of inheritance.
Introducing Inheritance
The word inheritance conjures up several different non-computer meanings:
- Children inherit certain characteristics from the parents. For example, two of my three children have red hair. (Hopefully, they won't be half bald by the time they're 30.)
- Children can also inherit behavior from their parents. As they say, the apple doesn't fall far from the tree.
- When someone dies, their heirs get their stuff. Some of it is good stuff, but some of it may not be. My kids are going to have a great time rummaging through my garage deciding who gets what.
- You can inherit rights as well as possessions. For example, you may be a citizen of a country by virtue of being born to parents who are citizens of that country.
In Java, inheritance refers to a feature of object-oriented programming that lets you create classes that are derived from other classes. A class that's based on another class is said to inherit the other class. The class that is inherited is called the parent class, the base class, or the superclass. The class that does the inheriting is called the child class, the derived class, or the subclass.
Tip |
The terms subclass and superclass seem to be the preferred terms among Java gurus. So if you want to look like you know what you're talking about, use these terms. Also, be aware that the term subclass can be used as a verb. For example, when you create a subclass that inherits a base class, you are subclassing the base class. |
You need to know a few important things about inheritance:
- A derived class automatically takes on all the behavior and attributes of its base class. Thus, if you need to create several different classes to describe types that aren't identical but have many features in common, you can create a base class that defines all the common features. Then you can create several derived classes that inherit the common features.
- A derived class can add features to the base class it inherits by defining its own methods and fields. This is one way a derived class distinguishes itself from its base class.
- A derived class can also change the behavior provided by the base class. For example, a base class may provide that all classes derived from it have a method named play, but each class is free to provide its own implementation of the play method. In this case, all classes that extend the base class provide their own implementation of the play method.
-
Tip Inheritance is best used to implement is-a-type-of relationships. For example: Solitaire is a type of game; a truck is a type of vehicle; an invoice is a type of transaction. In each case, a particular kind of object is a specific type of a more general category of objects.
The following sections provide some examples that help illustrate these points.
Motorcycles, trains, and automobiles
Inheritance is often explained in terms of real-world objects such as cars and motorcycles, birds and reptiles, or other familiar real-world objects. For example, consider various types of vehicles. Cars and motorcycles are two distinct types of vehicles. If you're writing software that represents vehicles, you could start by creating a class called Vehicle that would describe the features that are common to all types of vehicles-such as wheels, a driver, the ability to carry passengers, and the ability to perform actions such as driving, stopping, turning, or crashing.
A motorcycle is a type of vehicle that further refines the Vehicle class. The Motorcycle class would inherit the Vehicle class, so it would have wheels, a driver, possibly passengers, and the ability to drive, stop, turn, or crash. In addition, it would have features that differentiate it from other types of vehicles. For example, it has two wheels and uses handlebars for steering control.
A car is also a type of vehicle. The Car class would inherit the Vehicle class, so it too would have wheels, a driver, possibly some passengers, and the ability to drive, stop, turn, or crash. Plus it would have some features of its own, such as having four wheels and a steering wheel, seat belts and air bags, and an optional automatic transmission.
Playing games
Because you'll unlikely ever actually write a program that simulates cars, motorcycles, or other vehicles, take a look at a more common example: games. Suppose you want to develop a series of board games such as Life, Sorry!, or Monopoly. Most board games have certain features in common:
- They have a playing board that has locations that players can occupy.
- They have players that are represented by objects.
- The game is played by each player taking a turn, one after the other. Once the game starts, it keeps going until someone wins. (If you don't believe me, ask the kids who tried to stop a game of Jumanji before someone won.)
Each specific type of game has these basic characteristics, but adds features of its own. For example, Life adds features such as money, insurance policies, spouses and children, and a fancy spinner in the middle of the board. Sorry! has cards you draw to determine each move and safety zones within which other players can't attack you. And Monopoly has Chance and Community Chest cards, properties, houses, hotels, and money.
If you were designing classes for these games, you might create a generic BoardGame class that defines the basic features common to all board games, and then use it as the base class for classes that represent specific board games, such as LifeGame, SorryGame, and MonopolyGame.
A businesslike example
If vehicles or games don't make it clear enough, here's an example from the world of business. Suppose you're designing a payroll system and you're working on the classes that represent the employees. You realize that the payroll basically includes two types of employees: salaried employees and hourly employees. So you decide to create two classes, sensibly named SalariedEmployee and HourlyEmployee.
You quickly discover that most of the work done by these two classes is identical. Both types of employees have names, addresses, Social Security numbers, totals for how much they've been paid for the year, how much tax has been withheld, and so on.
However, they have important differences. The most obvious is that the salaried employees have an annual salary and the hourly employees have an hourly pay rate. But there are other differences as well. For example, hourly employees have a schedule that changes week to week. And salaried employees may have a benefit plan that isn't offered to hourly employees.
So you decide to create three classes instead of just two. A class named Employee handles all the features that are common to both types of employees. Then this class is the base class for the SalariedEmployee and HourlyEmployee classes. These classes provide the additional features that distinguish salaried employees from hourly employees.
Inheritance hierarchies
One of the most important aspects of inheritance is that a class derived from a base class can (in turn) be used as the base class for another derived class. Thus you can use inheritance to form a hierarchy of classes.
For example, you've already seen how an Employee class can be used as a base class to create two types of subclasses: a SalariedEmployee class for salaried employees and an HourlyEmployee class for hourly employees. Suppose that salaried employees fall into two categories: management and sales. Then you could use the SalariedEmployee class as the base class for two more classes: Manager and SalesPerson.
Thus a Manager is a type of SalariedEmployee. And because a SalariedEmployee is a type of Employee, a Manager is also a type of Employee.
TECHNICAL STAUFF |
All classes ultimately derive from a Java class named Object. Any class that doesn't specifically state what class it is derived from is assumed to derive from the Object class. This class provides some of the basic features that are common to all Java classes, such as the toString method. For more information, see Book III, Chapter 5. |
Creating Subclasses
The basic procedure for creating a subclass is simple. You just use the extends keyword on the declaration for the subclass. The basic format of a class declaration for a class that inherits a base class is this:
public class ClassName extends BaseClass { // class body goes here }
For example, suppose you have a class named Ball that defines a basic ball, and you want to create a subclass named BouncingBall that adds the ability to bounce.
public class BouncingBall extends Ball { // methods and fields that add the ability to bounce // to a basic Ball object: public void bounce() { // the bounce method } }
Here I'm creating a class named BouncingBall that extends the Ball class. (Extends is Java's word for inherits.)
The subclass automatically has all the methods and fields of the class it extends. Thus, if the Ball class has fields named size and weight, the BouncingBall class has those fields too. Likewise, if the Ball class has a method named throw, the BouncingBall class gets that method too.
DESIGN PATTERN |
The Delegation pattern Inheritance is one of the great features of object-oriented programming languages, such as Java. However, it isn't the answer to every programming problem. And, quite frankly, many Java programmers use it too much. In many cases, simply including an instance of one class in another class rather than using inheritance is easier. This technique is sometimes called the Delegation Design pattern. For example, suppose you need to create a class named EmployeeCollection that represents a group of employees. One way to create this class would be to extend one of the collection classes supplied by the Java API, such as the ArrayList class. Then your EmployeeCollection class would be a specialized version of the ArrayList class, and would have all the methods that are available to the ArrayList class. However, a simpler alternative would be to declare a class field of type ArrayList within your EmployeeCollection class. Then you could provide methods that use this ArrayList object to add or retrieve employees from the collection. Why is this technique called the delegation? Because-rather than write code that implements the functions of the collection-you delegate that task to an ArrayList object, because ArrayList objects already know how to perform these functions. (For more information about the ArrayList class, see Book IV, Chapter 3.) |
You need to know some important details to use inheritance properly:
-
REMEMBER A subclass inherits all the members from its base class. However, constructors are not considered to be members. As a result, a subclass does not inherit constructors from its base class.
- The visibility (public or private) of any members inherited from the base class is the same in the subclass. That means that you can't access methods or fields that are declared in the base class as private from the subclass.
- You can override a method by declaring a new member with the same signature in the subclass. For more information, see the next section.
- A special type of visibility is called protected that hides fields and methods from other classes but makes them available to subclasses. For more information, see "Protecting Your Members" later in this chapter.
- You can also add more methods or fields, private or protected, to a subclass. For example, the BouncingBall class shown previously in this section adds a public method named bounce.
Overriding Methods
If a subclass declares a method that has the same signature as a public method of the base class, the subclass version of the method overrides the base class version of the method. This technique lets you modify the behavior of a base class to suit the needs of the subclass.
For example, suppose you have a base class named Game that has a method named play. The base class, which doesn't represent any particular game, implements this method:
public class Game { public void play() { } }
Then you declare a class named Chess that extends the Game class-but also provides an implementation for the play method:
public class Chess extends Game { public void play() { System.out.println("I give up. You win."); } }
Here, when you call the play method of a Chess object, the game announces that it gives up. (I was going to provide a complete implementation of an actual chess-game program for this example, but it would have made this chapter about 600 pages long. So I opted for the simpler version here.)
Note that to override a method, three conditions have to be met:
- The class must extend the class that defines the method you want to override.
- The method must be declared in the base class with public access. You can't override a private method.
- The method in the subclass must have the same signature as the method in the base class. In other words, the name of the method and the parameter types must be the same.
Protecting Your Members
You're already familiar with the public and private keywords, used to indicate whether class members are visible outside of the class or not. When you inherit a class, all the public members of the superclass are available to the subclass, but the private members aren't. They do become a part of the derived class, but you can't access them directly in the derived class.
Java provides a third visibility option that's useful when you create subclasses: protected. A member with protected visibility is available to subclasses, but not to other classes. For example, consider this example:
public class Ball { private double weight; protected double getWeight() { return this.weight; } protected void setWeight(double weight) { this.weight = weight; } } public class BaseBall extends Ball { public BaseBall() { setWeight(5.125); } }
Here the getWeight and setWeight methods are declared with protect access, which means they're visible in the subclass BaseBall. However, these methods aren't visible to classes that don't extend Ball.
Using this and super in Your Subclasses
You already know about the this keyword: It provides a way to refer to the current object instance. It's often used to distinguish between a local variable or a parameter and a class field with the same name. For example:
public class Ball { private int velocity; public void setVelocity(int velocity) { this.velocity = velocity; } }
Here the this keyword indicates that the velocity variable referred to on the left side of the assignment statement is the class field named velocity, not the parameter with the same name.
But what if you need to refer to a field or method that belongs to a base class? To do that, you use the super keyword. It works similar to this, but refers to the instance of the base class rather than the instance of the current class.
For example, consider these two classes:
public class Ball { public void hit() { System.out.println("You hit it a mile!"); } } class BaseBall extends Ball { public void hit() { System.out.println("You tore the cover off! "); super.hit(); } }
Here the hit method in the BaseBall class calls the hit method of its base class object. Thus, if you call the hit method of a BaseBall object, the following two lines are displayed on the console:
You tore the cover off! You hit it a mile!
You can also use the super keyword in the constructor of a subclass to explicitly call a constructor of the superclass. For more information, see the next section.
Inheritance and Constructors
When you create an instance of a subclass, Java automatically calls the default constructor of the base class before it executes the subclass constructor. For example, consider the following classes:
public class Ball { public Ball() { System.out.println( "Hello from the Ball constructor"); } } public class BaseBall extends Ball { public BaseBall() { System.out.println( "Hello from the BaseBall constructor"); } }
If you create an instance of the BaseBall class, the following two lines are displayed on the console:
Hello from the Ball constructor Hello from the BaseBall constructor
If you want, you can explicitly call a base-class constructor from a subclass by using the super keyword. Because Java automatically calls the default constructor for you, the only reason to do this is to call a constructor of the base class that uses a parameter. For example, here's a version of the Ball and BaseBall classes in which the BaseBall constructor calls a Ball constructor that uses a parameter:
public class Ball { private double weight; public Ball(double weight) { this.weight = weight; } } public class BaseBall extends Ball { public BaseBall() { super(5.125); } }
Here the BaseBall constructor calls the Ball constructor to supply a default weight for the ball.
REMEMBER |
You need to obey a few rules and regulations when working with superclass constructors:
|
Using final
Java has a final keyword that serves three purposes. When you use final with a variable, it creates a constant whose value can't be changed once it has been initialized. Constants are covered in Book II, Chapter 2, so I won't describe this use of the final keyword more here. The other two uses of the final keyword are to create final methods and final classes. I describe these two uses of final in the following sections.
Final methods
A final method is a method that can't be overridden by a subclass. To create a final method, you simply add the keyword final to the method declaration. For example:
public class SpaceShip { public final int getVelocity() { return this.velocity; } }
Here the method getVelocity is declared as final. Thus any class that uses the SpaceShip class as a base class can't override the getVelocity method. If it tries, the compiler issues an error message.
Here are some additional details about final methods:
- You might think that a subclass wouldn't need to override a method, but there's no reason to be sure of that. Predicting how other people might use your class is difficult. As a result, you should usually avoid using final methods unless you have a compelling reason to.
-
TECHNICAL STAUFF Final methods execute more efficiently than non-final methods. That's because the compiler knows at compile time that a call to a final method won't be overridden by some other method. The performance gain isn't huge, but for applications where performance is crucial, it can be noticeable.
- Private methods are automatically considered to be final. That's because you can't override a method you can't see.
Final classes
A final class is a class that can't be used as a base class. To declare a class as final, just add the final keyword to the class declaration:
public final class BaseBall { // members for the BaseBall class go here }
Then no one can use the BaseBall class as the base class for another class.
When you declare a class to be final, all of its methods are considered to be final as well. That makes sense when you think about it. Because you can't use a final class as the base class for another class, no class can possibly be in a position to override any of the methods in the final class. Thus all the methods of a final class are final methods.
Casting Up and Down
An object of a derived type can be treated as if it were an object of its base type. For example, if the BaseBall class extends the Ball class, a BaseBall object can be treated as if it were a Ball object. This is called upcasting, and Java does it automatically, so you don't have to code a casting operator. Thus the following code is legal:
Ball b = new BaseBall();
Here an object of type BaseBall is created. Then a reference to this object is assigned to the variable b, whose type is Ball, not BaseBall.
Now suppose you have a method in a ballgame application named hit that's declared like this:
public void hit(Ball b)
In other words, this method accepts a Ball type as a parameter. When you call this method, you can pass it either a Ball object or a BaseBall object, because BaseBall is a subclass of Ball. So the following code works:
BaseBall b1 = new BaseBall(); hit(b1); Ball b2 = b1; hit(b2);
Warning |
Automatic casting doesn't work the other way, however. Thus you can't use a Ball object where a BaseBall object is called for. For example, suppose your program has a method declared like this: public void toss(BaseBall b) |
Then the following code does not compile:
Ball b = new BaseBall(); toss(b); // error: won't compile
However, you can explicitly cast the b variable to a BaseBall object, like this:
Ball b = new BaseBall(); toss((BaseBall) b);
Note that the second statement throws an exception of type ClassCastException if the object referenced by the b variable isn't a BaseBall object. So the following code won't work:
Ball b = new SoftBall(); toss((BaseBall) b); // error: b isn't a Softball
Tip |
What if you want to call a method that's defined by a subclass from an object that's referenced by a variable of the superclass? For example, suppose the SoftBall class has a method named riseBall that isn't defined by the Ball class. How can you call it from a Ball variable? One way to do that is to create a variable of the subclass, and then use an assignment statement to cast the object: Ball b = new SoftBall(); SoftBall s = (SoftBall)b; // cast the Ball to a // SoftBall s.riseBall(); |
But there's a better way: Java lets you cast the Ball object to a SoftBall and call the riseBall method in the same statement. All you need is an extra set of parentheses:
Ball b = new SoftBall(); ((SoftBall) b).riseBall();
Here the expression ((SoftBall) b) returns the object referenced by the b variable, cast as a SoftBall. You can then call any method of the SoftBall class using the dot operator. (This operator throws a ClassCastException if b is not a SoftBall object.)
Tip |
As a general rule, you should declare method parameters with types as far up in the class hierarchy as possible. For example, rather than create separate toss methods that accept BaseBall and SoftBall objects, create a single toss method that accepts a Ball object. If necessary, the toss method can determine which type of ball it's throwing by using the instanceof operator, which is described in the next section. |
Determining an Object s Type
As described in the previous section, a variable of one type can possibly hold a reference to an object of another type. For example, if SalariedEmployee is a subclass of the Employee class, the following statement is perfectly legal:
Employee emp = new SalariedEmployee();
Here the type of the emp variable is Employee, but the object it refers to is a SalariedEmployee.
Suppose you have a method named getEmployee whose return type is Employee, but that actually returns either a SalariedEmployee or an HourlyEmployee object:
Employee emp = getEmployee();
In many cases, you don't need to worry about which type of employee this method returns. But sometimes you do. For example, suppose the SalariedEmployee class extends the Employee class by adding a method named getFormattedSalary, which returns the employee's salary formatted as currency. Similarly, the HourlyEmployee class extends the Employee class with a getFormattedRate method that returns the employee's hourly pay rate formatted as currency. Then you'd need to know which type of employee a particular object is to know whether you should call the getFormattedSalary method or the getFormattedRate method to get the employee's pay.
To tell what type of object has been assigned to the emp variable, you can use the instanceof operator, which is designed specifically for this purpose. Here's the previous code rewritten with the instanceof operator:
Employee emp = getEmployee(); String msg; if (emp instanceof SalariedEmployee) { msg = "The employee's salary is "; msg += ((SalariedEmployee) emp).getFormattedSalary(); } else { msg = "The employee's hourly rate is "; msg += ((HourlyEmployee) emp).getFormattedRate(); } System.out.println(msg);
Here the instanceof operator is used in an if statement to determine the type of the object returned by the getEmployee method. Then the emp can be cast without fear of CastClassException.
Poly What?
The term polymorphism refers to the ability of Java to use base-class variables to refer to subclass objects, to keep track of which subclass an object belongs to, and to use overridden methods of the subclass, even though the subclass isn't known when the program is compiled.
This sounds like a mouthful, but it's not that hard to understand when you see an example. Suppose you're developing an application that can play the venerable game of Tic-Tac-Toe. You start out by creating a class named Player that represents one of the players. This class has a public method named move that returns an int to indicate which square of the board the player wants to mark:
class Player { public int move() { for (int i = 0; i < 9; i++) { System.out.println( " The basic player says:"); System.out.println( "I'll take the first open square!"); return firstOpenSquare(); } return -1; } private int firstOpenSquare() { int square = 0; // code to find the first open square goes here return square; } }
This basic version of the Player class uses a simple strategy to determine what its next move should be: It chooses the first open square on the board. This strategy stokes your ego by letting you think you can beat the computer every time. (To keep the illustration simple, I omitted the code that actually chooses the move.)
Now, you need to create a subclass of the Player class that uses a more intelligent method to choose its next move:
class BetterPlayer extends Player { public int move() { System.out.println(" The better player says:"); System.out.println( I'm looking for a good move..."); return findBestMove(); } private int findBestMove() { int square; // code to find the best move goes here return square; } }
As you can see, this version of the Player class overrides the move method and uses a better algorithm to pick its move. (Again, to keep the illustration simple, I don't show the code that actually chooses the move.)
The next thing to do is write a short class that uses these two Player classes to actually play a game. This class contains a method named playTheGame that accepts two Player objects. It calls the move method of the first player, and then calls the move method of the second player:
public class TicTacToeApp { public static void main(String[] args) { Player p1 = new Player(); Player p2 = new BetterPlayer(); playTheGame(p1, p2); } public static void playTheGame(Player p1, Player p2) { p1.move(); p2.move(); } }
Notice that the playTheGame method doesn't know which of the two players is the basic player and which is the better player. It simply calls the move method for each Player object.
When you run this program, the following output is displayed on the console:
Basic player says: I'll take the first open square! Better player says: I'm looking for a good move. . .
When the move method for p1 is called, the move method of the Player class is executed. But when the move method for p2 is called, the move method of the BetterPlayer class is called.
TECHNICAL STAUFF |
Java knows to call the move method of the BetterPlayer subclass because it uses a technique called late binding. Late binding simply means that when the compiler can't tell for sure what type of object a variable references, it doesn't hard-wire the method calls when the program is compiled. Instead, it waits until the program is executing to determine exactly which method to call. |
Creating Custom Exceptions
The last topic I want to cover in this chapter is how to use inheritance to create your own custom exceptions. I covered most of the details of working with exceptions in Book II, Chapter 8. However, I hadn't explored inheritance, so I couldn't discuss custom exception classes in that chapter. So I promised I'd get to it in this minibook. The following sections deliver on that long-awaited promise.
The Throwable hierarchy
As you know, you use the try/catch statement to catch exceptions, and the throw statement to throw exceptions. Each type of exception that can be caught or thrown is represented by a different exception class. What you might not have realized is that those exception classes use a fairly complex inheritance chain, as shown in Figure 4-1.
Figure 4-1: The hierarchy of exception classes.
The following paragraphs describe each of the classes in this hierarchy:
- Throwable: The root of the exception hierarchy is the Throwable class. This class represents any object that can be thrown with a throw statement and caught with a catch clause.
- Error: This subclass of Throwable represents serious error conditions that reasonable programs can't recover from. The subclasses of this class represent the specific types of errors that can occur. For example, if the Virtual Machine runs out of memory, a VirtualMachineError is thrown. You don't have to worry about catching these errors in your programs.
- Exception: This subclass of Throwable represents an error condition that most programs should try to recover from. Thus Exception is effectively the top of the hierarchy for the types of exceptions you catch with try/catch statements.
With the exception (sorry) of RuntimeException, the subclasses of Exception represent specific types of checked exceptions that must be either caught or thrown. Note that some of these subclasses have subclasses of their own. For example, the exception class named IOException has more than 25 subclasses representing different kinds of I/O exceptions that can occur.
- RuntimeException: This subclass of Exception represents unchecked exceptions. You don't have to catch or throw unchecked exceptions, but you can if you want to. Subclasses of RuntimeException include NullPointerException and ArithmeticException.
If your application needs to throw a custom exception, you can create an exception class that inherits any of the classes in this hierarchy. Usually, however, you start with the Exception class to create a custom checked exception. The next section explains how to do that.
Creating an exception class
To create a custom exception class, you just define a class that extends one of the classes in the Java exception hierarchy. Usually you extend Exception to create a custom checked exception.
For example, suppose you're developing a class that retrieves product data from a file or database, and you want methods that encounter I/O errors to throw a custom exception rather than the generic IOException that's provided in the Java API. You can do that by creating a class that extends the Exception class:
public class ProductDataException extends Exception { }
Unfortunately, constructors aren't considered to be class members, so they aren't inherited when you extend a class. As a result, the ProductDataException has only a default constructor. The Exception class itself, and most other exception classes, have a constructor that lets you pass a String message that's stored with the exception and can be retrieved via the getMessage method. Thus you want to add this constructor to your class. That means you want to add an explicit default constructor too. So the ProductDataException class now looks like this:
public class ProductDataException extends Exception { public ProductDataException { } public ProductDataException(String message) { super(message); } }
Although possible, adding additional fields or methods to a custom exception class is unusual.
Throwing a custom exception
As for any exception, you use a throw statement to throw a custom exception. You usually code this throw statement in the midst of a catch clause that catches some other more generic exception. For example, here's a method that retrieves product data from a file and throws a ProductDataException if an IOException occurs:
public class ProductDDB { public static Product getProduct(String code) throws ProductDataException { try { Product p; // code that gets the product from a file // and might throw an IOException p = new Product(); return p; } catch (IOException e) { throw new ProductDataException( "An IO error occurred."); } } }
Here's some code that calls the getProduct method and catches the exception:
try { Product p = ProductDB.getProduct(productCode); } catch (ProductDataException e) { System.out.println(e.getMessage()); }
Here the message is simply displayed on the console if a ProductDataException is thrown. In an actual program, you want to log the error, inform the user, and figure out how to gracefully continue the program even though this data exception has occurred.