Saturday, July 26, 2008

Java Collections Listeners

Maybe I've been looking at various JavaScript frameworks, etc., for too long, but I'm surprised that there's really no positive way to listen for add/remove/update events within Java's Collections Framework.

Assumptions / Use Case:

  1. Implement the Map interface. This allows for passing into other methods that only accept an implementation of Map, as well as providing a consistent and familiar API to other developers.
  2. Add some additional processing, e.g.:
    • Disallowing certain keys or values from being inserted.
    • Keeping one or more associated maps or sets for performance, e.g. a map of lists when an entry needs to be obtained by value rather than key.
    • Extending HashMap into an "OrderedMap", where unique keys are still guaranteed, but the order of insertions is also kept.

Considerations

Clearly public interfaces are designed to be implemented, and public abstract classes are available to extend. Sun's "Custom Implementations" lesson in the Collections tutorial demonstrates this. As the lesson describes, there are several abstract classes available to serve as a starting point for a custom collection implementation. However, extending these to match the full functionality and efficiency of the existing concrete implementations is not a small effort, especially for Map implementations. The most significant features to note are the methods that return a "view" to a different part or representation of the map, e.g. keySet(), values(), and entrySet(). All are documented to return a view of the Map, such that changes in the view are reflected in the map, and vice-versa.

If that's not already a lot of extra work, consider the methods that are available on the returned view that should also remain functional, such as the iterator()'s remove() method, or worse, the List.listIterator() method. Sure, many of these are listed as "optional operations", which may simply throw an UnsupportedOperationException for simple implementations. However, this can be frustrating to developers attempting to use these methods, and some code may depend on these methods to function.

One solution to save a lot of work is to extend an existing concrete implementation, e.g. HashMap. However, there always seems to be some debate over extending a non-abstract class. My view is that if the class wasn't meant to be extended, it should be marked final or have less-than-public constructors. (As a compromise, the class could remain open to extension, while protecting various methods by marking them final.) java.util.Properties is one example of a public class that extends java.util.Hashtable, another concrete, public class.

Considerable progress seems possible on a HashMap subclass by simply overriding the put(K, V), remove(Object), and clear() methods. However, as described above, the "view" methods provide alternate access points to modify the underlying collection data, not all of which chain down to the overridden methods. This can quickly lead to an incomplete and buggy class.

My working solution

The best approach I've found is to follow the recommendation in the tutorial, and to extend AbstractMap. A child HashMap can then be held as an instance variable, with most of the AbstractMap methods delegated or proxied to it. For returning the "view" methods, they can be made "unmodifiable" by wrapping them in calls to the Collections.unmodifiable*(…) wrapper methods. While this may lead to some frustrations defined above, it provides a solid class that still properly implements the core collection methods.

I did find one Sun bug report that is a request for enhancement regarding this issue: 5078552 - "(coll) ChangeListener, VetoableChangeListener for Collections, Lists and others". It was submitted in July 2004, and hasn't yet received any attention since.

1 comment:

Anonymous said...

Indeed, public non-abstract classes exist against intentions and can be abused by extending them. See this nice confession by Josh Bloch ;-) http://www.artima.com/intv/blochP.html.
Still, when you have a choice, like with Map where it is an interface, you can use and hide that public HashMap class: create your class by implementing Map and delegate to a private HashMap instance. Then you have best of both worlds.