Developer

Extending the Java Collections Framework

The basic data structures available in the Java Collections Framework serve most purposes, but situations can arise where custom structures are needed. Learn how to extend the Collections Framework to create your own structures.


In my previous article, I presented the concepts and architecture of the Java Collections Framework and explained how to create new implementations based on the abstract base classes it provides. However, sometimes you don't need an entirely new data structure—just a customized version of an existing implementation. Given the architecture of the Collections Framework, one of the most convenient ways of doing this is using wrappers.

What is a wrapper?
A wrapper is an object that encapsulates another to change its behavior or to add some feature. This encapsulation is implemented as a "has a" relationship—that is, the wrapper contains a reference to the wrapped object—and not as an "is a" relationship (inheritance).

Usually, this is accomplished by passing a reference to the object to wrap to the constructor of the wrapper, which saves it somewhere. Later, when the wrapper receives a method call, it just forwards the request to the wrapped object, using the stored reference.

Notice that the real work is still done by the original, wrapped object. The wrapper usually just forwards the method calls, performing some pre- or post-processing to present a different, customized result.

The Java platform offers several examples of this technique. For instance, take a look at the java.io.FilterInputStream class. It was designed to be extended in order to create wrappers for input streams. The idea behind java.io.FilterInputStream is to take an input stream and transform its data or add some functionality.

The java.io.FilterInputStream subclasses add buffering to the underlying input stream (java.io.BufferedInputStream), uncompress compressed data (java.util.zip.zipInputStream), and calculate a message digest on the input data (java.security.DigestInputStream), among other things.

It is also common for a wrapper to add some feature not found in the wrapped object. The class java.io.PushBackInputStream, for example, adds the capability to push back data already read to the input stream. But, even in these cases, the wrapper still relies on the methods of the wrapped object.

Collections wrappers
The Collections Framework itself uses wrappers to provide synchronized and read-only versions of any data structure that implements one of the standard interfaces (Table A).
Table A
The java.util.Collections class invokes methods that take an object implementing one of the Collections Framework's interfaces and returns another that implements the same interface but is read-only or thread-safe.
Method Description
synchronizedCollection Returns a synchronized (thread-safe) collection backed by the specified collection
synchronizedList Returns a synchronized (thread-safe) list backed by the specified list
synchronizedMap Returns a synchronized (thread-safe) map backed by the specified map
synchronizedSet Returns a synchronized (thread-safe) set backed by the specified set
synchronizedSortedMap Returns a synchronized (thread-safe) sorted map backed by the specified sorted map
synchronizedSortedSet Returns a synchronized (thread-safe) sorted set backed by the specified sorted set
unmodifiableCollection Returns an unmodifiable view of the specified collection
unmodifiableList Returns an unmodifiable view of the specified list
unmodifiableMap Returns an unmodifiable view of the specified map
unmodifiableSet Returns an unmodifiable view of the specified set
unmodifiableSortedMap Returns an unmodifiable view of the specified sorted map
unmodifiableSortedSet Returns an unmodifiable view of the specified sorted set
Collections Framework wrapper implementations

Take, for example, the method synchronizedCollection. It takes any object that implements the Collection interface and returns another object that implements that interface, but with all methods synchronized. Similarly, the unmodifiableCollection method takes a Collection and returns another that just forwards any method calls to the underlying Collection, except those that change it, which throws an UnsupportedOperationException.

Notice that for this purpose, the wrapper technique is much better than inheritance. Since the wrapper wraps an object that implements an interface, it doesn't depend on that object-specific implementation and can work with any existing or future class as long as it implements the required interface.

Similarly, since the wrapper also implements that same interface, it is no different, in terms of the interface definition, from the original object and looks exactly the same.

Extending the Framework
You can use the same concepts to easily create wrappers that provide specialized versions of the standard interfaces. If you also use the abstract classes presented in my previous article as your base implementation, you can substantially reduce your effort.

Just follow these basic steps:
  • ·        Extend one of the abstract base classes.
  • ·        Define a constructor that takes as a parameter an object that implements the corresponding interface.
  • ·        Save the object passed to the constructor.
  • ·        Implement the required methods, forwarding them to the saved object and doing any necessary processing.

Listing A is a simple wrapper that uses an AbstractList and the Reflection API to wrap an object that implements the List interface, allowing objects of only a given type to be added to the list. Since it uses AbstractList for its implementation, it is not appropriate to wrap implementations based on sequential access data structures, like LinkedList, without performance penalties.

For example, to create an ArrayList that accepts only java.lang.Integer objects (or subclasses), you could use the following code fragment:
List list = new TypeSafeList(new ArrayList(),Integer.class);

Once created, the following code adds the number 10 to the list:
list.add( new Integer(10) );

However, if you try to add an object that is not a java.lang.Integer or a subclass, like the example below:
list.add( "this will throw an exception" );

you get a java.lang.ClassCastException, according the Framework's guidelines. Notice that you can pass an interface as a parameter to the TypeSafeList constructor. In this case, the list will accept any object that implements the specified interface.

Conclusion
The Collections Framework is one of the fundamental pieces of the Java Platform. Its architecture is clean, powerful, and flexible. Although it already provides ready-made classes for the most common needs, it was designed with extensibility in mind.

Creating new or specialized data structures that comply with the Framework's standard is fairly easy, and the advantages are enormous: implementation independence, interoperability, code reuse, and low learning curve, to name a few.

In this two-part series, I presented the concepts behind the Collections Framework and explained how to create new data structures or customize existing ones. Keep these concepts and techniques in mind when you design your next program, and I am sure you will greatly improve your coding practices.

Editor's Picks

Free Newsletters, In your Inbox