While playing around with the new Java 8 Stream
API I got to wondering, why not:
public interface Map<K,V> extends Function<K, V>
Or even:
public interface Map<K,V> extends Function<K, V>, Predicate<K>
It would be fairly easy to implement with default
methods on the Map
interface
:
@Override default boolean test(K k) {
return containsKey(k);
}
@Override default V apply(K k) {
return get(k);
}
And it would allow for the use of a Map
in a map
method:
final MyMagicMap<String, Integer> map = new MyMagicHashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
map.put("D", 4);
final Stream<String> strings = Arrays.stream(new String[]{"A", "B", "C", "D"});
final Stream<Integer> remapped = strings.map(map);
Or as a Predicate
in a filter
method.
I find that a significant proportion of my use cases for a Map
are exactly that construct or a similar one - as a remapping/lookup Function
.
So, why did the JDK designers not decide to add this functionality to the Map
during the redesign for Java 8?
The JDK team was certainly aware of the mathematical relationship between java.util.Map
as a data structure and java.util.function.Function
as a mapping function. After all, Function
was named Mapper
in early JDK 8 prototype builds. And the stream operation that calls a function on each stream element is called Stream.map
.
There was even a discussion about possibly renaming Stream.map
to something else like transform
because of possible confusion between a transforming function and a Map
data structure. (Sorry, can't find a link.) This proposal was rejected, with the rationale being the conceptual similarity (and that map
for this purpose is in common usage).
The main question is, what would be gained if java.util.Map
were a subtype of java.util.function.Function
? There was some discussion in comments about whether subtyping implies an "is-a" relationship. Subtyping is less about "is-a" relationships of objects -- since we're talking about interfaces, not classes -- but it does imply substitutability. So if Map
were a subtype of Function
, one would be able to do this:
Map<K,V> m = ... ;
source.stream().map(m).collect(...);
Right away we're confronted with baking in the behavior of what is now Function.apply
to one of the existing Map
methods. Probably the only sensible one is Map.get
, which returns null if the key isn't present. These semantics are, frankly, kind of lousy. Real applications are probably going to have to write their own methods that supply key-missing policy anyway, so there seems to be very little advantage of being able to write
map(m)
instead of
map(m::get)
or
map(x -> m.getOrDefault(x, def))
The question is “why should it extend Function
?”
Your example of using strings.map(map)
doesn’t really justify the idea of changing the type inheritance (implying adding methods to the Map
interface), given the little difference to strings.map(map::get)
. And it’s not clear whether using a Map
as a Function
is really that common that it should get that special treatment compared to, e.g. using map::remove
as a Function
or using map::get
of a Map<…,Integer>
as ToIntFunction
or map::get
of a Map<T,T>
as BinaryOperator
.
That’s even more questionable in the case of a Predicate
; should map::containsKey
really get a special treatment compared to map::containsValue
?
It’s also worth noting the type signature of the methods. Map.get
has a functional signature of Object → V
while you suggests that Map<K,V>
should extend Function<K,V>
which is understandable from a conceptional view of maps (or just by looking at the type), but it shows that there are two conflicting expectations, depending on whether you look at the method or at the type. The best solution is not to fix the functional type. Then you can assign map::get
to either Function<Object,V>
or Function<K,V>
and everyone is happy…
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With