Session 18: Subclasses

Inheritance (Section 11.1)
    Developing a subclass
    Writing constructors
    Inheritance hierarchy
Overriding methods (Section 11.3)
Conversions (Section 11.4)
    Implicit conversion
    Explicit conversion
    The instanceof operator

Inheritance

Textbook: Section 11.1

Sometimes there are many different distinct types of a certain object. For example, a bank has many accounts, but some of them are savings accounts, and some are checking accounts. It also has many employees - some are tellers, while others are accountants, and others are vice-presidents, and others are janitors.

It's intuitive to have a SavingsAccount class and a CheckingAccount class, to represents accounts of each type. But both types of accounts have some common operations - for example, people deposit money to both, and people withdraw money from both. In general, we want to avoid duplicating code.

What we want is some way of expressing that both are Accounts, but with some additional features. To accomplish this, we might somehow define both SavingsAccount and CheckingAccount as subclasses of the Account class. This expresses that both are types of accounts, and that everything that applies to an Account should apply to a SavingsAccount also. We would say that SavingsAccount extends the Account class.

Developing a subclass

So we write the following.

public class SavingsAccount extends Account {
    private double interest;
    public SavingsAccount(double inter) {
        interest = inter;
    }
    public void addInterest() {
        deposit(getBalance() * interest);
    }
}
With this, a SavingsAccount inherits all the pieces of Account we've defined before. It's completely legitimate to write the following.
SavingsAccount mine = new SavingsAccount(0.02);
mine.deposit(1000.0); // deposit $1000 into account
mine.addInterest();
As a SavingsAccount, mine inherits the deposit() instance method from from SavingsAccount's superclass, Account. And of course, we defined the addInterest() instance method when we defined the SavingsAccount, so it gets that method also.

In memory, a SavingsAccount has two instance variables: the balance instance variable inherited from the Account class, and the interest instance variable defined directly in the SavingsAccount class. The balance variable is invisible to the SavingsAccount instance methods, since it is defined as private. But it still exists, so that the methods inherited from SavingsAccount (like deposit()) can work with it.

Writing constructors

A subclass inherits everything from its parent. Everything, that is, except the parent's constructor methods.

When I wrote the SavingsAccount, I defined a new constructor method for initializing the interest instance variable based on the constructor method parameter. What you didn't see is that the computer automatically called the default constructor method for the parent class, which would have initialized balance to 0.0.

When you create a new object (as in new SavingsAccount(0.02)), the computer actually goes through a four-step process.

  1. The computer allocates memory to hold all the instance variables associated with the new object type.

  2. If the object's class has a superclass, the computer calls the default constructor method for that superclass, to initialize any instance variables defined by the parent class.

  3. The computer calls the constructor method for the object's class, to initialize instance variables defined by the object's class. Any arguments listed in the parentheses are passed in as parameters to this constructor method.

  4. The computer returns, as the value of the new expression, the address of the allocated memory.

Ah, you say, but what if I don't want to use the default constructor of the superclass in Step 2? What if I have another superclass constructor method I want to use instead? Java has a way of indicating that: You add, as the first line of the constructor method, something that looks like a call to a super method.

public SavingsAccount(double inter, double initial) {
    super(initial);
    interest = inter;
}
In the parentheses after super is a list of the arguments that should be passed to the appropriate constructor method for the superclass. In this case, we're calling the Account constructor method that initializes the balance to a number (in this case, it's the number passed in as the second argument to the SavingsAccount constructor method).

Inheritance hierarchy

We can diagram the subclass relationship as a tree, like the following.

            Account
            /     \
SavingsAccount   CheckingAccount
This illustrates that both SavingsAccount and CheckingAccount are subclasses of the Account class.

For large programs, the relationship among classes creates quite a complex tree. You can (and do) have some subclasses of subclasses, or even subclasses of subclasses of subclasses.

                Person
                /    \
          Employee  Customer
         /       \
WagedEmployee     SalariedEmployee
   /       \        /        \
Janitor Teller Accountant VicePresident
This whole tree is called an inheritance hierarchy.

Overriding methods

Textbook: Section 11.3

Sometimes the method inherited from the parent class isn't exactly right for the subclass. For example, say we want a SavingsAccount subclass, InvestmentAccount, where the bank takes off a 3% commission on each deposit (which the customer accepts because they want the higher interest rate). In this case, we want to change the behavior of the account when somebody calls deposit(). We can do this by defining a new version of deposit() in the subclass. This overrides Account's deposit() method.

