Short Assignment 2 is due Monday.
We have already seen several of the key ideas of object-oriented programming. The most important one we have yet to see is inheritance.
Inheritance is a way to create new classes from existing classes. We call the existing class the superclass and the new class created from it the subclass. With inheritance, each object of the subclass "inherits" the instance variables and methods of the superclass. That way, we don't have to write these methods for the subclass unless we want to.
Of course, we'll want to write some methods in the subclass, for if we don't, the subclass is exactly the same as the superclass and there would have been no point in creating the subclass. Compared with objects of the superclass, objects of the subclass have additional specialization.
We used interfaces to provide polymorphism: the ability of an object reference to refer to more than one type of object. We will see that inheritance gives us another, very powerful, way to achieve polymorphism. So now we have two ways to achieve polymorphism: interfaces and inheritance.
I'll give you some generalities about superclasses and subclasses that seem abstract at the moment, but we'll see a little later how they apply to object-oriented programming by specific examples.
If there is one thing you should remember about inheritance, it is
Use inheritance to model "is-a" relationships and only "is-a" relationships.
What does this mean? Suppose we have two classes: A
is the superclass of B
(which is therefore the subclass). If we have used inheritance correctly, then
B
is also an object of class A
.B
is a subset of A
.A
, then it is also true of an object of class B
.B
are like objects of class A
, but with additional specialization.BankAccount
classFor our first example of inheritance, we will look at bank accounts. BankAccount.java is a basic bank account class. It has an instance variable balance
that holds the current balance. It has two constructors. The first creates a new account with a balance of 0.00. The second takes in an initialAmount
and uses that for the initial balance.
The BankAccount
class also has a set of useful methods:
public void deposit(double amount)
public void withdraw(double amount)
public double getBalance()
public void transfer(BankAccount other, double amount)
public String toString()
The deposit
, withdraw
, and getBalance
methods do what you would expect. The transfer
method transfers amount
from the current bank account (referenced by this
) to the bank account referenced by other
. The method toString
converts the information about the bank account to a String
. (As you recall, every object in Java has a toString
method defined for it, and System.out.println
uses toString
to print the value of the object.)
We have seen much more complicated examples of classes. Perhaps the most interesting characteristic of our BankAccount
class is the calls to withdraw
and deposit
within transfer
. We could have written this method differently:
public void transfer(BankAccount other, double amount) {
balance -= amount;
other.balance += amount;
}
(Notice that a method may refer to private instance variables of another object in the same class. This capability has nothing to do with inheritance, but it's good to remember that methods of a class can always access instance variables of any object in the class for which they have a reference, not just the object referenced by this
.)
But instead, we chose to write the transfer
method by having it call the withdraw
and deposit
methods. Why? If something changes about the way the withdrawals or deposits are done (for example, if we have the withdraw
method check for overdrawn accounts) then transfer
will automatically use the modifed methods. Abstraction works in our favor even within a class.
Banks offer many kinds accounts, though. A savings account pays periodic interest. A checking account pays no interest and may have transaction fees for deposits and withdrawals. A time deposit account may have an interest penalty for early withdrawal. We could write a separate class for each of these variants of bank accounts. We would reproduce a lot of code, however. All of these different accounts have a balance
, and they all have to deal with deposits, withdrawals, getting the account balance, transfers, and converting their contents to a String
. Much of the code will be identical or only slightly modified.
It is this situation that inheritance was designed to deal with. The goal is to "inherit" the instance variable balance
and the various methods. Methods can be used as is, if they already do the right thing. If not, they can be overridden by writing new versions of some of the methods for the new account types. These new versions, however, can call the old versions to help them out if they need to.
SavingsAccount
classOur first subclass is SavingsAccount
, defined in SavingsAccount.java. A savings account is just like a basic bank account, except that it pays interest. All the methods for the BankAccount
class work fine for the SavingsAccount
class. The SavingsAccount
class has to add an instance variable interestRate
and a method addPeriodicInterest
, but otherwise it is just a BankAccount
.
We indicate that SavingsAccount
is a subclass of BankAccount
by adding the words extends Bankaccount
at the end of the header of the class declaration. Because SavingsAccount
is a subclass of BankAccount
, SavingsAccount
will have copies of all the instance variables and methods of BankAccount
. It can then add the declaration for the new instance variable interestRate
and the new method addPeriodicInterest
. Thus, every SavingsAccount
object has two instance variables:
balance
, which is inherited from BankAccount
, andinterestRate
, which is specific to SavingsAccount
.A couple of complications arise, however. First, although every SavingsAccount
object has an instance variable balance
, because this instance variable is private to BankAccount
, no methods of SavingsAccount
are allowed to access it directly. This restriction may seem strange at first, and there is in fact a way to let a subclass have direct access to instance variables of the superclass. The trick is to use protected
access instead of private
access when declaring the instance variable in the superclass. We will discuss this idea later. But we can deal with private instance variables in the superclass in the same way that any other class would: access them indirectly via methods.
The addPeriodicInterest
method demonstrates this idea. The getBalance
and deposit
methods of BankAccount
give indirect access to balance
, and they are all that are needed to write addPeriodicInterest
. But how is it determined what methods getBalance
and deposit
refer to?
Let's look at deposit
. We know that the method call deposit
is really an abbreviation for this.deposit
. We first look in the SavingsAccount
class to see whether SavingsAccount
defines a deposit
method. It does not. And so we look into the superclass BankAccount
. Because we find a definition of deposit
there, we need look no further. If we had not found deposit
in the BankAccount
class, then we would look at BankAccount
's superclass, if there is one. Eventually, we either find a superclass that defines the deposit
method or we don't. If we do, we use the first superclass that defines the method. If we don't, then we have an error: we have called a method not defined for the class.
The constructors get a bit trickier. We cannot see (and in general may not even know the names of) the private instance variables in BankAccount
. How do we initialize them in our constructors?
It turns out that a constructor for the superclass (BankAccount
) will always be called in the subclass (SavingsAccount
) constructors. The only questions are which superclass constructor is called (recall that BankAccount
has two constructors), and whether the call is explicit or implicit.
The first constructor for SavingsAccount
has an implicit call of the superclass constructor. The only code appearing in the first constructor sets the instance variable interestRate
. Because there is no explicit call to the superclass constructor, the Java compiler automatically inserts a call to the superclass's default constructor—the one with no parameters—as the first line of the subclass constructor. In this case, balance
is initialized to 0.00 by the implicit call of the default constructor for BankAccount
, and then interestRate
is initialized by the constructor for SavingsAccount
.
The second SavingsAccount
constructor explicitly calls the superclass constructor by calling super(initialAmount)
as its first line. We'll see several uses of the reserved word super
in our bank account example. Here, we use it to call the one-parameter constructor of the superclass BankAccount
. When a subclass constructor explicitly calls its superclass's constructor, this call must be the first line of the subclass constructor.
Let's return to the "golden rule" business above. We have made SavingsAccount
a subclass of BankAccount
. Have we fulfilled all four relationships that we said must hold?
It is important to note that these relationships go only one way. For example, is every bank account also a savings account? No, it is not true. Checking accounts are bank accounts that are not savings accounts. Or we can ask if a property is true of a savings account, is it also true of a bank account? No, for we can add interest to savings accounts by calling addPeriodicInterest
, but we cannot add interest to checking accounts; a call of addPeriodicInterest
on a checking account would make no sense.
It may be a bit confusing at first when you realize that a subclass is a subset of its superclass, yet the subclass may contain more instance variables and methods than the superclass. (I know that it confused me at first.) After a while, you get more comfortable with it.
CheckingAccount
classThe CheckingAccount.java class again is similar to the basic BankAccount
class, but it incorporates transaction fees. In this implementation, the account owner gets FREE_TRANSACTIONS
free transactions per month (3). After that, the account owner must pay a TRANSACTION_FEE
for each additional transaction (50 cents). These fees are assessed at the end of each month. We have declared the constants FREE_TRANSACTIONS
and TRANSACTION_FEE
as static
and final
since they will never change (hence final
) and they should be the same for all checking accounts (hence static
).
Rather than worrying strictly about months, our implementation of the CheckingAccount
class just worries about some period of unspecified length. The instance variable transactionCount
counts the number of transactions that have occurred in the current period. To keep this variable up-to-date, we increment transactionCount
every time we make a deposit or a withdrawal. The methods deposit
and withdraw
that are inherited from the superclass BankAccount
do not handle incrementing transactionCount
. We must therefore override these methods by writing new versions of them.
We can describe the desired behavior of CheckingAccount
's withdraw
method as
Do what the normal
withdraw
inBankAccount
does, and then incrementtransactionCount
.
Our implementation of withdraw
in CheckingAccount
does so. The code
super.withdraw(amount);
says to call the withdraw
method of the superclass. (If we left off the super.
it would try to call this.withdraw
, which is the same method we are writing. We'd get a recursive call of the same method with the same parameters, which would cause infinite recursion. Big trouble.) Our implementation of the deposit
method is analogous.
The new method deductFees
takes care of transaction fees by computing the amount to be charged, withdrawing it from the account, and then setting transactionCount
back to 0. It performs the withdrawal by calling super.withdraw
. (In this case, just calling withdraw
would also work. It would increase the number of transactions, but that gets immediately set back to 0. It is safer to use withdraw
from the superclass, which does not deal with transaction fees.)
TimeDepositAccount
classOur last class in this hierarchy of bank accounts is the TimeDepositAccount
class in TimeDepositAccount.java. It is like the SavingsAccount
class, except it has to charge a penalty for early withdrawal. Therefore it is a subclass of SavingsAccount
. Note that it inherits the methods deposit
, withdraw
, getBalance
, transfer
, and toString
from BankAccount
. Why? The direct superclass of TimeDepositAccount
is SavingsAccount
, and these methods are not overridden in SavingsAccount
. Therefore, they come from the superclass of SavingsAccount
, which is BankAccount
. On the other hand, TimeDepositAccount
inherits the method addPeriodicInterest
from SavingsAccount
. For instance variables, TimeDepositAccount
inherits balance
from BankAccount
, and it inherits interestRate
from SavingsAccount
; because they are private
, neither of these inherited instance variables is directly accessible within methods of TimeDepositAccount
.
TimeDepositAccount
overrides both addPeriodicInterest
and withdraw
. In both cases, much of the work is done by calls to super.addPeriodicInterest
and super.withdraw
.
AccountTest.java exercises the bank account classes. It also shows that inheritance allows polymorphism and dynamic binding, as we are about to see. In this program, three accounts are created, one of each of our subclasses:
momsSavings
is a reference to a SavingsAccount
object.collegeFund
is a reference to a TimeDepositAccount
object.harrysChecking
is a reference to a CheckingAccount
object.Each of these objects is created using new
and constructors.
Let's examine the method calls this program makes.
First is the call
momsSavings.deposit(10000.00);
Because momsSavings
references a SavingsAccount
object, we first see whether SavingsAccount
defines a deposit
method. It doesn't, so we go to the SavingsAccount
's superclass, BankAccount
. That's where deposit
is defined. We conclude that this call of deposit
actually invokes the deposit
method defined in the BankAccount
class. We could use the debugger to confirm this observation.
Next is the call
momsSavings.transfer(harrysChecking, 2000);
Like deposit
, the method transfer
is not defined in SavingsAccount
, but it is defined in the superclass, BankAccount
. Thus, what is invoked is BankAccount
's transfer
method.
But there's something even more interesting going on in this call. The first formal parameter to transfer
—other
—is declared to be a reference to BankAccount
. But when we look at the corresponding actual parameter, it is harrysChecking
, which is a reference to CheckingAccount
. This is OK, because of what is called the subclass principle:
Any variable that is declared to be a reference to an object of a superclass may actually reference an object of a subclass.
This principle applies as well to the variable
this
within non-static methods.
In fact, we've been using the subclass principle already. When we called momsSavings.deposit
, the deposit
method expected this
to be a reference to a BankAccount
object, but it was actually a reference to a SavingsAccount
object—the subclass principle at work!
Let's continue examining the call of transfer
. It first calls withdraw
on the same object—the one that momsSavings
references—on which it was invoked. That part's easy. But then it calls deposit
on the object that other
references—and that's the CheckingAccount
object referenced by harrysChecking
. Here's where polymorphism and the related concept of dynamic binding come into play. Although the declaration of deposit
says that other
is a reference to a BankAccount
object, the way that transfer
was called, other
is actually a reference to a CheckingAccount
object. And so it is the deposit
method in the CheckingAccount
class that is actually invoked.
This example shows the idea of polymorphism and dynamic binding. We have multiple versions of a method, appearing in various places within an inheritance hierarchy. Regardless of what class a method call appears in, which method is actually called depends only on the class of the object it is called on.
The calls to harrysChecking.withdraw
are now straightforward: they call the withdraw
method of the CheckingAccount
class.
Then there are three calls to the static method endOfMonth
, and this method is overloaded. Version 1 takes a reference to a SavingsAccount
, and version 2 takes a reference to a CheckingAccount
. In the first call, the actual parameter is momsSavings
, a reference to SavingsAccount
, and so the version of endOfMonth
actually invoked is version 1. In the second call, the actual parameter is collegeFund
, a reference to TimeDepositAccount
. We have no overloaded version of endOfMonth
that takes a reference to TimeDepositAccount
, but version 1 takes a reference to TimeDepositAccount
's superclass SavingsAccount
. We apply the subclass principle, which says that it's OK to invoke version 1 and substitute a reference to TimeDepositAccount
in place of a reference to SavingsAccount
. Finally, in the third call, the actual parameter is harrysChecking
, a reference to CheckingAccount
, and version 2 of endOfMonth
is actually invoked.
We conclude by looking at the System.out.println
calls. The first one has the actual parameter "Mom's savings. " + momsSavings
. Here, the Java compiler calls momsSavings.toString
in order to convert momsSavings
to a String
that it can concatenate with "Mom's savings. "
. The only version of toString
that we have implemented in this class hierarchy is in BankAccount
, and so that is the method invoked. The same holds for all the other implicit calls to toString
that occur in the System.out.println
calls in this example.
Although dynamic binding and overloading seem similar, there is an important difference between the two concepts. The program AccountTest2.java illustrates the difference. It's the same as AccountTest.java, but with six lines changed:
The references momsSavings
, collegeFund
, and harrysChecking
are all declared as references to the superclass BankAccount
. The objects created are as exactly as in AccountTest.java, however. momsSavings
actually references a SavingsAccount
object, collegeFund
actually references a TimeDepositAccount
object, and harrysChecking
actually references a CheckingAccount
object. This is OK, because of the subclass principle. Notice that we have polymorphism here: variables declared as a reference to one type (BankAccount
) actually reference objects of some other type.
In the three calls to endOfMonth
, the parameters all have to be cast. That is because each one is declared as a reference to BankAccount
, and there is no version of endOfMonth
that takes a reference to BankAccount
. By casting, we make it clear what type of object each parameter references, so that the appropriate version of endOfMonth
gets called.
Something seems fishy here, doesn't it? Why is it OK to have momsSavings
, collegeFund
, and harrysChecking
all declared as references to BankAccount
and the calls to deposit
, transfer
, and withdraw
work but the calls to endOfMonth
require us to cast?
The difference is in how the calls are made. When we call deposit
, transfer
, or withdraw
, the object reference is to the left of the dot, e.g., harrysChecking.withraw(200)
. When the reference is to the left of the dot, then and only then do we get dynamic binding. In other words, when the reference is to the left of the dot, the decision as to which method is actually called occurs at run time. When we call a non-static method but we don't supply a reference and a dot, then the method is called on this
, and we still decide which method is called based on the type of the object that this
references.
On the other hand, observe that in the calls to endOfMonth
, the object reference is not to the left of the dot. Instead, it's a parameter. In this case, no dynamic binding occurs. The decision as to which method is called is made at compile time, and it is based on how the reference is declared. The decision has nothing to do with what kind of objects the references really refer to! We call this compile-time decision static binding. In AccountTest2.java, momsSavings
, collegeFund
, and harrysChecking
are all declared as references to BankAccount
, and there is no version of endOfMonth
that takes BankAccount
, or any superclass of BankAccount
, as a parameter. Thus, we must cast each of these references to the appropriate class so that the compiler can make the proper decision at compile time.