Checked Collection - stronger type safety

CheckedCollection helps to enforce strong type safety when mixing generic and non-generic code.

With the introduction of generics in java5, strong type-safety was built right-in when working with collections. Generics allows us to ensure that only valid data is inserted into a list or any other collection.

From a developer standpoint, these checks are quite important as they allows us to identify any erroneous scenarios at the compile time itself instead of the runtime. But the only way to reap its full benefit is by always using generic code.

Generics, being backward compatible (thanks to type erasure) can be used with non-generic code. The non-generic code, being unaware of the member type, can modify the collection with non-compatible members. Such a case might pose hard-to-recover situations for the other code modules using the said collection.

Consider the following scenario:

class CityNameList{
    private final SomeOtherClass someOtherClass = new SomeOtherClass();
    private final List<String> cityNames = new ArrayList<>();

    public void add(String name){
        cityNames.add(name);
    }
    public void process(){
        someOtherClass.methodWithNonGenericArgs(cityNames);
    }
    public void sortAsc(){
        Collections.sort(cityNames);
    }
}

class SomeOtherClass{
    public void methodWithNonGenericArgs(List args){
        args.add(1);
        args.add(2);
    }
}
  1. CityNameList class creates a List that expects only String values.
  2. The add method is used to populate the List.
  3. After adding a few cities, the process method is called, which calls some other non-generic service or component. It could be a local class or a remote API.
  4. As mentioned above, type erasure allows List is to accept params of type List<String>
  5. As methodWithNonGenericArgs is unaware of the actual member types of args, it can add anything to the List, say integer.

The code in the original owner of the List - CityNameList, still expects the cityName list to hold only String values. And thus, when the sortAsc method is invoked, it throws java.lang.ClassCastException in protest.

CityNameList cityNameList = new CityNameList();
cityNameList.add("New York");
cityNameList.add("London");
cityNameList.add("Delhi");
cityNameList.process();

// java.lang.ClassCastException: class java.lang.String cannot be 
// cast to class java.lang.Integer
cityNameList.sortAsc();

Even though CityNameList class has declared the cityNameList only to accept String values, it cannot foresee the changes done by SomeOtherClass that works with non-generic code and can modify the List in any way.

CheckedCollection to the rescue

The API developers already expected such scenarios and shipped a couple of utility methods to create checked collections:

1. List<E> checkedList(List<E> list, Class<E> type)
2. <E> Collection<E> checkedCollection(Collection<E> c, Class<E> type)
3. <E> Set<E> checkedSet(Set<E> s, Class<E> type)

and a few others.

The resultant data structure enforces more robust type safety and throws an exception as soon as an incompatible element is inserted. Let’s re-write the CityNameList class to use a checked List instead of an ArrayList:

class CityNameList{
    private final SomeOtherClass someOtherClass = new SomeOtherClass();
    private final List<String> cityNames = Collections.checkedList(new ArrayList<>(), String.class);

    public void add(String name){
        cityNames.add(name);
    }
    public void process(){
        someOtherClass.methodWithNonGenericArgs(cityNames);
    }
    public void sortAsc(){
        Collections.sort(cityNames);
    }
}

Now, when methodWithNonGenericArgs tries to add an Integer value in the args, it fails at the very moment with the following exception:

// Strong Type Safety does not allow the add operation itself.
java.lang.ClassCastException: Attempt to insert class java.lang.Integer element into collection with element type class java.lang.String

This ensures that all the code that expects the cityNameList to hold only String values, does not encounter any surprises.

Even though the failure is still at runtime, it is quite an improvement, especially when working with remote APIs or multiple classes, as now the client does not have to bother about changes made by other classes or components. Also, the developers can now identify the exact code (in case of multiple update points), adding elements that do not adhere to the contract.

A quick look inside the CheckedList<E> static inner class to dig the implementation details lead us to typeCheck method:

E typeCheck(Object o) {
    if (o != null && !type.isInstance(o))
        throw new ClassCastException(badElementMsg(o));
    return (E) o;
}

The method is called from add, addAll, set, offer and other methods of various checked types and fails the original operation, attempting to add any element that fails the isInstance check.

A word of caution: as with everything else, there is a cost associated with the additional benefits provided by typeCheck - not that significant for most of the use-cases, but it is still there. So please consider your use case and any other performance impact before you “over-use” these.

That is all for this post. If you want to share any feedback, please drop me an email, or contact me on any of the social platform. I’ll try to respond at the earliest. Also, please consider subscribing for regular updates.

Be notified of new posts. Subscribe to the RSS feed.