CSCI A201/A597 and I210

Lecture Notes Twenty-Two

Second semester 2000-2001


Inheritance and the class extension mechanism.
You don't realize it, but you're constantly enjoying the benefits of science. For example, when you turn on the the radio, you take it for granted that music will come out;

But do you ever stop to think that this miracle would not be possible without the work of scientists? That's right: there are tiny scientists inside that radio, playing instruments.

A similar principle is used in automatic bank-teller machines, which is why they frequently say: "Sorry, out of service." They're too embarassed to say: "Sorry, tiny scientist going to the bathroom."

Speaking of banks, let's use the BankAccount class to study ... the concept of inheritance (in Java).

Inheritance is a mechanism for enhancing existing, working classes. If you need to implement a new class,

... and a class representing a more general concept is already available, .. then the new class can inherit from the existing class.

For example, suppose you need to define a class SavingsAccount ... to model an account that pays a fixed interest rate on deposits.

You already have a class BankAccount ... and a savings account is a special case of a bank account.

Ask any tiny scientist! So, in this case, it makes sense to use the language construct of inheritance.

Here is the syntax for the class definition:
class SavingsAccount extends BankAccount {
  <new methods>
  <new instance variables>   
} 
And the set union of features kicks in.

Exactly. In the SavingsAccount class definition you specify only new methods and instance variables.

All methods and instance variables of the BankAccount class ... are automatically inherited by the SavingsAccount class.

The more general class that forms the basis for inheritance is called the superclass. That would be BankAccount.

The more specialized class that inherits from the superclass is called the subclass. Here this is SavingsAccount.

In Java, every class that does not specifically extend another class, ... extends the class Object.

The Object class has a small number of methods that make sense for all objects, .. such as the toString method that you can use to obtain a string that describes the state of an object.

One important reason for inheritance is code reuse. By inheriting from an existing class, you do not have to replicate the effort that went into designing and perfecting that class.

For example, when implementing the SavingsAccount class, you can rely on the withdraw, deposit and getBalance methods of the BankAccount class without touching them. Let us see how our savings account objects are different from BankAccount objects.

We will set an interest rate in the constructor, ... and then we need a method to apply that interest periodically.

That is, in addition to the three methods that can be applied to every account, ... there is an additional method, addInterest.

These new methods and instance variables must be defined in the subclass. Here's the definition:
public class SavingsAccount extends BankAccount {

  double interestRate; 

  public SavingsAccount (double rate) {
    this.interestRate = rate; 
  } 

  public void addInterest() {
    double interest; 
    interest = this.getBalance() * this.interestRate / 100; 
    this.deposit(interest); 
  } 

} 

Given this definition, what is the structure of a SavingsAccount object? It inherits the balance instance variable from the BankAccount superclass, and it gains one additional instance variable:

... interestRate. Exactly.

Next we need to implement the new addInterest method. We have in fact, but we pretend not to, just to discuss it.

This method computes the interest due on the current balance, ... and deposits that interest to the account.

Note how the addInterest method calls the getBalance and deposit methods of the superclass. Let's draw a picture to illustrate what each type of object has and why:

The class SavingsAccount extends the class BankAccount. In other words, a SavingsAccount object is a special case of a BankAccount object.

A special case has more features. When I define a variable collegeFund of type SavingsAccount how do you anticipate using it?

Here are all possible uses: Very good.
collegeFund.deposit(___); 
collegeFund.withdraw(___); 
collegeFund.getBalance(); 
collegeFund.addInterest(); 

When I define a variable anAccount of type BankAccount how do you anticipate using it?
Here are all possible uses:
anAccount.deposit(___); 
anAccount.withdraw(___); 
anAccount.getBalance(); 

Very good. Can you then store a reference to a SavingsAccount object into an object variable of type BankAccount?

Like this?
BankAccount b = new SavingsAccount(10); 
Yes. Can you do that?

You would never utilize b fully, but perhaps you don't need that. So the answer is: yes. You can store the reference to a SavingsAccount object into an object variable of type BankAccount.

Indeed, because you just say: "I will not need the extra features that the class SavingsAccount is defining, ... I will just attempt to work with the features defined in BankAccount -- that's all I need in this particular case".

Can you do the opposite? You mean this?
SavingsAccount a = new BankAccount(___); 

Yes. The answer is: no.

Why? Well, what's the intended use of a?

I see, if I try to compute the added interest the object won't have an adequate instance variable, nor the capability to do that.
a.addInterest()
does not make sense.

So we can't let that happen. And the compiler will complain.

Why doesn't it complain in the previous situation? Because in that situation we are only giving up on some amenities,

... which is fine with the compiler for as long as it's fine with us, ... whereas here we might ask for the impossible,

