Secure type-safe collections
Overcome the problems of the generic type containers in the Java Collections Framework
The Java Collections Framework (JCF), introduced in 1998 in JDK 1.2, is quickly becoming the standard for storing dynamic data in Java systems. Many new APIs use JCF’s container interfaces to provide references to list-, sets-, and map-like data structures. JCF’s ubiquity is evident in the new EJB 2.0 standard. The framework uses Collection
, Set
, and Map
implementations to model one-to-many and many-to-many relationships between entity beans and dependent objects.
Note: To download this article’s complete source code, see Resources.
JCF’s design goals focus on simplicity and flexibility by employing the Java language features. As such, it’s based on the definition of interfaces for container classes. JCF includes two independent super interfaces: java.util.Collection
for storing collections of objects and java.util.Map
for storing key-value pairs. Derived from these roots are more specialized interfaces, like Set
, SortedSet
, and List
on the collection side, and SortedMap
on the map side.
Remember, this article assumes that you are familiar with JCF design concepts. If you need to get up to speed, read the introductory JCF articles in Resources.
After you’ve used JCF for a while, you’ll quickly encounter one of its problems: you can’t restrict JCF’s containers to store only objects of a specific type. Instead, all containers store and return objects of the common super class Object
. So normally, you have to cast up all objects that you retrieve from a container. But therein lies our problem. What happens if someone inserts an object of the wrong type? Here’s an example to illustrate the point:
...
List intList = new ArrayList(); //should store java.lang.Integer objects only
intList.add(new Integer(1)); //OK
intList.add("2"); //Wrong, but not detected by compiler or runtime system
...
Integer number0 = (Integer)intList.get(0); //OK
Integer number1 = (Integer)intList.get(1); //throws ClassCastException
The second object added to intList
is of type String
— not Integer
, as the programmer intended. The compiler can’t check the type of the objects you add to the container, because it accepts them all. To make things worse, the runtime system doesn’t prevent you from adding the wrong type. Only when you try to read your data back do you get the ClassCastException
that tells you that somewhere, you added an object of the wrong type to your container. As you can guess, finding the responsible code can prove difficult.
Templates represent a possible solution to this problem. Indeed, they are the basis for C++’s Standard Template Library (STL). Unfortunately, the Java language doesn’t include templates — at least not yet. Therefore, JCF’s designers made all containers store objects of the common super class, Object
, in order to provide generic collections. On the other hand, templates present their own problems — heavy usage bloats your binary code.
With that in mind, frameworks offer a better solution. As such, the framework presented in this article solves the problem so you can store objects of all types in a JFC collection. The framework prechecks objects as you add them to a container. If an object possesses the wrong type, the framework immediately throws the ClassCastException
, making it easier to find and change the troublesome code.
In the next section, I’ll show you how to check the type of a Java object during runtime. Then we’ll discuss in detail the framework’s type-safe container classes. In the last section, I’ll present a small test application that demonstrates how to use the new library.
Reflection
The first question to answer is: “How do we check an object’s type?” Fortunately, Java includes a built-in mechanism to verify and inspect the type of each object during runtime. Normally, you do this with the instanceof
operator, which returns a boolean
value whether the object is of the given type or not. For example, to check whether your object in the variable myObject
is of type String
, you can write:
if (myObject instanceof String) {
//do something
}
Unfortunately, the instanceof
operator expects the type statically at compile time. Therefore, it wouldn’t allow us to write generic code to check every type. To solve our problem, we turn to Reflection.
In Java, you can request information about every object during runtime with its Class
object, returned by the getClass()
method every class inherits from Object
. With the help of the Class
object, you can retrieve information such as the class name, method names, and so on. You can also find out whether another object is an instance of this class. Therefore, we have to call the isInstance(Object)
method of the Class
object.
To achieve our desired result, we must get the Class
object for our container’s type, and check each object added to it by calling isInstance()
. Let’s assume the container type resides in:
Class _type;
We have to modify the add()
method of a collection:
public boolean add(Object o) {
if (_type.isInstanceOf(o)) {
// add o to our collection
}
else {
throw new ClassCastExeption(_type.getName() + " expected but " +
o.getClass().getName() + " found");
}
}
If the object is not of the required type, we throw a ClassCastException
to inform the caller that the programmer’s object possesses the wrong type (and that he should read the manual more thoroughly). This precondition check has to be done for all methods that add something to the container.
Type-safe wrapper classes
How do we apply this solution to our existing container classes (either base classes like ArrayList
or our special implementations)? It would be tedious to extend all the existing container classes to add the new type-checking code. There is a more elegant solution: We can use the same approach as the JCF itself by employing a wrapper object, also known as the Decorator pattern. The Decorator pattern is applied by the java.util.Collections
object, which includes methods to wrap your existing container objects with new containers that allow synchronized or read-only access to your original object. The solution’s advantage is its flexibility — you can assemble your container’s properties as you need them, just like Legos. For example, if you want a read-only version of your existing List
object, you take the List
reference returned by:
Collections.unmodifiableList(List)
This static method expects as its parameter your pre-existing List
object. The returned read-only List
object stores this reference and forwards all method calls to it, except the ones that modify your List
. Those method calls are blocked. Effectively, it works as a filter to your existing List
object.
To use this approach, we must do two things: create the wrapper classes for the base interfaces of the collection classes (Collection
, Set
, List
, and Map
), and write the object that returns these containers in static methods.
The wrapper classes prove straightforward. They all begin with TypeSafe
and end with their interface names. Listing 1 shows the results.
Listing 1
In the code above, the wrapper classes’ constructors take the wrapped container object as the first parameter and store it in a private attribute:
private Class _type;
The second parameter is the Class
object with the type of the objects we want to store. The method:
protected void checkType(Object o) {
if (!_type.isInstance(o)) {
throw new ClassCastException(o.getClass().getName());
}
}
performs the type-checking and throws the ClassCastException
if its parameter is not an instance of the correct type. First, we have to call checkType()
for methods that add a new object. For the classes TypeSafeCollection
and TypeSafeSet
, the add(Object)
and addAll(Collection)
methods fit that description. The implementations of all other methods defined in the container interface call the corresponding method of the wrapped container.
TypeSafeList
The implementation of the TypeSafeList
is based on the TypeSafeCollection
class, as seen in Listing 2.
Listing 2
We have to check the parameter of the TypeSafeList
‘s set()
method. But there are more methods of the List
that have to be protected. The subList(int fromIndex, int toIndex)
method of the List
returns a List
object for the subregion in the list. We have to protect this List
as well because it changes the original container. Therefore, we return another new TypeSafeList
wrapper instead of the original sublist. Also, the List
‘s ListIterator
object not only lets you read data from the List
like the simple Iterator
, but it has set()
and add()
methods to modify the List
. Therefore, we have to write a special wrapper class for the ListIterator
that checks the parameter to its set()
and add()
methods. This class is implemented as an inner class of our TypeSafeList
.
Type-safe SortedSet
The SortedSet
extends the normal Set
interface with special methods that return subviews on the Set
. Here we use the same approach as we do for the sublists in the List
interface. The methods simply return a new TypeSafeSortedSet
object that wraps the subset from the original SortedSet
container, shown in Listing 3.
Listing 3
Type-safe Map
In the class hierarchy of the Java Collections library, the Map
interface acts independently of the Collection
interfaces Collection
, List
, Set
, and SortedSet
. So we can’t use inheritance to speed up the implementation of the TypeSafeMap
, as shown in Listing 4.
Listing 4
Unlike a Collection
, a map stores pairs of keys and values. Therefore, we must check the type for both objects, the keys and the values, independently. That means the constructor must take two Class
objects: one for the type of the keys and one for the type of the values public TypeSafeMap(Map map, Class keytype, Class valuetype) {...}
. Moreover, the map has no simple add(Object)
and addAll(Collection)
methods, but includes respective put(Object key, Object value)
and putAll(Map map)
methods where we have to check both parameter types. Fortunately, we need not worry about the keys()
and values()
methods, which, according to the JDK documentation, return unmodifiable collections. Here, we can simply return the original return values.
However, for the entrySet()
method, this is no longer true. entrySet()
returns a Set
with Map.Entry
objects. You cannot add new objects to this Set
, but the Map.Entry
objects allow the values to be set. That means we have to wrap all elements read from this set by a new TypeSafeMapEntry
object. The code for this class, TypeSafeMapEntrySet
, can be found in the complete source code for the type-safe collections framework. (See Resources for a link.)
Again, the implementation of the TypeSafeSortedMap
proves quite easy because we have to return only TypeSafeMap
objects for all the subviews of the map.
Static factory methods
Now you have seen how to implement the wrapper classes. However, we don’t want to use them directly. Instead, like the Collections
object in the JCF, we define a TypeSafeCollections
class with static methods to return our type-safe wrapper objects. It’s all in Listing 5.
Listing 5
They work as a factory method for our wrapper object, but their parameters and return types use the Collection
interface types only. Therefore, TypeSafeCollections
must be the only public class in our typesafecollections
package. The wrapper classes themselves can be packaged internally; they are a package implementation detail.
A test application
Our last task is to create a test application, detailed in Listing 6, to see how to use the new library.
Listing 6
In the test application, we first want to find out how to get the Class
object for the type. In this case, we can either call the getClass()
method, or if we don’t have an instance of such an object, we can use the <classname>.class
attribute available for all classes. The second option means we can provide the Class
object without possessing an instance of that class, allowing us to use an interface as the type of the objects in our container:
tsc = TypeSafeCollections.typeSafeCollection(c, java.util.Set.class);
Therefore, in the Collection tsc
we can store all objects that implement the java.util.Set
interface. Similarly, we can store in a container extended objects that are restricted to their common (and probably abstract) super class.
Conclusion
In this article, I’ve presented a solution to a drawback of the widely used standard Java Collections Framework: its containers lack the ability to restrict themselves to storing objects of a specific type. The solution uses the Java Reflection mechanism to check the type of all objects added to a container. The solution also employs wrapper classes to add this behavior to all implementations of the basic container interfaces defined in JCF. A central object creates the wrapper classes with static factory methods similar to the already existing java.util.Collections
class.
Of course, the solution in this article can’t give you the same safety level as a compile-time error you’d receive if you employed templates in the C++ Standard Template Library. But throwing a ClassCastException
while adding objects of the wrong type makes it much easier to find the code that initiated the problem. Moreover, compared to templates, our solution, using wrapper classes and Reflection, results in much smaller code sizes.