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