460

What are the reasons behind the decision to not have a fully generic get method in the interface of java.util.Map<K, V>.

To clarify the question, the signature of the method is

V get(Object key)

instead of

V get(K key)

and I'm wondering why (same thing for remove, containsKey, containsValue).

3

11 Answers 11

289

As mentioned by others, the reason why get(), etc. is not generic because the key of the entry you are retrieving does not have to be the same type as the object that you pass in to get(); the specification of the method only requires that they be equal. This follows from how the equals() method takes in an Object as parameter, not just the same type as the object.

Although it may be commonly true that many classes have equals() defined so that its objects can only be equal to objects of its own class, there are many places in Java where this is not the case. For example, the specification for List.equals() says that two List objects are equal if they are both Lists and have the same contents, even if they are different implementations of List. So coming back to the example in this question, according to the specification of the method is possible to have a Map<ArrayList, Something> and for me to call get() with a LinkedList as argument, and it should retrieve the key which is a list with the same contents. This would not be possible if get() were generic and restricted its argument type.

Sign up to request clarification or add additional context in comments.

37 Comments

Then why is V Get(K k) in C#?
The question is, if you want to call m.get(linkedList), why didn't you define m's type as Map<List,Something>? I can't think of a usecase where calling m.get(HappensToBeEqual) without changing the Map type to get an interface makes sense.
Wow, serious design flaw. You get no compiler warning either, screwed up. I agree with Elazar. If this is really useful, which I doubt happens often, a getByEquals(Object key) sounds more reasonable...
This decision seems like it was made on the basis of theoretical purity rather than practicality. For the majority of usages, developers would much rather see the argument limited by the template type, than to have it unlimited to support edge cases like the one mentioned by newacct in his answer. Leaving the non-templated signatures creates more problems than it solves.
@newacct: “perfectly type safe” is a strong claim for a construct which can fail unpredictably at runtime. Don’t narrow your view to hash maps which happen to work with that. TreeMap may fail when you pass objects of the wrong type to the get method but may pass occasionally, e.g. when the map happens to be empty. And even worse, in case of a supplied Comparator the compare method (which has a generic signature!) might be called with arguments of the wrong type without any unchecked warning. This is broken behavior.
|
122

An awesome Java coder at Google, Kevin Bourrillion, wrote about exactly this issue in a blog post a while ago (admittedly in the context of Set instead of Map). The most relevant sentence:

Uniformly, methods of the Java Collections Framework (and the Google Collections Library too) never restrict the types of their parameters except when it's necessary to prevent the collection from getting broken.

