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.

1 2 Page
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more