Cannot use Java 8 method with lambda arguments without specifying type arguments

I made a method with type arguments, returning a generic type using these type arguments, and accepting Function arguments, which also depend on the type arguments. When I use lambdas as arguments, the compiler forces me to specify arguments like the method that seems wrong.

I am developing a utility class with methods for use with Stream.flatMap . It displays each type of collection entry in FlatEntry, which contains a key and value element, and can do this at several levels using the builder. flatEntryMapperBuilder method. 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 is with its use. Let's say I have:

 Map<String, Set<String>> level1Map; 

I can display each element in subsets in FlatEntry by doing:

 level1Map.entrySet().stream().flatMap(GdkStreams.flatEntryMapper(Entry::getKey, entry -> entry.getValue().stream())); 

And everything works fine. But when I try to do this:

 level1Map.entrySet() .stream() .flatMap(GdkStreams.flatEntryMapperBuilder(Entry::getKey, entry -> entry.getValue().stream()).build()); 

The eclipse compiler (Mars 4.5.0) is broken into:

 - 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) is broken into:

 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 with entry -> entry.getKey() , javac will dramatically change its result:

 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, specifying type parameters as I expected:

 level1Map.entrySet() .stream() .flatMap(GdkStreams.<Entry<String, Set<String>>, String, String> flatEntryMapperBuilder(Entry::getKey, entry -> entry.getValue() .stream()) .build()); 

or by 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 awkward! Imagine how awkward it would be to write all type parameters with 2 levels on the map using the chain method (which is my target use):

 Map<String, Map<String, Set<String>>> level2Map; 

I read a lot of other questions about pins like lambdas and generics, but no one answers my specific case.

Am I missing something? Can I fix my API so that its use is less awkward, or am I always stuck with type arguments? Thanks!

+8
java generics lambda java-8 type-inference
source share
2 answers

Holger had the best answer in the comments section, in my opinion:

This is a well-known limitation of output like Java 8s: it does not work with chain calls like genericFactoryMethod().build() .

Thanks! About my API, I will indicate functions before using them as arguments, for example:

 Function<Entry<String, Set<String>>, String> keyMapper = Entry::getKey; Function<Entry<String, Set<String>>, Stream<String>> valueMapper = entry -> entry.getValue().stream(); 

EDIT: I reworked the API thanks to Holger's comments (thanks again!). It saves the original element instead of the key along with the flattened value.

 public static <T, R> Function<? super T, Stream<FlatEntry<T, R>>> flatEntryMapper(Function<? super T, ? extends Stream<? extends R>> mapper) { return element -> mapper.apply(element).map(value -> new FlatEntry<>(element, value)); } public static class FlatEntry<E, V> { /** The original stream element */ public final E element; /** The flattened value */ public final V value; private FlatEntry (E element, V value) { this.element = element; this.value = value; } } 

It is connected to the chain, starting from level 2, the handler must handle FlatEntry . Usage is like a simple flatMap :

 Map<String, Map<String, Map<String, Set<String>>>> level3Map; // gives a stream of all the flattened values level3Map.entrySet() .stream() .flatMap(entry -> entry.getValue().entrySet().stream()) .flatMap(entry -> entry.getValue().entrySet().stream()) .flatMap(entry -> entry.getValue().stream()); // gives a stream of FlatEntries with flattened values and all their original elements in nested FlatEntries level3Map.entrySet() .stream() .flatMap(GdkStreams.flatEntryMapper(entry -> entry.getValue().entrySet().stream())) .flatMap(GdkStreams.flatEntryMapper(flatEntry -> flatEntry.value.getValue().entrySet().stream())) .flatMap(GdkStreams.flatEntryMapper(flatEntry -> flatEntry.value.getValue().stream())); 
+8
source share

One way to provide sufficient type information to the compiler is to declare an explicit type for one of the lambda arguments. This is in the same vein as your answer , but a little more compact, since you only need to specify the type of the argument, not the entire function.

This looks pretty good for a single-level map:

 level1Map.entrySet().stream() .flatMap(GdkStreams.flatEntryMapperBuilder( (Entry<String, Set<String>> entry) -> entry.getKey(), entry -> entry.getValue().stream()).build()); 

A two-level map is located on the border of the grotesque, however:

 level2Map.entrySet().stream() .flatMap(GdkStreams.flatEntryMapperBuilder( (Entry<String, Map<String, Set<String>>> entry1) -> entry1.getKey(), entry1 -> entry1.getValue().entrySet().stream() .flatMap(GdkStreams.flatEntryMapperBuilder( (Entry<String, Set<String>> entry2) -> entry2.getKey(), entry2 -> entry2.getValue().stream()).build())).build()); 
+3
source share

All Articles