Second Summer 2002


Lecture Notes Twenty-Two: 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 and ATMs, let's use the BankAccount class to study the class extension mechanism, or 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 BankAccount.

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. I see... Concatenation of blueprints (almost).

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.

Whoa!... that explains everything! Yes. It's been a well kept secret until now.

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, any object.

I remember the other day we had this code.
class Vehicle {
  String owner; 
  Vehicle (String owner) { 
    this.owner = owner; 
  }
  public String toString() {
    return "I belong to: " + this.owner; 
  } 
  public static void main(String[] args) {
    Vehicle a = new Vehicle("Michael Jordan");
    System.out.println(a.toString()); 
  } 
} 
And we asked two questions about it.

First off, if you run it now, no mistery. Yes. All's copacetic now. But do this:
class Vehicle {
  String owner; 
  Vehicle (String owner) { 
    this.owner = owner; 
  }
  public String toString() {
    return "I belong to: " + this.owner; 
  } 
  public static void main(String[] args) {
    Vehicle a = new Vehicle("Michael Jordan");
    System.out.println(a           ); 
  } 
} 
Yes, that's the minor mistery. The toString is invoked by default.

Very good. Now do this:
class Vehicle {
  String owner; 
  Vehicle (String owner) { 
    this.owner = owner; 
  }



  public static void main(String[] args) {
    Vehicle a = new Vehicle("Michael Jordan");
    System.out.println(a.toString()); 
  } 
} 
Yes, that's the major mistery. A toString is already there from Object.

Doesn't work that well, but it's there. And Object is responsible for providing it.

That's called inheritance. It's a side-effect of the class extension mechanism (for efficiency of expression).

When you extend a set of features provide the name of the set your starting from (extends) followed by the list of features you're adding. 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.

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 BankAccount... ... we now have an additional method, addInterest which will only work for the new type of SavingsAccounts.

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 (as far as fields go)? It inherits the balance instance variable from the BankAccount superclass, and it gains one additional instance variable.

Which is... interestRate. Exactly.

It's nice to be able to develop things in stages. Yes, we will have a longer example later.

Next we need to implement the new
public void addInterest()
instance method.
We have in fact already implemented it, but we pretend not to have done, just to discuss it.

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

Note how the addInterest() method calls the getBalance() and deposit() methods of the superclass (BankAccount). Hot ziggity, you're right!
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); 
  } 

} 
Thanks for emphasizing, I would have missed it.
You're welcome.

Let's now draw a picture to illustrate... ... what each type of object has and why:
Boy, that looks good! I sure think so.

The class SavingsAccount extends the class BankAccount (which extends Object... ... by default). A SavingsAccount object is a special case of BankAccount, just as a BankAccount is a special kind of 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(); 
One less.

Very good. Get ready for a subtle question now.

Hit me. OK, here it goes.


Can you 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

And the reason is that you're just saying:

"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" Almost like casting a double to an int.

Almost but not exactly. Yes, because the new feature is (still) there.

OK. 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'd say: if I try to compute the added interest the object won't have an adequate instance variable, nor the capability to do that. That is,
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.

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

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

However, the object variable anAccount knows less than the full story about the object to which it refers. (The object only knows the truth). Because anAccount is a variable of type BankAccount, you can use it to refer to the deposit and withdraw methods used to change the balance of the actual SavingsAccount object.

You can't use the addInterest method, though. It is not a method of the BankAccount superclass.

You can't see it when you decide to ignore it. Exactly. 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 (or care, for that matter) that, in this case, other refers to an actual 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.

I liked your first example. Thank you. I liked it too.

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 then, later, 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, like this: 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 will see examples of casting soon now.

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.

I think I get that. 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.

I can visualize that. Good. 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: A simple, very basic class hierarchy.
Now suppose that you have 100 bank account objects, and half of them are checking accounts and the other half are...

... savings accounts? That's plausible. Can you keep them all in an array?

Only if the array is declared as having an interest in (being concerned with describing) 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. I mean: that's it! 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 and not as a Rectangle. So you will need to cast the reference to a Rectangle or it won't work.
Vector v = new Vector();
v.addElement(new Rectangle(_,_,_,_));
((Rectangle)(v.elementAt(0))).translate(_,_);

Whatever you want to do with it as a Rectangle, you need to cast it to a Rectangle from the Object that it comes back as. Not casting it, 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 one can 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  
} 

