2009-05-13 11:28:30 8 Comments

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).


@Erwin Smout 2017-03-23 08:38:17


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.)

@henva 2017-02-09 14:48:15

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.

@Stilgar 2014-12-18 13:00:54

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.

@Apocalisp 2009-05-13 13:38:18

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.


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

@finnw 2009-09-21 18:44:02

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.

@cgp 2012-07-12 15:02:56

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.

@Owheee 2014-01-01 14:37:09

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).

@Sentenza 2014-05-24 17:19:54

why is the set method strongly typed then?

@Owheee 2014-06-17 13:57:08

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"

@newacct 2009-05-13 17:14:45

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.

@user166390 2011-09-14 15:21:25

Then why is V Get(K k) in C#?

@Elazar Leibovich 2012-02-14 12:07:59

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.

@mmm 2012-09-26 12:25:37

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...

@user949300 2013-11-05 04:22:52

+1 to Elazar's comment. PLus, if you really want to be this flexible, use Object as the type for K.

@Sam Goldberg 2013-12-17 18:03:42

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.

@Timmos 2014-02-14 15:11:50

Things get more confusing if working with EnumMap. A map that can only accept an Enum for its key type. One cannot override the equals(Object o) method for an enum, hence the argument that objects from different types can be equal to each other fails if one of these objects is an Enum. So, for an EnumMap, it doesn't make sence not to ask for the specific Enum type which was used to create the EnumMap. But it implements the Map interface so we're stuck with the get(Object o) method here, too.

@mishal153 2014-04-25 11:10:11

One of the big reason for me to use Generics is type safety. And for me this is definitely a hole in that safety net.

@newacct 2014-04-25 18:45:57

@mishal153: It is perfectly type safe. Type safe means no unsafe casts that can fail. This does not add any casts.

@Chris K 2014-07-11 09:10:07

@Sam Goldberg, it strikes me that the reason it ended up this way was to preserve backwards compatibility. That method was designed before Generics came out, and when the retro fit occurred Sun went out of their way to keep older code working the same way that it did before.

@Holger 2014-10-20 18:43:59

@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.

@GlenPeterson 2015-08-14 10:16:44

Note: HashMap.get() relies on equals as you say, but TreeMap.get() relies on a.compareTo(b) == 0, not equals/hashCode.

@newacct 2015-08-14 20:12:11

@GlenPeterson: The contract of the Map interface is to use equals. The TreeMap documentation says that compareTo must be consistent with equals for it to properly implement Map. It is true that it will still work if compareTo is not consistent with equals, but then it isn't a proper implementation of Map.

@GlenPeterson 2015-08-15 01:03:58

@newacct Everything you say is true. But... The whole point of providing a comparator to TreeMap is to define a context for comparison other than the one defined by equals(). Just sayin', when I realized that, it opened up a whole new world of goodness. Now I can define a comparator for some interface, then throw anything into a TreeSet (using that comparator) that implements that interface, even if the actual objects are totally incompatible with each other in terms of their one-sided equals() implementations.

@toniedzwiedz 2016-08-11 12:21:23

Using mutable lists as Map keys doesn't sound like a good idea (as both their equality to other objects as well as the value of their hash code is free to change, which may lead to unexpected consequences) and I don't think it should be provided here as an example as to why Map#get should be using equals.

@newacct 2016-08-12 02:18:10

@toniedzwiedz: You could have immutable lists then. The same issue would apply with different implementations of immutable lists. And this is not explaining why the Map API uses equals -- that has many reasons but is a separate question. This is explaining, given that the Map API uses equals, why Map.get takes Object.

@toniedzwiedz 2016-08-14 19:14:29

@newacct that's not a reason for this implementation, that's the problem with it. Losing compile time checks to cater for questionable design cases. A possible reason being... the Map#get method predates generics and it had to remain broken to maintain backward compatibility. Although at the same time, this wasn't a problem for Map#put which also used to use Object as the type of its parameters prior to Java 5. Your argument could also be applied to it, yet it IS generic now. How is this case different? The way I see it, you're reiterating what the contract is, not explaining why.

@newacct 2016-08-17 05:28:23

@toniedzwiedz: It has nothing to do with "backwards compatibility". In fact, you just proved that "backwards compatibility" has nothing to do with it, by noting that "backwards compatibility" does not prevent something from being generified when generics was added, e.g. for Map#put -- if the type E is indeed the proper intended type for that parameter. The fact that the parameter of Map#get is Object indicates that E is NOT the proper intended type for that parameter -- that if generics was in the first version of Java, the would still have used Object as the type.

@newacct 2016-08-17 05:35:12

@toniedzwiedz: "Your argument could also be applied to it" No it can't. In fact, that is the very reason why the types are different. The first argument of Map#put is the value that is put into the entry in the map, so obviously it must be the right type because it's the value that goes in the map. Map#get is to lookup a value that is already in the map (which has the right type) given an argument that is a different object, that just has to be equal. This passed object is never inserted into the map; it's basically just a selector used to determine which entry to pull out from the map.