I'm not entirely sure I agree with it as a principle - .NET seems to be fine requiring the right key type, for example - but it's worth following the reasoning in the blog post. (Having mentioned .NET, it's worth explaining that part of the reason why it's not a problem in .NET is that there's the bigger problem in .NET of more limited variance...)

10 Comments

I'm sure Josh Bloch has written about it somewhere. An earlier attempt did use the generic parameter for the parameter, but was found to be too awkward.
Apocalisp: that's not true, the situation is still the same.
@user102008 No, the post is not wrong. Even though an Integer and a Double can never be equal to one another, it's still a fair question to ask whether a Set<? extends Number> contains the value new Integer(5).
I have never once wanted to check membership in a Set<? extends Foo>. I have very frequently changed the key type of a map and then been frustrated that the compiler could not find all the places where the code needed updating. I am really not convinced that this is the correct tradeoff.
@EarthEngine: It's always been broken. That's the whole point - the code is broken, but the compiler can't catch it.
|
31

The contract is expressed thus:

More formally, if this map contains a mapping from a key k to a value v such that (key==null ? k==null : key.equals(k)), then this method returns v; otherwise it returns null. (There can be at most one such mapping.)

(my emphasis)

and as such, a successful key lookup depends on the input key's implementation of the equality method. That is not necessarily dependent on the class of k.

5 Comments

It is also dependent on hashCode(). Without a proper implementation of hashCode(), a nicely implemented equals() is rather useless in this case.
I guess, in principle, this would let you use a lightweight proxy for a key, if recreating the whole key was impractical - as long as equals() and hashCode() are correctly implemented.
@rudolfson: As far as I'm aware, only a HashMap is reliant upon the hash code to find the correct bucket. A TreeMap, for example, uses a binary search tree, and doesn't care about hashCode().
Strictly speaking, get() does not need to take an argument of type Object to satisfy the contact. Imagine the get method were restricted to the key type K - the contract would still be valid. Of course, uses where the compile time type was not a subclass of K would now fail to compile, but that doesn't invalidate the contract, since contracts implicitly discuss what happens if the code compiles.
When we talk about the contract, especially around key.equals(k), I think it is important to also note that the contract also allows simply throwing a ClassCastException is key is of an incompatible type. This reduces the usefulness of remove(Object) as a sort of removeIf(Predicate), because it depends on the collection implementation. If we are depending on a particular collection implementation's behavior, then maybe that should be exposed as a method of that particular implementation and the collection interface should have consistent behavior for all implementations.
15

It's an application of Postel's Law, "be conservative in what you do, be liberal in what you accept from others."

Equality checks can be performed regardless of type; the equals method is defined on the Object class and accepts any Object as a parameter. So, it makes sense for key equivalence, and operations based on key equivalence, to accept any Object type.

When a map returns key values, it conserves as much type information as it can, by using the type parameter.

6 Comments

Then why is V Get(K k) in C#?
It's V Get(K k) in C# because it also makes sense. The difference between the Java and .NET approaches is really only who blocks off non-matching stuff. In C# it's the compiler, in Java it's the collection. I rage about .NET's inconsistent collection classes once in a while, but Get() and Remove() only accepting a matching type certainly prevents you from accidentally passing a wrong value in.
It's a mis-application of Postel's Law. Be liberal in what you accept from others, but not too liberal. This idiotic API means that you can't tell the difference between "not in the collection" and "you made a static typing mistake". Many thousands of lost programmer hours could have been prevented with get : K -> boolean.
Of course that should have been contains : K -> boolean.
|
14

I think this section of Generics Tutorial explains the situation (my emphasis):

"You need to make certain that the generic API is not unduly restrictive; it must continue to support the original contract of the API. Consider again some examples from java.util.Collection. The pre-generic API looks like:

interface Collection { public boolean containsAll(Collection c); ... } 

A naive attempt to generify it is:

interface Collection<E> { public boolean containsAll(Collection<E> c); ... } 

While this is certainly type safe, it doesn’t live up to the API’s original contract. The containsAll() method works with any kind of incoming collection. It will only succeed if the incoming collection really contains only instances of E, but:

  • The static type of the incoming collection might differ, perhaps because the caller doesn’t know the precise type of the collection being passed in, or perhaps because it is a Collection<S>,where S is a subtype of E.
  • It’s perfectly legitimate to call containsAll() with a collection of a different type. The routine should work, returning false."

3 Comments

why not containsAll( Collection< ? extends E > c ), then?
@JudgeMental, though not given as an example above it is also necessary to allow containsAll with a Collection<S> where S is a supertype of E. This would not be allowed if it were containsAll( Collection< ? extends E > c ). Furthermore, as is explicitly stated in the example, it's legitimate to pass a collection of a different type (with the return value then being false).
It should not be necessary to allow containsAll with a collection of a supertype of E. I argue that it is necessary to disallow that call with a static type check to prevent a bug. It's a silly contract, which I think is the point of the original question.
9

Compatibility.

Before generics were available, there was just get(Object o).

Had they changed this method to get(<K> o) it would have potentially forced massive code maintenance onto java users just to make working code compile again.

They could have introduced an additional method, say get_checked(<K> o) and deprecate the old get() method so there was a gentler transition path. But for some reason, this was not done. (The situation we are in now is that you need to install tools like findBugs to check for type compatibility between the get() argument and the declared key type <K> of the map.)

The arguments relating to the semantics of .equals() are bogus, I think. (Technically they're correct, but I still think they're bogus. No designer in his right mind is ever going to make o1.equals(o2) true if o1 and o2 do not have any common superclass.)

2 Comments

But there also the method put(Object key, Object value) has been changed to put(K key, V value), and no problem with that!
That's presumably because if it's "working code", then the objects being passed to any put method invocation for key and value would already "naturally" satisfy the stricter constraints. Thus there might have been an expectation that "working code" would simply recompile without review in +- 99% of the cases. If not more.
6

The reason is that containment is determined by equals and hashCode which are methods on Object and both take an Object parameter. This was an early design flaw in Java's standard libraries. Coupled with limitations in Java's type system, it forces anything that relies on equals and hashCode to take Object.

The only way to have type-safe hash tables and equality in Java is to eschew Object.equals and Object.hashCode and use a generic substitute. Functional Java comes with type classes for just this purpose: Hash<A> and Equal<A>. A wrapper for HashMap<K, V> is provided that takes Hash<K> and Equal<K> in its constructor. This class's get and contains methods therefore take a generic argument of type K.

Example:

HashMap<String, Integer> h = new HashMap<String, Integer>(Equal.stringEqual, Hash.stringHash); h.add("one", 1); h.get("one"); // All good h.get(Integer.valueOf(1)); // Compiler error 

2 Comments

This in itself does not prevent the type of 'get' from being declared as "V get(K key)", because 'Object' is always an ancestor of K, so "key.hashCode()" would still be valid.
While it doesn't prevent it, I think it explains it. If they switched the equals method to force class equality, they certainly couldn't tell people that the underlying mechanism for locating the object in the map utilizes equals() and hashmap() when the method prototypes for those methods aren't compatible.
5

There is one more weighty reason, it can not be done technically, because it brokes Map.

Java has polymorphic generic construction like <? extends SomeClass>. Marked such reference can point to type signed with <AnySubclassOfSomeClass>. But polymorphic generic makes that reference readonly. The compiler allows you to use generic types only as returning type of method (like simple getters), but blocks using of methods where generic type is argument (like ordinary setters). It means if you write Map<? extends KeyType, ValueType>, the compiler does not allow you to call method get(<? extends KeyType>), and the map will be useless. The only solution is to make this method not generic: get(Object).

2 Comments

why is the set method strongly typed then?
if you mean 'put': The put() method changes map and it will not be avaliable with generics like <? extends SomeClass>. If you call it you got compile exception. Such map will be "readonly"
3

We are doing big refactoring just now and we were missing this strongly typed get() to check that we did not missed some get() with old type.

But I found workaround/ugly trick for compilation time check: create Map interface with strongly typed get, containsKey, remove... and put it to java.util package of your project.

You will get compilation errors just for calling get(), ... with wrong types, everything others seems ok for compiler (at least inside eclipse kepler).

Do not forget to delete this interface after check of your build as this is not what you want in runtime.

Comments

2

Backwards compatibility, I guess. Map (or HashMap) still needs to support get(Object).

2 Comments

But the same argument could be made for put (which does restrict the generic types). You get backwards compatibility by using raw types. Generics are "opt-in".
Personally, I think the most likely reason for this design decision is backwards compatibility.
2

I was looking at this and thinking why they did it this way. I don't think any of the existing answers explains why they couldn't just make the new generic interface accept only the proper type for the key. The actual reason is that even though they introduced generics they did NOT create a new interface. The Map interface is the same old non-generic Map it just serves as both generic and non-generic version. This way if you have a method that accepts non-generic Map you can pass it a Map<String, Customer> and it would still work. At the same time the contract for get accepts Object so the new interface should support this contract too.

In my opinion they should have added a new interface and implemented both on existing collection but they decided in favor of compatible interfaces even if it means worse design for the get method. Note that the collections themselves would be compatible with existing methods only the interfaces wouldn't.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.