Is that all there is to it? Almost.

Can you give me a complete summary? Yes. Complete for all practical purposes.

Let's start. The format, though, will a bit cruder.

I like that better.
Unadulterated account follows, then.

THE INHERITANCE MECHANISM REVIEWED

We first defined class Point.

A Point has a position (x, y).

class Point {
  int x;
  int y; 
} 
These are the features of any Point object: together defining the position of any Point.

A Pixel is a Point with Color.

In Java this is easy to write:

class Pixel extends Point {
  Color c; 
} 
The features of a Pixel are three: This set of features is the union between in other words:

That is the resulting blueprint (for Pixel) is a putting together of the two descriptions.

That's fine, but set union means that names should be kept distinct.

We'll come back to this in a second.

How do we use Point and Pixel?

Nothing unusual. We use new and expect the blueprints to define the resulting structures.

// somewhere in a method... 
Point a = new Point(); 
Pixel b = new Pixel(); 
a.x = 2; 
a.y = -10; 
b.x = 3; 
b.y = 24;
b.color = Color.blue; 
Notice that the Color class is defined in the java.awt package.

Next we asked: have you ever seen a Horse?

The answer was: yes.

Can you describe a Horse?

The answer was: that's actually quite complicated.

OK, so fortunately we know what we're talking about:

class Horse {
  // lots of features ... 
}
Next we asked: have you ever seen a Unicorn?

The answer was: no.

Can you describe a Unicorn though?

Everybody said: yes, that's easy.

Here's a picture to get the idea:

So a definition is almost immediate.

class Unicorn extends Horse {
  Horn h; 
}
And we had the following situation: That's because we factored out the Horse.

Now we said: let's write a play with horses and unicorns.

In our play we could have:

Horse   h = new Horse(); 
Unicorn u = new Unicorn(); 
Both h and u are special kinds of binoculars.

If you put them on you should see the features that their type is defining.

So h.mane and u.mane make sense.

So does u.horn but h.horn doesn't.

For this reason it's not adequate to say:

Unicorn g = new Horse(); 
It is OK, however, to ignore some features, for the sake of being more general.

From our description it follows that all Unicorns are Horses.

That's called polymorphism.

So writing something like this is acceptable:

Horse z = new Unicorn(); 
You can never access the Horn with z but sometimes you don't even need that.

We could come up with the following similar example:

class Shape {
  // two coordinates 
} 

class Circle extends Shape {
  // add a radius 
}

class Rectangle extends Shape {
  // add a width and a height 
} 

class Triangle extends Shape {
  // add two other points relative to location 
} 
Why is this useful?

If you want to create an array that can store

the only you can do that is by relying on their generality as Shapes.

Shape p[] = new Shape[100]; 
I've got room for 100 such shapes (circles, triangles, or rectangles).

There's no other way around it, as far as arrays are concerned.

Now we want to explore the name collision problem.

Consider this example:

class Horse {
  void neigh() { System.out.println("Horse: Howdy!"); } 
} 

class Unicorn extends Horse {
  void neigh() { System.out.println("Unicorn: Bonjour! "); } 
}
Unicorn is listing a feature: neigh.

If Horse had not had it already listed things would've been easy.

But Horses already know how to neigh. They say: "Howdy!".

So Unicorns redefine the feature by saying "Hello!" in French.

(Unicorns are from Paris, TX)?

That's called overriding.

The mechanism is that no matter how you look at a Unicorn,

you are guaranteed to obtain the French greeting out of it.

Here's the proof:

frilled.cs.indiana.edu%cat Ionesco.java
class Horse {
    void neigh() { System.out.println("I am a Horse: Howdy!"); }
} 

class Unicorn extends Horse {
    void neigh() { System.out.println("I am a Unicorn: Bonjour! "); }
} 

class Ionesco {
    public static void main(String[] args) {
	Unicorn a = new Unicorn(); 
	Horse   b = new Unicorn(); 
	a.neigh(); 
	b.neigh();

	Horse c = new Horse(); 
	c.neigh(); 

    } 
} 
frilled.cs.indiana.edu%javac Ionesco.java
frilled.cs.indiana.edu%java Ionesco
I am a Unicorn: Bonjour! 
I am a Unicorn: Bonjour! 
I am a Horse: Howdy!
frilled.cs.indiana.edu%
That's all we need to know before we go into applets.


Last updated: Jul 21, 2002 by Adrian German for A201