public class InvestmentAccount extends SavingsAccount {
    public static final double INTEREST = 0.05;
    public static final double COMMISSION = 0.03;

    public InvestmentAccount() {
        super(INTEREST);
    }

    public void deposit(double amount) {
        super.deposit(amount * (1.0 - COMMISSION));
    }
}
In our definition of the deposit() instance method, you can see something new: we've called deposit() on super - this indicates that the computer should call SavingsAccount's deposit() method - but you can see that the argument we pass it is only 97% of the argument passed into InvestmentAccount's deposit() method.

When a subclass overrides a method, the effect even applies to times the method is called in the parent class. For example, InvestmentAccount inherited from SavingsAccount the addInterest() method, defined as follows in SavingsAccount.

    public void addInterest() {
        deposit(getBalance() * (1.0 + interest));
    }
This method calls the deposit() method. Say I call addInterest() on an InvestmentAccount. You might think that this would call Account's deposit() method. It does not. It calls InvestmentAccount's deposit() method (which itself calls Account's deposit - after taking off the commission). The overall result is that the bank culls a commission even on the interest!

This behavior is not accidental - in fact, Java goes to great lengths to accomplish it. It's quite useful. In fact, often a class will define a method simply so that subclasses will override it. For example, the DrawableFrame class defines a keyTyped() method that gets called every time the user types a key. The method defined there does nothing. But a subclass can override the method to define new behavior.

You'll be using this in your next Breakout lab, as you'll want a user's keypress to alter the direction of the paddle. You'll do something like the following.

public class BreakoutFrame extends DrawableFrame {
    private Paddle paddle;
    public void keyTyped(char c) {
        // move paddle based on key typed
    }
}
Your own program will never actually call the keyTyped() method. But it's still useful to have, as the code within DrawableFrame calls it each time the user types a key.

Conversions

Textbook: Section 11.4

Implicit conversion

Like with primitive types, Java recognizes a hierarchy among the object types (classes). It will freely convert to a more general type (a parent class) when appropriate. For example, say we define the following class to represent a bank.

public class Bank {
    public static final int MAX_ACCOUNTS = 100;
    private Account[] accounts;
    private int num_accounts;

    public Bank() {
        accounts = new Account[MAX_ACCOUNTS];
        num_accounts = 0;
    }

    public void addAccount(Account acct) {
        accounts[num_accounts] = acct;
        ++num_accounts;
    }

    public Account getAccount(int index) {
        return accounts[index];
    }
}
Java has no problem with the following.
    Bank bank = new Bank();
    bank.addAccount(new SavingsAccount(0.02));
When we call addAccount(), the compiler wanted an Account as a parameter. It saw a SavingsAccount. But SavingsAccount is a subclass of Account, so it automatically converts SavingsAccount into an Account. (It's not actually converted at all - the Account still ``knows'' it's a SavingsAccount, and the interest rate is still saved with it. But, as far as Bank is concerned, it's just an Account.)

Incidentally, this feature of Java is called polymorphism - objects automatically change form as circumstances warrant.

Explicit conversion

But of course Java won't convert down the inheritance hierarchy.

    bank.getAccount(0).addInterest(); // won't compile
This won't compile, since getAccount(0) returns an Account, and as far as Java knows, it doesn't have a method called addInterest(). (It actually does, but the Java compiler can't verify that it does - all it knows is that getAccount() returns an Account.)

Sometimes you'll run into a case where you as the programmer are confident of what the object's type actually is because of some reasoning you've gone through. And you want to do something to it based on that type. In our example so far, for example, we're confident that the bank only holds SavingsAccounts, so we should be allowed to call addInterest().

In this case, you can use explicit conversion - using the casting operator we already saw for the primitive types.

    ((SavingsAccount) bank.getAccount(0)).addInterest(); // ok

The instanceof operator

What if you're not sure about an object has a specific type, but you want to do something if it has the type? This is what the instanceof operator is for: It tells you whether an object's actual type is what you think it is. For example, if we wanted to add any interest due to account 0, but we're not sure that it's a SavingsAccount, we could test whether it is first as follows.

    if(bank.getAccount(0) instanceof SavingsAccount) {
        ((SavingsAccount) bank.getAccount(0)).addInterest();
    }

The instanceof operator recognizes an object as being of a given type, even if that type is a superclass of the actual type of the object. If mine is an InvestmentAccount, the following are all true.

mine instanceof InvestmentAccount
mine instanceof SavingsAccount
mine instanceof Account
!(mine instanceof Bank)