I made a method with type arguments, returning a generic type using these type arguments, and taking Function
arguments which also depends on the type arguments. When I use lambdas as arguments, the compiler forces me to specify the type arguments of the method, which feels wrong.
I am designing a utility class with methods to use with Stream.flatMap
. It maps every kind of collection entry to a FlatEntry which contains a key and value element, and can do this on multiple levels with a builder. The affected method is flatEntryMapperBuilder
. Here is the code:
import java.util.function.Function;
import java.util.stream.Stream;
public class GdkStreams
{
public static <T, K, V> Function<T, Stream<FlatEntry<K, V>>> flatEntryMapper(Function<T, K> keyMapper,
Function<T, Stream<V>> valueMapper)
{
return input -> {
K key = keyMapper.apply(input);
return valueMapper.apply(input).map(value -> new FlatEntry<>(key, value));
};
}
public static <T, K, V> FlatEntryMapperBuilder<T, K, V> flatEntryMapperBuilder(Function<T, K> keyMapper,
Function<T, Stream<V>> valueMapper)
{
return new FlatEntryMapperBuilder<>(keyMapper, valueMapper);
}
public static class FlatEntryMapperBuilder<T, K, V>
{
private Function<T, K> keyMapper;
private Function<T, Stream<V>> valueMapper;
private FlatEntryMapperBuilder (Function<T, K> keyMapper, Function<T, Stream<V>> valueMapper)
{
this.keyMapper = keyMapper;
this.valueMapper = valueMapper;
}
public Function<T, Stream<FlatEntry<K, V>>> build()
{
return flatEntryMapper(keyMapper, valueMapper);
}
public <K2, V2> FlatEntryMapperBuilder<T, K, FlatEntry<K2, V2>> chain(Function<V, K2> keyMapper2,
Function<V, Stream<V2>> valueMapper2)
{
return new FlatEntryMapperBuilder<>(keyMapper,
valueMapper.andThen(stream -> stream.flatMap(flatEntryMapper(keyMapper2,
valueMapper2))));
}
}
public static class FlatEntry<K, V>
{
public final K key;
public final V value;
public FlatEntry (K key, V value)
{
this.key = key;
this.value = value;
}
}
}
The problem comes with its usage. Say I have:
Map<String, Set<String>> level1Map;
I can map every element in the sub Sets to a FlatEntry by doing:
level1Map.entrySet().stream().flatMap(GdkStreams.flatEntryMapper(Entry::getKey, entry -> entry.getValue().stream()));
And it works just fine. But when I try to do this:
level1Map.entrySet()
.stream()
.flatMap(GdkStreams.flatEntryMapperBuilder(Entry::getKey, entry -> entry.getValue().stream()).build());
The eclipse (Mars 4.5.0) compiler breaks with:
- The type Map.Entry does not define getKey(Object) that is applicable here
- The method getValue() is undefined for the type Object
- Type mismatch: cannot convert from GdkStreams.FlatEntryMapperBuilder<Object,Object,Object> to
<unknown>
And javac (1.8.0_51) breaks with:
MainTest.java:50: error: incompatible types: cannot infer type-variable(s) T,K#1,V#1
.flatMap(GdkStreams.flatEntryMapperBuilder(Entry::getKey, entry -> entry.getValue().stream()).build());
^
(argument mismatch; invalid method reference
method getKey in interface Entry<K#2,V#2> cannot be applied to given types
required: no arguments
found: Object
reason: actual and formal argument lists differ in length)
where T,K#1,V#1,K#2,V#2 are type-variables:
T extends Object declared in method <T,K#1,V#1>flatEntryMapperBuilder(Function<T,K#1>,Function<T,Stream<V#1>>)
K#1 extends Object declared in method <T,K#1,V#1>flatEntryMapperBuilder(Function<T,K#1>,Function<T,Stream<V#1>>)
V#1 extends Object declared in method <T,K#1,V#1>flatEntryMapperBuilder(Function<T,K#1>,Function<T,Stream<V#1>>)
K#2 extends Object declared in interface Entry
V#2 extends Object declared in interface Entry
MainTest.java:50: error: invalid method reference
.flatMap(GdkStreams.flatEntryMapperBuilder(Entry::getKey, entry -> entry.getValue().stream()).build());
^
non-static method getKey() cannot be referenced from a static context
where K is a type-variable:
K extends Object declared in interface Entry
2 errors
If I replace Entry::getKey
by entry -> entry.getKey()
, javac changes its output drastically:
MainTest.java:51: error: cannot find symbol
.flatMap(GdkStreams.flatEntryMapperBuilder(entry -> entry.getKey(), entry -> entry.getValue().stream()).build());
^
symbol: method getKey()
location: variable entry of type Object
MainTest.java:51: error: cannot find symbol
.flatMap(GdkStreams.flatEntryMapperBuilder(entry -> entry.getKey(), entry -> entry.getValue().stream()).build());
^
symbol: method getValue()
location: variable entry of type Object
2 errors
It compiles fine by specifying type parameters, which is what I expected:
level1Map.entrySet()
.stream()
.flatMap(GdkStreams.<Entry<String, Set<String>>, String, String> flatEntryMapperBuilder(Entry::getKey,
entry -> entry.getValue()
.stream())
.build());
or specifying one of the arguments type parameters:
Function<Entry<String, Set<String>>, String> keyGetter = Entry::getKey;
level1Map.entrySet()
.stream()
.flatMap(GdkStreams.flatEntryMapperBuilder(keyGetter, entry -> entry.getValue().stream()).build());
But this is clumsy! Imagine now how clumsy it would be to write all type parameters with 2 levels in the map, using the chain method (which is my target usage):
Map<String, Map<String, Set<String>>> level2Map;
I have read many other questions about lambdas and generics type inference but none is answering my particular case.
Am I missing something? Can I correct my API so that its usage is less clumsy, or am I stuck with always specifying type arguments? Thanks!
chain
method has more type arguments, but for the presented use case it is not used (I included it for completeness sake). The problem occurs only by usingflatEntryMapperBuilder
. Removingchain
from the typeFlatEntryMapperBuilder
does not change the problem. – ClippedgenericFactoryMethod().build()
. – Fenn.stream().flatMap(flatEntryMapper(Entry::getKey, entry -> entry.getValue().stream().flatMap(flatEntryMapper(…))))
… – Fenn.stream().flatMap(flatEntryMapperBuilder(functionFoo1, functionBar1).chain(functionFoo2, functionBar2).chain(functionFoo3, functionBar3).build())
instead of:.stream().flatMap(flatEntryMapper(functionFoo1, functionBar1.andThen(stream -> stream.flatMap(flatEntryMapper(functionFoo2, functionBar2.andThen(stream -> stream.flatMap(flatEntryMapper(functionFoo3, functionBar3))))))))
– Clipped.stream().flatMap(flatEntryMapper(functionFoo1, functionBar1)) .flatMap(flatEntryMapper(functionFoo2, functionBar2)) .flatMap(flatEntryMapper(functionFoo3, functionBar3))
should do as well… – FennflatEntry -> flatEntry.value.getValue() .stream() .map(value -> new FlatEntry<>(flatEntry.value.getKey(), value)))
– ClippedflatEntryMapper(…)
does. Note that in my code example, it is still being used. – FennMap<String, Map<String, Set<String>>>
into a stream ofFlatEntry<String, FlatEntry<String, String>>
.flatEntryMapper
does its job nicely for one level, but if you have multiple levels you have to chain it like you mentionned in the 5th comment, with nested calls. If you write it not nested, you have to use a complicated function that takes aFlatEntry
as argument and recreate one, because the result of the previous call left you with aStream<FlatEntry<...>>
. – Clipped