@toniedzwiedz 2016-08-17 15:08:46

Then what's wrong with a hypothetical generic Map#get(K key) and restricting the type to List in the example you provided? Why couldn't that work? You're describing how Map#get works and not why it works the way it works. Your example (List) is actually a very good use case for a generic Map#get, it just happens to use an overly restrictive key type.

@newacct 2016-08-18 02:16:39

@toniedzwiedz: Because the restriction is unnecessary (unlike Map#put where the argument is put in and needs to come out as E, Map#get doesn't actually use the argument in a way that requires it to be E) and incorrectly restrictive given its API (the API of Map#get only cares about the behavior of its .equals() method, which does not technically allow you to say anything about its type, given how .equals() is defined in Java).

@newacct 2016-08-18 02:19:40

@toniedzwiedz: From what you have said, it seems that the disagreement basically goes back to your belief that it's "questionable design" for instances of different classes to be equal, and therefore you think APIs should proactively constrain types so that when .equals() is used, it's enforced that both things should be the same type, even though it's not necessary on a language level. Well, I don't share that view, and that's why our conclusions are different.

@Little Santi 2018-11-25 17:36:54

@newacct A Map<ArrayList, Something> object shall not contain a LinkedList key, because put will not allow any key parameter other than ArrayList.

@newacct 2018-11-26 05:13:04

@LittleSanti: I agree, and I never claimed a Map<ArrayList, Something> object contains any LinkedList keys, so I am not sure what you are referring to.

@Little Santi 2018-11-26 09:36:01

@newacct You wrote "is possible to have a Map<ArrayList, Something> and for me to call get() with a LinkedList as argument".

@newacct 2018-11-26 16:38:44

@LittleSanti: And that's absolutely correct. You should be able to call get() with a LinkedList as argument, and that has nothing to do with a LinkedList key being in the Map. Read the contract of get() -- it gets a value in the Map whose key is equal to the passed object, not a key that is the same object. So you can get things out of a Map with get() by passing an argument which is not in the Map.

@Little Santi 2018-11-26 16:57:33

@newacct Not only an argument which is not in the map itself, but that is not even the type parameter defined for that map. Sounds quite, quite weird.

@newacct 2018-11-27 05:38:30

@LittleSanti: Yes, since objects of different types can be equal.

@Holger 2019-07-24 19:56:46

If calling get with a LinkedList when the key type is ArrayList, is an example that must work due to the equality constraint, then put with the same LinkedList should also work when the equal key is already present. Then, in turn computeIfPresent should accept Object as key as well, as it never stores the specified key, but only modifies the map when the equal key is already present. Same for replace, which never stores the specified key.

@newacct 2019-07-25 06:20:53

@Holger: But we don't know at compile-time if an equal key is already present, so they cannot allow you to call put with a value of compile-time type LinkedList. The default implementation for computeIfPresent does store the specified key into the map if the functions returns non-null, and the default implementation of replace does store the specified key into the map if the old value matches the passed value. (the Map interface specifies no method to efficiently get the key in the map that is equal to a given key, nor a method to set a value without also setting the key)

@Holger 2019-07-25 09:08:33

The default implementations of computeIfPresent and replace rely on put, but that clearly specifies “If the map previously contained a mapping for the key, the old value is replaced by the specified value.” So no key replacement for these methods. The JRE has several Set implementations relying on this contract. Regarding put itself, you are right, the compiler can’t check whether the key is already present, but you derived a conclusion for get from the equality definition that does apply to put the same way. So if your conclusion is right, the put method is broken.

@newacct 2019-07-25 16:01:54

@Holger: The specification you quote of Map.put says that if the map contains a mapping for the key, the value is replaced, but it is silent on whether the key is replaced, or whether the old key is retained, so some class that implements Map might replace the key, and it would be unsafe for a default implementation of computeIfPresent or replace to assume that the implementation doesn't store the key.

@newacct 2019-07-25 16:04:31

@Holger: And even if the implementing class doesn't store the key in the case where the mapping already exists, since Map.put() specifies that it takes type K, the implementation of put in an implementing class should be able to rely on the fact that the passed argument is type K, and be able do things to it that can only be done with type K (other than storing it int the map), so it would be unsafe for the default implementation of computeIfPresent or replace to cast something that might not be K into K when it calls put.

@Holger 2019-07-25 16:28:42

Collections.newSetFromMap relies on Map.put not changing an existing key, as otherwise, it would violate the contract of Set.add. The rest of your comment is circular reasoning. So your explanation why get must take Object does not apply to other methods when they do not accept Object but require K. So, in other words, your explanation would be moot if get required K too. The safety argument is pointless, as get isn’t safe either. E.g. TreeMap.get does throw a ClassCastException when the specified object isn’t Comparable resp. incompatible to the Comparator.

@newacct 2019-07-25 20:47:32

@Holger: get and put are methods specified by the Map interface, while computeIfPresent and replace are default implementations added later. If computeIfPresent or replace were methods originally specified by the Map interface that implementing classes had to implement, then the signature could take Object and the implementing class would have to implement the method to not rely on the argument being K.

@newacct 2019-07-25 20:48:46

@Holger: But, computeIfPresent and replace are default implementations that have to work with classes that did not implement them without knowing how the specific implementation works, so they must use put to set the value, and since put takes K (for reasons already explained), and the implementing class is allowed to rely on the argument being K, the default implementations of computeIfPresent and replace must take K also, to pass into put.

@Holger 2019-07-26 08:50:25

Actually, the signatures of the replace methods were already fixed in Java 5, when default methods did not even exist, as the ConcurrentMap interface defines them since then and Map could not introduce methods with a different signature without breaking compatibility. The methods of ConcurrentMap are required to work atomically, which rules out implementing them atop put. The reasoning is incomplete at best, but tends to be just inconsistent. But anyway, that’s already a too long discussion and it doesn’t seem like we’ll find an agreement…

@newacct 2019-07-26 18:40:46

@Holger: I agree that in the case of ConcurrentMap.replace(), it should be able to take Object as the type of the key parameter, since, unlike with Map, replace was a method that ConcurrentMap implementations had to implement from the beginning. I don't know why they didn't use Object.

@erickson 2009-06-25 19:24:33

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.

@user166390 2011-09-14 15:20:58

Then why is V Get(K k) in C#?

@Wormbo 2012-05-29 11:33:19

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.

@Judge Mental 2013-04-29 23:19:04

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.

@Judge Mental 2014-06-10 15:41:52

Of course that should have been contains : K -> boolean.

@Alnitak 2017-03-13 12:58:15

@erickson 2017-03-13 17:43:37

@Alnitak Perhaps. My answer isn't to justify or defend the choice that was made, but simply to answer the question, why was this approach chosen.

@Yardena 2009-05-14 14:16:56

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."

@Judge Mental 2013-04-29 23:20:00

why not containsAll( Collection< ? extends E > c ), then?

@davmac 2014-06-10 12:13:05

@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).

