当前位置: 动力学知识库 > 问答 > 编程问答 >

generics - 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 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!

网友答案:

Holger had the best answer in the comment section in my opinion:

This is a known limitation of Java 8’s type inference: it doesn’t work with chained method invocations like genericFactoryMethod().build().

Thanks! About my API, I will specify the functions before using them as arguments, like this:

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

EDIT: I redesigned the API thanks to Holger's comments (thanks again!). It keeps the original element instead of a 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 chainable, starting with level 2 the mapper has to process a FlatEntry. The usage is similar to 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()));
网友答案:

One way to provide enough type information to the compiler is to declare an explicit type of one of the lambda argument. This is in the same spirit as your answer but a little more compact, since you only have to provide the type of the argument, not the whole function.

This looks pretty okay for the one-level map:

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

The two-level map is 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());
分享给朋友:
您可能感兴趣的文章:
随机阅读: