I have the following map
Map<Integer, ? extends Collection<Integer>> map
Sometimes there is a List
and sometimes there is a Set
as a value. Now I'd like to get value, but there is a problem, it does not compile.
Collection<Integer> value = map.getOrDefault(1, Collections.emptyList())
I know I can do something like this:
Collection<Integer> value = map.get(1);
if (value == null) {
value = Collections.emptyList();
}
But I want to understand how getOrDefault()
method is supposed to work with wild-card generics.
CodePudding user response:
It's a very vied case - in short, you can't make getOrDefalt()
working with an upper bounded Value.
default V getOrDefault(Object key, V defaultValue)
As the second argument, getOrDefalt()
expects an instance of type V
. That means that the argument should match exactly the type of the Value how it was declared, and in this case it's ? extends Collection<Integer>
.
We can not perform type casting using wild cards. This code will be syntactically invalid:
Collection<Integer> value =
m.getOrDefault(1, (? extends Collection<Integer>) Collections.emptyList()); // will not compile
There's no way to provide the default value of the type that's required.
CodePudding user response:
The short of it: You cannot use getOrDefault
, at all, with any map whose 'V' is declared to be ?
or ? extends Anything
. There is no fixing this short of cloning the JDK sources or making a utility method with your own take on getOrDefault
.
A getOrDefault
implementation is imaginable that would not have this problem. Unfortunately, the OpenJDK sources simply didn't write it properly, or there is some tradeoff I'm not seeing. Unfortunately, 'fixing it' is backwards incompatible, so it's probably never going to happen; we're stuck with the broken getOrDefault. Thank you for this question - by thinking about it, it made me realize it is broken. I now feel bad for not having given feedback on amber-dev when feedback was asked :)
The explanation for why is a bit detailed and requires good intuition and knowledge about how generics work. Put on your thinking cap and read on if you are intrigued!
So, why?
Map<Integer, ? extends Collection<Integer>> map
Okay: This means that this map
variable could point at, for example, a new HashMap<Integer, List<Integer>>()
. Or, it could point at a new HashMap<Integer, Set<Integer>>()
or even a new HashMap<Integer, Collection<Integer>>()
.
map.getOrDefault(....)
Uhoh. This is not possible. You need to provide a default value, and this default value must 'work' regardless of what the map is actually pointing at - it needs to be the same type as the values in this map are. Imagine you have a Map<Integer, Set<Integer>>
- what would work? Well, new HashSet<Integer>()
would work. But what would work for all 3 (Set<Integer>
, List<Integer>
, and Collection<Integer>
, and of course any other collection type - there are an infinite amount of them)?
The answer is nothing.
You can't do this. You can't use getOrDefault
, at all, here. Well, except if you trivially pass null
, literally, which is the only value that is 'all types', but then you should just write .get(k)
instead of .getOrDefault(k, null)
of course.
The fixed version
This is the implementation of getOrDefault
, pasted straight from the OpenJDK sources:
default V getOrDefault(Object key, V defaultValue) {
V v;
return (((v = get(key)) != null) || containsKey(key))
? v
: defaultValue;
}
This fundamentally is never going to work for you - you can have no V if V is ? anything
- that ? means: We do not know what the type is, and there is no typevar that represents it either, so no expression could ever fit. This take on getOrDefault would work fine here:
default <Z super V> getOrDefault(Object key, Z defaultValue) {
Z z;
return (((z = get(key) != null) || containsKey(key))
? z
: defaultValue;
}
What's happening here is that in this case, whilst the map is perhaps a Set, or List, etc - you just want it as Collection<Integer>
, you don't actually 'care', and you're fine if the default value is not, in fact, a 'V' (If your map is actually a Map<Integer, List<Integer>>
, you don't mind if calling getOrDefault ends up giving you an object that is not a List<Integer>
- Collections.empty()
would not be).
Thus, we need to establish that there is some new type Z which is a supertype of V (thus guaranteeing that if the key is in the map, you get a V, which is definitely a kind of Z, given that Z is a supertype of V as per declaration), and there is a default value that is also definitely of type Z, thus guaranteeing that in either 'branch' (key is found, and key is not found), the returned value is at least Z.
But, map does not work that way, and thus you can't use .getOrDefault
, at all.
I don't think modifying getOrDefault
at this point would be backwards compatible, so I don't think there is any point filing a feature request with the openjdk core team: They'll just reject it, we're stuck with getOrDefault as written. But, you can make a static utility method if you must have it that does the above. Note that you may want to write it differently - cast to raw, do the work in raw mode, ignore the warnings, and clean up afterwards. In theory some Map implementations could have a different impl of getOrDefault, though I'm having a hard time to imagine what that would look like (unlike, say, computeIfAbsent
which absolutely has crucial custom implementations, e.g. in ConcurrentHashMap).