450

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

WMR
  • 12,661
  • 5
  • 35
  • 30
  • 4
    Similar question regarding Collection: http://stackoverflow.com/questions/104799/why-arent-java-collections-remove-methods-generic – AlikElzin-kilaka Sep 04 '12 at 12:37
  • 1
    Possible duplicate of [Why aren't Java Collections remove methods generic?](http://stackoverflow.com/questions/104799/why-arent-java-collections-remove-methods-generic) – Ole V.V. Oct 26 '16 at 04:45
  • 5
    Amazing. I am using Java since 20+ years, and today I realize this problem. – GhostCat Aug 09 '18 at 08:52

11 Answers11

285

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.

Dave Newton
  • 158,873
  • 26
  • 254
  • 302
newacct
  • 119,665
  • 29
  • 163
  • 224
  • 32
    Then why is `V Get(K k)` in C#? –  Sep 14 '11 at 15:21
  • 148
    The question is, if you want to call `m.get(linkedList)`, why didn't you define `m`'s type as `Map`? I can't think of a usecase where calling `m.get(HappensToBeEqual)` without changing the `Map` type to get an interface makes sense. – Elazar Leibovich Feb 14 '12 at 12:07
  • 66
    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... – mjs Sep 26 '12 at 12:25
  • 3
    +1 to Elazar's comment. PLus, if you really want to be this flexible, use Object as the type for K. – user949300 Nov 05 '13 at 04:22
  • 44
    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. – Sam Goldberg Dec 17 '13 at 18:03
  • 6
    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. – Timmos Feb 14 '14 at 15:11
  • 6
    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. – mishal153 Apr 25 '14 at 11:10
  • 2
    @mishal153: It is perfectly type safe. Type safe means no unsafe casts that can fail. This does not add any casts. – newacct Apr 25 '14 at 18:45
  • 8
    @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. – Chris K Jul 11 '14 at 09:10
  • 17
    @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. – Holger Oct 20 '14 at 18:43
  • 3
    Note: HashMap.get() relies on equals as you say, but TreeMap.get() relies on a.compareTo(b) == 0, not equals/hashCode. – GlenPeterson Aug 14 '15 at 10:16
  • 1
    @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`. – newacct Aug 14 '15 at 20:12
  • 1
    @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. – GlenPeterson Aug 15 '15 at 01:03
  • 6
    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`. – toniedzwiedz Aug 11 '16 at 12:21
  • 2
    @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`. – newacct Aug 12 '16 at 02:18
  • 6
    @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. – toniedzwiedz Aug 14 '16 at 19:14
  • 1
    @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 Aug 17 '16 at 05:28
  • 1
    @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. – newacct Aug 17 '16 at 05:35
  • 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. – toniedzwiedz Aug 17 '16 at 15:08
  • @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 Aug 18 '16 at 02:16
  • @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. – newacct Aug 18 '16 at 02:19
  • @newacct A `Map` object shall not contain a `LinkedList` key, because `put` will not allow any key parameter other than `ArrayList`. – Little Santi Nov 25 '18 at 17:36
  • @LittleSanti: I agree, and I never claimed a `Map` object contains any `LinkedList` keys, so I am not sure what you are referring to. – newacct Nov 26 '18 at 05:13
  • @newacct You wrote "is possible to have a Map and for me to call get() with a LinkedList as argument". – Little Santi Nov 26 '18 at 09:36
  • @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`. – newacct Nov 26 '18 at 16:38
  • @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. – Little Santi Nov 26 '18 at 16:57
  • @LittleSanti: Yes, since objects of different types can be equal. – newacct Nov 27 '18 at 05:38
  • 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`](https://docs.oracle.com/javase/8/docs/api/java/util/Map.html#computeIfPresent-K-java.util.function.BiFunction-) 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. – Holger Jul 24 '19 at 19:56
  • @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) – newacct Jul 25 '19 at 06:20
  • 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. – Holger Jul 25 '19 at 09:08
  • @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 Jul 25 '19 at 16:01
  • @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`. – newacct Jul 25 '19 at 16:04
  • `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`. – Holger Jul 25 '19 at 16:28
  • @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 Jul 25 '19 at 20:47
  • @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`. – newacct Jul 25 '19 at 20:48
  • 1
    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… – Holger Jul 26 '19 at 08:50
  • @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`. – newacct Jul 26 '19 at 18:40
117

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

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 1
    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. – Tom Hawtin - tackline May 13 '09 at 12:19
  • The problem that Kevin cites with Set extends Foo> is an artificial one, even in Java. If you pass a type parameter and then use Set, the problem goes away. So I very much doubt that this is the reason why get, contains, and containsKey take Object. – Apocalisp May 13 '09 at 13:12
  • 4
    Apocalisp: that's not true, the situation is still the same. – Kevin Bourrillion Nov 04 '09 at 16:24
  • 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. – user102008 Apr 29 '12 at 01:22
  • 10
    @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)`. – Kevin Bourrillion Jun 19 '12 at 15:10
  • 35
    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. – Porculus Sep 21 '12 at 20:52
  • The first example of that blog is now broken because `new Long(10).equals(new Integer(10))` is false in Java 7. – Earth Engine Mar 20 '13 at 01:46
  • 6
    @EarthEngine: It's always been broken. That's the whole point - the code is broken, but the compiler can't catch it. – Jon Skeet Mar 20 '13 at 06:46
  • 1
    And its still broken, and just caused us a bug ... awesome answer. – GhostCat Aug 09 '18 at 08:54
  • @TomHawtin-tackline Maybe the reference in this answer? https://stackoverflow.com/a/105812/257299 – kevinarpe Sep 15 '20 at 16:02
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.

Brian Agnew
  • 268,207
  • 37
  • 334
  • 440
  • 5
    It is also dependent on `hashCode()`. Without a proper implementation of hashCode(), a nicely implemented `equals()` is rather useless in this case. – rudolfson May 13 '09 at 11:51
  • 5
    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. – Bill Michell May 13 '09 at 12:32
  • 6
    @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(). – Rob May 13 '09 at 17:19
  • 4
    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. – BeeOnRope Jun 02 '16 at 23:09
  • 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. – Jesse Feb 01 '22 at 14:07
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.

Community
  • 1
  • 1
erickson
  • 265,237
  • 58
  • 395
  • 493
  • 4
    Then why is `V Get(K k)` in C#? –  Sep 14 '11 at 15:20
  • 2
    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. – Wormbo May 29 '12 at 11:33
  • 28
    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 Apr 29 '13 at 23:19
  • 1
    Of course that should have been `contains : K -> boolean`. – Judge Mental Jun 10 '14 at 15:41
  • 7
    [Postel was wrong](https://tools.ietf.org/html/draft-thomson-postel-was-wrong-00). – Alnitak Mar 13 '17 at 12:58
  • 1
    @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. – erickson Mar 13 '17 at 17:43
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."
Yardena
  • 2,837
  • 20
  • 17
  • 2
    why not `containsAll( Collection< ? extends E > c )`, then? – Judge Mental Apr 29 '13 at 23:20
  • 1
    @JudgeMental, though not given as an example above it is also necessary to allow `containsAll` with a `Collection` 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`). – davmac Jun 10 '14 at 12:13
  • 1
    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. – Judge Mental Jun 10 '14 at 15:51
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.)

Mr Lister
  • 45,515
  • 15
  • 108
  • 150
Erwin Smout
  • 18,113
  • 4
  • 33
  • 52
  • 2
    But there also the method put(Object key, Object value) has been changed to put(K key, V value), and no problem with that! – Manuel Romeiro Jul 14 '20 at 16:50
  • 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. – Erwin Smout Jun 09 '23 at 21:49
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
Apocalisp
  • 34,834
  • 8
  • 106
  • 155
  • 5
    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. – finnw Sep 21 '09 at 18:44
  • 1
    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. – cgp Jul 12 '12 at 15:02
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).

Holger
  • 285,553
  • 42
  • 434
  • 765
Owheee
  • 51
  • 1
  • 3
  • why is the set method strongly typed then? – Sentenza May 24 '14 at 17:19
  • 1
    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" – Owheee Jun 17 '14 at 13:57
2

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.

henva
  • 31
  • 3
1

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.

Stilgar
  • 22,354
  • 14
  • 64
  • 101
1

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

Anton Gogolev
  • 113,561
  • 39
  • 200
  • 288
  • 14
    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". – Thilo Sep 01 '11 at 10:02
  • Personally, I think the most likely reason for this design decision is backwards compatibility. – geekdenz May 07 '16 at 07:07