Lecture Notes Nineteen:
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.
|
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.
|
That's called inheritance.
|
And Object is responsible for providing it. 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 is 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.
|
Whic is... interestRate.
|
Exactly.
|
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.
|
|
Let's now draw a picture to illustrate...
|
... what each type of object has and why:
|

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();
|
|
Very good.
|
Can you then store a reference to a SavingsAccount object into an object variable of type BankAccount?
|
BankAccount b = new SavingsAccount(10);
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(___);
|
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;
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.
|
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.
|
|
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, 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.
|
|
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? 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.
|
|
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 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.
|
|
|
Unadulterated account follows, then.
|
INHERITANCE MECHANISM REVEALED
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:
- an
x coordinate, and
- a
y coordinate
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:
- an
x coordinate (which is an int)
- a
y coordinate (which is an int)
- a
Color, call it color
This set of features is the union between
- the features of a
Point and
- the one new feature that class
Pixel is defining
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.
class Unicorn extends Horse {
Horn h;
}
So we had the following situation:
- everybody has seen horses, but nobody felt it was easy to describe them
- nobody has seen unicorns, but everybody thought it was easy to describe them
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
-
Circles,
-
Triangles, and
-
Rectangles,
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.
That's called overriding.
The mechanism is that no matter how you look at a Unicorn,
- as the
Unicorn that it is, or
- as the
Horse that it is
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 19, 2001 by Adrian German for A201