@Judge Mental 2014-06-10 15:51:14

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.

@Jon Skeet 2009-05-13 11:42:15

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...)

@Tom Hawtin - tackline 2009-05-13 12:19:40

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 2009-05-13 13:12:47

The problem that Kevin cites with Set<? extends Foo> is an artificial one, even in Java. If you pass a type parameter <F extends Foo> and then use Set<F>, the problem goes away. So I very much doubt that this is the reason why get, contains, and containsKey take Object.

@Kevin Bourrillion 2009-11-04 16:24:19

Apocalisp: that's not true, the situation is still the same.

@user102008 2012-04-29 01:22:00

It's wrong. The post complains that Set<? extends Foo> would be unable to take any non-null value in its contains() method. But if objects of different types could not be equal (suppose that .equals() throws an exception on an object of a different class), then that restriction would be the right behavior from a type safety point of view. Why would you want to be able to test a set of unknown component type for the membership of an object that may not be the right type? That would be type unsafe. Only when you consider that objects of different types can be equal does it makes sense.

@Kevin Bourrillion 2012-06-19 15:10:30

@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).

@Porculus 2012-09-21 20:52:57

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.

@Earth Engine 2013-03-20 01:46:25

The first example of that blog is now broken because new Long(10).equals(new Integer(10)) is false in Java 7.

@Jon Skeet 2013-03-20 06:46:21

@EarthEngine: It's always been broken. That's the whole point - the code is broken, but the compiler can't catch it.

@GhostCat 2018-08-09 08:54:14

And its still broken, and just caused us a bug ... awesome answer.

@Brian Agnew 2009-05-13 11:34:45

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.

@rudolfson 2009-05-13 11:51:37

It is also dependent on hashCode(). Without a proper implementation of hashCode(), a nicely implemented equals() is rather useless in this case.

@Bill Michell 2009-05-13 12:32:13

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.

@Rob 2009-05-13 17:19:06

@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().

@BeeOnRope 2016-06-02 23:09:16

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.

@Anton Gogolev 2009-05-13 11:33:02

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

@Thilo 2011-09-01 10:02:20

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".

@geekdenz 2016-05-07 07:07:30

Personally, I think the most likely reason for this design decision is backwards compatibility.

Related Questions

Sponsored Content

29 Answered Questions

[SOLVED] Most efficient way to increment a Map value in Java

12 Answered Questions

17 Answered Questions

[SOLVED] What's the reason I can't create generic array types in Java?

21 Answered Questions

[SOLVED] What is reflection and why is it useful?

26 Answered Questions

[SOLVED] What is a serialVersionUID and why should I use it?

17 Answered Questions

2 Answered Questions

1 Answered Questions

Sponsored Content