.. which the compiler can't accept, ... even if it's fine with us.

a.addInterest()
is what it wants to guard as against.
So, going back to our original example,
SavingsAccount collegeFund = new SavingsAccount(10); 
BankAccount anAccount = collegeFund; 
Object anObject = collegeFund; 
... are all OK.

Now the three object references stored in collegeFund, anAccount, and anObject, ... all refer to the same object, of type SavingsAccount.

However, the object variable anAccount knows less than the full story aboutm the object to which it refers. Because anAccount is an object of type BankAccount, you can use the deposit and withdraw methods to change the balance of the savings account.

You cannot use the addInterest method, though -- it is not a method of the BankAccount superclass. And, of course, the variable anObject knows even less.

You can't even apply the deposit method to it. deposit is not a method of the Object class.

Why would anyone want to know less about an object and store a reference to it in a variable of the superclass's type? For generality and uniformity.

Have any example? I have two of them.

Let's see the first one. Consider the transfer method which transfers money from one account into another.
void transfer (BankAccount other, double amount) {
  this.withdraw(amount); 
  other.deposit(amount); 
} 
You can use this method to transfer money from one BankAccount to another,

... and you can also use the method to transfer money into a SavingsAccount. The transfer method expects a reference to a BankAccount, which it will use to deposit.

Any SavingsAccount object can do that too, so it can be passed as the first explicit argument to transfer. The transfer method doesn't actually know that, in this case, other refers to a SavingsAccount.

It knows only that other is a BankAccount, ... that is, that it can
  • deposit
  • withdraw, and
  • getBalance

... and it doesn't need to know anything else. Precisely.

What's the second example? It involves arrays, but we need to discuss inheritance hierarchies first.

Very good, let's do that. Occasionally, it happens that you convert an object to a superclass reference and you need to convert it back.

Suppose you captured a reference to a savings account in a variable of type Object: A variable reference is like a pair of binoculars.
Object myObj = new SavingsAccount(10); 

Or a pair of blinkers ... of the type that's used on skittish racehorses.

If you put it on, ... you can only see what it lets you see.

Much later, if you want to add interest or deposit to the account, ... you can do that, with care.

The object still has all the features, ... you just need to put the right pair of binoculars on to see them.

That's called casting. As long as you are absolutely sure that myObj really refers to a SavingsAccount object,

... you can use the cast notation to convert it back: What if you're sure but wrong?
SavingsAccount x = (SavingsAccount)myObj; 

If you are wrong, and the object doesn't actually refer to a savings account, your program will throw an exception, and terminate. You saw one example of using casts in graphics programs, when we had to cast the Graphics object to a Graphics2D object.

In real world, we often categorize concepts into hierarchies. Hierarchies are frequently represented as trees, with the most general concepts at the root of the hierarchy,

... and the more specialized ones towards the branches. Let's see an example in Java.

Suppose that we have more than just one extension to BankAccount. Consider a CheckingAccount class that describes accounts with no interest,

... gives you a small number of free transactions per month, ... and charges a transaction fee for each additional transaction.

All accounts have something in common. They are all bank accounts with a balance and the ability to deposit money,

... and (within limits) to withdraw money. This leads us to the following inheritance hierarchy:
Now suppose that you have 100 bank account objects, and half of them are checking accounts and the other half are

... savings accounts. Can you keep them all in an array?

Only if the array is declared as having an interest in only their most general, common features.
Like this:
BankAccount[] a = new BankAccount[100];

This strategy, in its most general form, is used by the Vector class. Which makes use of arrays of Objects.

To store something in a Vector it must be of type Object. So you can't store an int?

Not directly. But you can store a Rectangle

If you do, it will get stored as an Object. And when you retrieve it,
Vector v = new Vector();
v.addElement(new Rectangle(_,_,_,_));
(v.elementAt(0)).translate(_,_);
... it comes back as an Object not as a Rectangle.

Whatever you want to do with it as a Rectangle, ... you need to cast it to a Rectangle from the Object that it is.

Not casting will give you an error first time you try to use it as a Rectangle. In most situations of this kind, though, it is better to play it safe and test whether a cast will succeed,

... before carrying out the cast. For that purpose you use the instanceof operator.

That's right, it tests whether an object belongs to a particular class. For example, when retrieving one of our accounts from the array we could take the appropriate type of action depending on the type of account:
if (anObject instanceOf SavingsAccount) {
  // ... do savings account type of work 
} else if (anObject instanceOf CheckingAccount) {
  // ... do checking account type of work 
} else {
  // ... in which case the tiny scientist reports an error  
} 

Last updated: Mar 9, 2001 by Adrian German for A201