Let's think over the progression of ideas so far in the course.
Each abstraction technique lets us hide details so we can concentrate on the problem a piece at a time, and gives us a way to look at the "big picture" without drowning in detail.
Now, let's think about what will happen as we try to build larger software systems. We can organize them as collections of objects. But there will be many, many objects! And some of these objects will be similar in their purpose or functionality. We would like abstraction mechanisms to deal with these large collections of objects, to make sense of them in an organized way.
In everyday life or in scientific disciplines, what do people generally do to manage large collections of things? For example, how do biologists think about large numbers of species? How does the Yellow Pages provide an organized way for people to easily find its advertisers? They use:
For example, biologists group animals and plants separately, and each of those categories contain subgroups, etc. They build a classification system of types, subtypes, subsubtypes, etc., as a way of managing large numbers of species.
Similarly, the Yellow Pages has a classification system based on the kinds of goods and services provided by its advertisers.
In general, each class inherits variables and
methods from its superclass, and may add more
variables and methods or override existing ones.
Here, for example, a ResizableObject
has a
resize
method not available to every
GraphicsObject
.
extends
. For example,
public class ResizableObject extends GraphicsObject { public void resize(int width, int height) { ... // changes object to have the given width and height } }
All other methods and all instance variables are inherited.
Only the new information that distinguishes this subclass from its
superclass needs to be defined. The rest comes "for free" just by
saying that the class extends GraphicsObject
.
extends
and
implements
:
A given class may extend at most one other class, but it may (in addition) implement many interfaces. Some languages, such as C++, allow a class to be a subclass of more than one class. This concept, called "multiple inheritance," is not supported by Java.
One of the many benefits of inheritance is the ability to do incremental design and implementation. You can start with some simple classes and then create various subclasses that extend the functionality of the system.
Let's do an example of incremental design using as a starting
point the CalorieCounter
class we implemented earlier
in the course. Recall that the CalorieCounter
provided the following methods:
CalorieCounter(int limit)
-- sets the limit to
the given value and initializes instance variableseat(int carbs, int prot, int fat)
-- checks
limit and prints message if exceeded; records consumptiontotal()
-- returns total calories consumed so
farpercentFat()
-- returns percentage of calories
consumed from fatcheat(int fat)
-- reduces grams of fat by given
amountreset()
-- initializes all instance variables
expect limittoString()
-- returns something like "75g
carb, 15g prot, 20g fat"
Now suppose we have many happy users of our
CalorieCounter
, but we want to expand the market. If
seems that many potential customers don't like the idea of having
to specify the carbs, prot, and fat separately. In fact, all they
want to do is tell the calorie counter what food they ate, and let
it do the rest.
However, we don't want to upset our existing customer base by
changing the CalorieCounter
interface. Instead,
we'll create a new kind of calorie counter by subclassing the one
we already have.
Our subclass needs a new method, eat(Food x)
, so
people can just provide a food object, and the calorie counter
will take care of updating the various grams of fat, prots, and
carbs.
Food
object has methods to
extract grams of fat, proteins, and carbohydrates.
public class HealthOMatic extends CalorieCounter { public void eat(Food x) { eat(x.getCarbs(), x.getProt(), x.getFat()); // ^^^ method inherited from the superclass } }
That's it! We use the superclass eat
method
within the new one, to avoid repeating all that effort (checking
the limit, etc.). This means that whenever we update the
eat
method in the parent, the child is automatically
updated as well. (The "traditional" eat
method is
also available.)
Now, to continue with our design, we need to define the
Food
class.
public class Food { int carbs, prot, fat; // in grams String name; public Food(int c, int p, int f, String name) { carbs = c; prot = p; fat = f; this.name = name; } public boolean healthy() { return (fat * 7 < carbs + prot); } public int getCarbs() { return carbs; } public int getProt () { return prot ; } public int getFat () { return fat ; } public String about() { return "food item"; } public String toString() { return (name + "(" + carbs + "g carb, " + prot + "g prot, " + fat + "g fat)"); } }
This is nice. We can create a library of Food
objects, and the users of our HealthOMatic
just need
to pick one whenever they eat.
But maybe we can make it better. There's more to eating than counting calories. We'd like to tap into a high-end market where people are concerned not only about calories, but about balanced diets.
Following the "food pyramid," we can create various kinds of
Food
objects:
toString
method that uses the toString
method from the base
class (superclass).
public class Meat extends Food { public String about() { return "meat --- it's what's for dinner"; } public String toString() { return "meat " + super.toString(); } }
Note that super
is needed here because there's a local
definition of toString
in the Meat
class and we want to use the one in the parent class.
We didn't need to say super.eat
in the
HealthOMatic
example because the compiler could distinguish
between the two eat
methods in the parent and child classes on
that basis of their parameter types.
We could go on adding other methods to the various subclasses
of Food
, but let's go back to our main discussion.
Since we have created all these different kinds of foods, let's
see if we can provide a top-of-the-line calorie counter that uses
this information.
We can create a subclass of HealthOMatic
with even
more features. It can keep track of the number of servings from
each food group for proper nutritional balance.
public class Dietician extends HealthOMatic { int meat, dairy, bread, fruit, veggie, junk; // Additional instance variables public Dietician(int limit) { super(limit); // calls parent's constructor -- must be first!! meat = dairy = bread = fruit = veggie = junk = 0; } public void reset() { meat = dairy = bread = fruit = veggie = junk = 0; super.reset(); } public void eat(Food f) { if (! f.healthy()) junk++; else if (f instanceof Meat) meat++; else if (f instanceof Fruit) fruit++; else if ... ... super.eat(f); } public void cheat(int fat) { Terminal.println("Get with the program!"); } public String toString() { return (string containing info about food groups); } }
instanceof
is a built-in operator to see if an
object is an instance of a class.
We could also add new methods for accessing food group information, and add other features, but this is sufficient to illustrate the concept of incremental design.
super
refers to the parent class:
The operator instanceof
can be used to check
membership in a class.
Fruit apple = new Fruit(50, 0, 0, "apple"); (apple instanceof Fruit) => true (apple instanceof String) => false (apple instanceof Meat) => false (apple instanceof Food) => trueOne way to think of the
instanceof
operator is that every
object is considered an instance of its own class and all ancestors of
its class, but the object is not an instance of descendants
of its own class. This makes sense for the following reason.
Consider an object X that is an instance of a class C. X has all the
methods and instance variables of C and all the ansestors of C through
inheritance, so X can be used anywhere something of type C or an
ancestor of type C is needed. However, X does not necessarily have
all the methods and instance variables of descendants of the class.
That is, the descendants classes may define more methods or variables,
and so it would not be safe to use X where an instance of a subclass
of C is needed.
Food x = new Food(1, 2, 3, "foo"); (x instanceof Food) => true (x instanceof Meat) => false
Casting is a directive to the compiler to treat an expression as being of a certain type. We've already seen the use of casting for primitive types, such as integers and doubles. For example,
double x = 6 + 9 / 2; double y = 6 + ((double) 9) / 2;
The value of x
is 10, but y
is 10.5.
The expression 9 is an integer expression, but when the compiler
sees a cast ((double) 9)
, it treats
the 9 as type double
. For primitive types
int
and double
, Java knows how to do the
conversions. They are built into the language.
In the case of class hierarchies, casting is needed when you have an expression that evaulates to an object reference, and you want that reference to be treated by the compiler as a more specific type than the compiler can determine by itself.
For example, recall the Relation
class we
implemented to store associations between objects. Each
association mapped an Object
to some other
Object
. So, we had methods:
void map(Object d, Object r)
Object lookup(Object d)
Now suppose we tried the following:
Relation m = new Relation(); String s = "CS101"; String t = "great course"; m.map(s, t); Terminal.println(m.lookup(s));
We'll get a compiler error because the return type of
lookup
is Object
, but the
println
method expects type String
.
However, since we know that the object
returned will, in fact, be a String
, we can
tell the compiler that this is the case, so it
won't complain about the type mismatch. You're telling the
compiler: "Trust me, it's a string."
Terminal.println((String) m.lookup(s));
What if we make a mistake? Suppose that, in fact, we had done
m.map(s, m)
, accidentally mapping the String
"CS101"
to the mapping itself. The program would
compile, but at run-time, Java checks each cast. If the object
isn't, in fact, the type you said it was, then an exception will
occur.
Whether or not a cast is needed depends upon the relationship between the two types in the class hierarchy.
Suppose you have an expression that the compiler can determine
to be of type Foo
, but you want to treat it as
another type Bar
. For example,
Bar x = getFoo();where the return type of
getFoo
is
Foo
.
Is a cast needed? It depends.
If, in the class hierarchy, Foo
is a descendant of
Bar
, then a cast is not needed
(because every Foo
is a Bar
, just a
special kind of Bar
).
On the other hand, if Bar
is a descendant of
Foo
, then a cast is needed (because
there may be other Foo
objects that are not
Bar
objects, so the compiler can't determine by
itself that this is safe).
If neither is a descendant of the other, then the assignment is not allowed, with or without a cast.
In general, it is good programming practice to avoid casting
unless absolutely necessary, and then to be sure that you won't
get a run-time exception. Sometimes, it's a good idea to put a
cast inside a conditional statement, where an
instanceof
expression is used as the test.
Bar x; Foo y = getFoo(); if (y instanceof Bar) x = (Bar) y; // Cannot get a run-time exception // from this cast else if ...
We have seen that one of the advantages of inheritance is the
ability to quickly define new types that are specializations of
types we have previously defined. For example, we defined the
Food
class, and then several specializations, such as
the Meat
and Fruit
classes.
We have also seen that a class hierarchy represents "is-a"
relationships. Each instance of a subclass is also an instance of
the parent class and all ancestors. For example,
Meat
"is-a" Food
and Food
"is-a" Object
. This meant that we could create a
calorie counter that could receive any Food
object as
a parameter to its eat
method, but that we could
actually pass instances of Food
or any of its
subclasses into the eat
method.
The beauty of this is that when we have an instance of
Food
, we can call any of the methods defined for
Food
. If the object happens to be an instance of a
subclass of Food
that has overriden the method, then
we'll automatically get the specialized behavior defined for that
subclass. That is, when we call the method, we need not know what
kind of Food
we have. We just call the method and
the object knows how to perform whatever action is defined by its
particular specialized subclass.
This powerful idea, of invoking methods on objects that may do different things depending on their specialization, is called polymorphism. The roots are poly, meaning "many," and morph, meaning "shape." We deal with a general class of object that may be instantiated with many "shapes", defined by specialized subclasses, but we treat them uniformly, as instances of the more general class.
Let's go back to the graphics object hierarchy and see how the power of polymorphism is used.
Several different things happen in a class hierarchy:
Inheritance, in which a subclass uses behavior defined by a parent.
Example: move
is defined by
GraphicsObject
and inherited by all subclasses.
Augmentation, in which a subclass adds a new behavior not defined in the parent (an extra method).
Example: setFilled
is a new method defined in
FilledObject
(inherited by its children, but not
defined for all GraphicsObject
's).
Specialization, in which a subclass overrides a behavior of the parent class to do something different in the subclass (the new behavior may include the prior behavior).
Example: draw
is defined in
GraphicsObject
but is overriden to do something
different for each kind of graphics object.
Polymorphism exploits the notion of specialization. This is
most clearly seen in the implementation of the
CS101Canvas
. The canvas contains, as part of its
internal representation, a linked list of
GraphicsObject
's.
Note that although each GraphicsObject
is
different (lines, text, ovals, etc.), the canvas doesn't "know"
that they are different. It treats them all uniformly, as
graphics objects. Whenever the add
method is called,
the canvas puts the GraphicsObject
in the list.
Then how do the graphics objects show up differently on the screen? That's the beauty of polymorphism...
The canvas has a repaint
method that is called
whenever something changes to require that the graphics be redrawn
on the screen. (For example, when an object is moved or when an
overlapping window uncovers part of the canvas.) The
repaint
method goes through the list, something like
this:
GraphicsList ptr = first; while (ptr != null) { ptr.graphic.draw(); ptr = ptr.next; }
Note that ptr.graphic
has type
GraphicsObject
. The canvas doesn't "know" the
specific type of GraphicsObject
, though. It just
calls the draw
method on each one. And, since each
subclass (Line
, Oval
, Rect
,
Text
, etc.) has defined its own specialized
draw
method, each of the graphics objects "knows" how
to draw itself.
There are no special cases, no big if-then-else statements.
Just a simple call to draw
is all it takes, and each
object knows what to do.