Containers: Building a Better Mousetrap

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 Objects 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.

Can Containers Be Fixed?

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 Dates 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 Dates. 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 Objects 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.