Wednesday, November 14, 2007

Map.containsKey(Object key)

The fact that Map methods don't consistently take typed (K and/or V) or untyped (Object) argument(s) is baffling to me. Didn't Java 5 introduce generics for a reason? The put() method only accepts arguments of type K and V, yet containsKey() accepts any Object. Why the inconsistency? If the containsKey argument isn't of type K, it's an error (as indicated by the fact that containsKey throws ClassCastException). Wasn't one of the reasons for introducing generics to catch such errors at compile time?

What's worse (for me, at least) is that this odd mish-mash of strong and weak typing introduces an opportunity for bugs that couldn't exist before. A dilemma I recently found myself in was a HashMap which didn't seem to be able to determine when two keys were equal to each other. I was using a key type of my own construction, call it CustomKey. The problem was that I hadn't defined an equals(Object) method. It wasn't that I didn't think I needed to override the Object equals() method. Rather, I thought that the Map would be calling equals(CustomKey). The keys were typed as such, so I couldn't imagine that HashMap would be casting them to Object before calling equals. But, that's exactly what Map does. Well, it's a bit more subtle than that. HashMap doesn't explicitly cast the key to an Object. Rather, put(K,V) calls containsKey(Object) to determine whether the key already exists. Calling containsKey(Object) on the key effectively casts it to Object so that when equals is called within containsKey, the equals(CustomKey) is ignored in favor of equals(Object).

A careful read of the API docs will make it obvious that you should override equals(Object) for any custom key. But, I wonder why they had to make it so non-intuitive? Actually, nevermind, I can make a well-educated guess: backwards compatibility. Though, if that's the case, I must wonder: why update the put() method for genetics, but not containsKey(), containsValue(), get(), and remove()? And, I further wonder---why not explain the rationale in the API documentation? Even in the fifth edition of Java In A Nutshell by David Flanagan I haven't been able to find a relevant discussion...