Abstract base classes and interfaces
Often in a design, you want the base class to present only an interface for its derived classes.
That is, you don’t want anyone to actually create an object of the base class, only to upcast
to it so that its interface can be used. This is accomplished by making that class abstract
using the abstract keyword. If anyone tries to make an object of an abstract class, the
compiler prevents them. This is a tool to enforce a particular design.
You can also use the abstract keyword to describe a method that hasn’t been implemented
yet – as a stub indicating “here is an interface function for all types inherited from this class,
but at this point I don’t have any implementation for it.” An abstract method may be
created only inside an abstract class. When the class is inherited, that method must be
implemented, or the inherited class becomes abstract as well. Creating an abstract method
allows you to put a method in an interface without being forced to provide a possibly
meaningless body of code for that method.
The interface keyword takes the concept of an abstract class one step further by preventing
any function definitions at all. The interface is a very useful and commonly-used tool, as it
provides the perfect separation of interface and implementation. In addition, you can
combine many interfaces together, if you wish. (You cannot inherit from more than one
regular class or abstract class.)
Wednesday, July 23, 2008
Abstract base classes and interfaces
Labels: Learning about Java
Posted by Ganesh Kumar Devarasetty at 12:20 PM 0 comments
Dynamic binding
Dynamic binding
What’s amazing about the code in doStuff( ) is that somehow the right thing happens.
Calling draw( ) for Circle causes different code to be executed than when calling draw( ) for
a Square or a Line, but when the draw( ) message is sent to an anonymous Shape, the
correct behavior occurs based on the actual type that the Shape handle happens to be
connected to. This is amazing because when the Java compiler is compiling the code for
doStuff( ), it cannot know exactly what types it is dealing with. So ordinarily, you’d expect
it to end up calling the version of erase( ) for Shape, and draw( ) for Shape and not for the
specific Circle, Square, or Line. And yet the right thing happens. Here’s how it works.
When you send a message to an object even though you don’t know what specific type it is,
and the right thing happens, that’s called polymorphism. The process used by object-oriented
programming languages to implement polymorphism is called dynamic binding. The
compiler and run-time system handle the details; all you need to know is that it happens
and more importantly how to design with it.
Some languages require you to use a special keyword to enable dynamic binding. In C++
this keyword is virtual. In Java, you never need to remember to add a keyword because
functions are automatically dynamically bound. So you can always expect that when you
send a message to an object, the object will do the right thing, even when upcasting is
involved.
50 Thinking in Java www.BruceEckel.com
Labels: Learning about Java
Posted by Ganesh Kumar Devarasetty at 12:19 PM 0 comments
Interchangeable objects
Interchangeable objects
with polymorphism
Inheritance usually ends up creating a family of classes, all based on the same uniform
interface. We express this with an inverted tree diagram:5
One of the most important things you do with such a family of classes is to treat an object
of a derived class as an object of the base class. This is important because it means you can
write a single piece of code that ignores the specific details of type and talks just to the base
class. That code is then decoupled from type-specific information, and thus is simpler to
write and easier to understand. And, if a new type – a Triangle, for example – is added
through inheritance, the code you write will work just as well for the new type of Shape as
it did on the existing types. Thus the program is extensible.
Consider the above example. If you write a function in Java:
void doStuff(Shape s) {
s.erase();
// ...
s.draw();
}
This function speaks to any Shape, so it is independent of the specific type of object it’s
drawing and erasing. If in some other program we use the doStuff( ) function:
Circle c = new Circle();
Triangle t = new Triangle();
Line l = new Line();
doStuff(c);
doStuff(t);
doStuff(l);
5 This uses the Unified Notation, which will primarily be used in this book.
Shape
draw()
erase()
Circle
draw()
erase(
Square
draw()
erase()
Line
draw()
erase()
Chapter 1: Introduction to Objects 49
The calls to doStuff( ) automatically work right, regardless of the exact type of the object.
This is actually a pretty amazing trick. Consider the line:
doStuff(c);
What’s happening here is that a Circle handle is being passed into a function that’s
expecting a Shape handle. Since a Circle is a Shape it can be treated as one by doStuff( ).
That is, any message that doStuff( ) can send to a Shape, a Circle can accept. So it is a
completely safe and logical thing to do.
We call this process of treating a derived type as though it were its base type upcasting. The
name cast is used in the sense of casting into a mold and the up comes from the way the
inheritance diagram is typically arranged, with the base type at the top and the derived
classes fanning out downward. Thus, casting to a base type is moving up the inheritance
diagram: upcasting.
An object-oriented program contains some upcasting somewhere, because that’s how you
decouple yourself from knowing about the exact type you’re working with. Look at the code
in doStuff( ):
s.erase();
// ...
s.draw();
Notice that it doesn’t say “If you’re a Circle, do this, if you’re a Square, do that, etc.” If you
write that kind of code, which checks for all the possible types a Shape can actually be, it’s
messy and you need to change it every time you add a new kind of Shape. Here, you just
say “You’re a shape, I know you can erase( ) yourself, do it and take care of the details
correctly.”
Labels: Learning about Java
Posted by Ganesh Kumar Devarasetty at 12:18 PM 0 comments
Is-a vs. is-like-a relationships
Is-a vs. is-like-a relationships
There’s a certain debate that can occur about inheritance: Should inheritance override only
base-class functions? This means that the derived type is exactly the same type as the base
class since it has exactly the same interface. As a result, you can exactly substitute an object
of the derived class for an object of the base class. This can be thought of as pure substitution.
In a sense, this is the ideal way to treat inheritance. We often refer to the relationship
between the base class and derived classes in this case as an is-a relationship, because you
can say “a circle is a shape.” A test for inheritance is whether you can state the is-a
relationship about the classes and have it make sense.
There are times when you must add new interface elements to a derived type, thus extending
the interface and creating a new type. The new type can still be substituted for the base type,
but the substitution isn’t perfect in a sense because your new functions are not accessible
from the base type. This can be described as an is-like-a relationship; the new type has the
interface of the old type but it also contains other functions, so you can’t really say it’s
exactly the same. For example, consider an air conditioner. Suppose your house is wired with
all the controls for cooling; that is, it has an interface that allows you to control cooling.
Imagine that the air conditioner breaks down and you replace it with a heat pump, which
can both heat and cool. The heat pump is-like-an air conditioner, but it can do more. Because
your house is wired only to control cooling, it is restricted to communication with the
cooling part of the new object. The interface of the new object has been extended, and the
existing system doesn’t know about anything except the original interface.
When you see the substitution principle it’s easy to feel like that’s the only way to do things,
and in fact it is nice if your design works out that way. But you’ll find that there are times
when it’s equally clear that you must add new functions to the interface of a derived class.
With inspection both cases should be reasonably obvious.
Labels: Learning about Java
Posted by Ganesh Kumar Devarasetty at 12:17 PM 0 comments
Overriding base-class functionality
Overriding base-class functionality
Although the extends keyword implies that you are going to add new functions to the
interface, that’s not necessarily true. The second way to differentiate your new class is to
change the behavior of an existing base-class function. This is referred to as overriding that
function.
To override a function, you simply create a new definition for the function in the derived
class. You’re saying “I’m using the same interface function here, but I want it to do
something different for my new type.”
Labels: Learning about Java
Posted by Ganesh Kumar Devarasetty at 12:16 PM 0 comments
Inheritance:reusing the interface
Inheritance:reusing the interface
By itself, the concept of an object is a convenient tool. It allows you to package data and
functionality together by concept, so you can represent an appropriate problem-space idea
rather than being forced to use the idioms of the underlying machine. These concepts are
expressed in the primary idea of the programming language as a data type (using the class
keyword).
It seems a pity, however, to go to all the trouble to create a data type and then be forced to
create a brand new one that might have similar functionality. It’s nicer if we can take the
existing data type, clone it and make additions and modifications to the clone. This is
effectively what you get with inheritance, with the exception that if the original class (called
the base or super or parent class) is changed, the modified “clone” (called the derived or
inherited or sub or child class) also reflects the appropriate changes. Inheritance is
implemented in Java with the extends keyword. You make a new class and you say that it
extends an existing class.
When you inherit you create a new type, and the new type contains not only all the
members of the existing type (although the private ones are hidden away and inaccessible),
but more importantly it duplicates the interface of the base class. That is, all the messages
you can send to objects of the base class you can also send to objects of the derived class.
Since we know the type of a class by the messages we can send to it, this means that the
derived class is the same type as the base class. This type equivalence via inheritance is one of
the fundamental gateways in understanding the meaning of object-oriented programming.
Since both the base class and derived class have the same interface, there must be some
implementation to go along with that interface. That is, there must be a method to execute
Chapter 1: Introduction to Objects 47
when an object receives a particular message. If you simply inherit a class and don’t do
anything else, the methods from the base-class interface come right along into the derived
class. That means objects of the derived class have not only the same type, they also have the
same behavior, which doesn’t seem particularly interesting.
You have two ways to differentiate your new derived class from the original base class it
inherits from. The first is quite straightforward: you simply add brand new functions to the
derived class. These new functions are not part of the base class interface. This means that
the base class simply didn’t do as much as you wanted it to, so you add more functions. This
simple and primitive use for inheritance is, at times, the perfect solution to your problem.
However, you should look closely for the possibility that your base class might need these
additional functions.
Labels: Learning about Java
Posted by Ganesh Kumar Devarasetty at 12:15 PM 0 comments
Reusing the implementation
Reusing the implementation
Once a class has been created and tested, it should (ideally) represent a useful unit of code. It
turns out that this reusability is not nearly so easy to achieve as many would hope; it takes
experience and insight to achieve a good design. But once you have such a design, it begs to
46 Thinking in Java www.BruceEckel.com
be reused. Code reuse is arguably the greatest leverage that object-oriented programming
languages provide.
The simplest way to reuse a class is to just use an object of that class directly, but you can
also place an object of that class inside a new class. We call this “creating a member object.”
Your new class can be made up of any number and type of other objects, whatever is
necessary to achieve the functionality desired in your new class. This concept is called
composition, since you are composing a new class from existing classes. Sometimes
composition is referred to as a “has-a” relationship, as in “a car has a trunk.”
Composition comes with a great deal of flexibility. The member objects of your new class are
usually private, making them inaccessible to client programmers using the class. This allows
you to change those members without disturbing existing client code. You can also change
the member objects at run time, which provides great flexibility. Inheritance, which is
described next, does not have this flexibility since the compiler must place restrictions on
classes created with inheritance.
Because inheritance is so important in object-oriented programming it is often highly
emphasized, and the new programmer can get the idea that inheritance should be used
everywhere. This can result in awkward and overcomplicated designs. Instead, you should
first look to composition when creating new classes, since it is simpler and more flexible. If
you take this approach, your designs will stay cleaner. It will be reasonably obvious when
you need inheritance.
Labels: Learning about Java
Posted by Ganesh Kumar Devarasetty at 12:13 PM 0 comments