|
Spring Semester 2003
|
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.
|
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 SavingsAccount s.
|
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.
|
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 ?
|
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
|
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(___);
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.
(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.
|
|
BankAccount[] a = new BankAccount[100];
This strategy, in its most general form, is used by the Vector class.
|
Which makes use of arrays of Object s.
|
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.
|
|
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:
- 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.
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:
- everybody had seen horses, but nobody felt it was easy to describe them
- nobody had 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 Unicorn
s are Horse
s.
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
-
Circle
s,
-
Triangle
s, and
-
Rectangle
s,
the only you can
do that is by relying on their generality as Shape
s.
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 Horse
s already know how to neigh
.
They say: "Howdy!".
So Unicorn
s redefine the feature
by saying "Hello!" in French.
(Unicorn
s are from Paris, TX
)?
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: Apr 3, 2003 by Adrian German for A201