In an object-oriented programming language like Java,
polymorphism means that objects are always to some degree interchangeable.
Any child of a type of object can serve in place of its parent type and,
ultimately, every object is a child of java.lang.Object
, the object-oriented “Eve,” so
to speak. It is natural, therefore, for the most general types of
containers in Java to work with the type Object
so that they can hold just about
anything. By containers, we mean classes that hold instances of other
classes in some way. The Java Collections Framework is the best example of
containers. A List
, for example, holds
an ordered collection of elements of type Object
. A Map
holds an association of key-value pairs, with the keys and values also
being of the most general type, Object
.
With a little help from wrappers for primitive types, this arrangement has
served us well. But (not to get too Zen on you) in a sense, a “collection
of any type” is also a “collection of no type,” and working with Object
s pushes a great deal of responsibility
onto the user of the container.
It’s kind of like a costume party for objects where everybody is
wearing the same mask and disappears into the crowd of the collection.
Once objects are dressed as the Object
type, the compiler can no longer see the real types and loses track of
them. It’s up to the user to pierce the anonymity of the objects later by
using a type cast. And like attempting to yank off a party-goer’s fake
beard, you’d better have the cast correct or you’ll get an unwelcome
surprise.
Date
date
=
new
Date
();
List
list
=
new
ArrayList
();
list
.
add
(
date
);
...
Date
firstElement
=
(
Date
)
list
.
get
(
0
);
// Is the cast correct? Maybe.
The List
interface has an
add()
method that accepts any type of
Object
. Here, we assigned an instance
of ArrayList
, which is simply an
implementation of the List
interface,
and added a Date
object. Is the cast in
this example correct? It depends on what happens in the elided “...”
period of time.
It’s natural to ask if there is a way to make this situation
better. What if we know that we are only going to put Date
s into our list? Can’t we just make our
own list that only accepts Date
objects, get rid of the cast, and let the compiler help us again? The
answer, surprisingly perhaps, is no. At least, not in a very satisfying
way.
Our first instinct may be to try to “override” the methods of
ArrayList
in a subclass. But of
course, rewriting the add()
method in
a subclass would not actually override anything; it would add a new
overloaded method.
public
void
add
(
Object
o
)
{
...
}
public
void
add
(
Date
d
)
{
...
}
// overloaded method
The resulting object still accepts any kind of object—it just invokes different methods to get there.
Moving along, we might take on a bigger task. For example, we
might write our own DateList
class
that does not extend ArrayList
, but
rather delegates the guts of its methods to the ArrayList
implementation. With a fair amount
of tedious work, that would get us an object that does everything a
List
does but that works with
Date
s. However, we’ve now shot
ourselves in the foot because our container is no longer an
implementation of List
and we can’t
use it interoperably with all of the utilities that deal with
collections, such as Collections
.sort()
, or add it to another collection with
the Collection addAll()
method.
To generalize, the problem is that instead of refining the
behavior of our objects, what we really want to do is to change their
contract with the user. We want to adapt their API to a more specific
type and polymorphism doesn’t allow that. It would seem that we are
stuck with Object
s for our
collections. And this is where generics come in.
Get Learning Java, 4th Edition now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.