【问题标题】:Cannot use Java 8 method with lambda arguments without specifying type arguments无法在不指定类型参数的情况下使用带有 lambda 参数的 Java 8 方法
【发布时间】:2016-01-06 02:34:46
【问题描述】:

我用类型参数创建了一个方法,使用这些类型参数返回一个泛型类型,并采用Function 参数,这也取决于类型参数。当我使用 lambdas 作为参数时,编译器强制我指定方法的类型参数,感觉不对。

我正在设计一个实用程序类,其中包含与Stream.flatMap 一起使用的方法。它将每种集合条目映射到包含键和值元素的 FlatEntry,并且可以使用构建器在多个级别上执行此操作。受影响的方法是flatEntryMapperBuilder。代码如下:

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;
        }
    }
}

问题在于它的用法。说我有:

Map<String, Set<String>> level1Map;

我可以通过以下方式将子集中的每个元素映射到 FlatEntry:

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

而且效果很好。但是当我尝试这样做时:

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

eclipse (Mars 4.5.0) 编译器中断:

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

而 javac (1.8.0_51) 中断:

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

如果我将Entry::getKey 替换为entry -&gt; entry.getKey(),javac 会大幅改变其输出:

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

通过指定类型参数可以很好地编译,这是我所期望的:

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

或指定参数类型参数之一:

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

但这很笨拙!现在想象一下,使用链式方法(这是我的目标用法)在映射中编写具有 2 个级别的所有类型参数是多么笨拙:

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

我已经阅读了许多关于 lambda 和泛型类型推断的其他问题,但没有一个回答我的特殊情况。

我错过了什么吗?我可以更正我的 API 以使其使用不那么笨拙,还是我总是坚持指定类型参数?谢谢!

【问题讨论】:

  • 在您的实际代码中,您还有两个类型参数 k2,v2 ,应该是 K 和 V 吗?
  • 这是 Java 8 类型推断的一个已知限制:它不适用于像 genericFactoryMethod().build() 这样的链式方法调用。
  • 但是您不需要构建器,因为您可以就地链接函数:.stream().flatMap(flatEntryMapper(Entry::getKey, entry -&gt; entry.getValue().stream().flatMap(flatEntryMapper(…))))...
  • 你不应该需要函数组合。 .stream().flatMap(flatEntryMapper(functionFoo1, functionBar1)) .flatMap(flatEntryMapper(functionFoo2, functionBar2)) .flatMap(flatEntryMapper(functionFoo3, functionBar3)) 也应该这样做……
  • 真的吗?我认为这就是flatEntryMapper(…) 所做的。请注意,在我的代码示例中,它仍在使用中。

标签: java generics lambda java-8 type-inference


【解决方案1】:

在我看来,Holger 在评论部分给出了最好的答案:

这是 Java 8 类型推断的一个已知限制:它不适用于像 genericFactoryMethod().build() 这样的链式方法调用。

谢谢!关于我的 API,我将在将它们用作参数之前指定函数,如下所示:

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

编辑:感谢 Holger 的 cmets,我重新设计了 API(再次感谢!)。它保留原始元素而不是键,以及展平的值。

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;
    }
}

它是可链接的,从级别 2 开始,映射器必须处理 FlatEntry。用法类似于简单的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()));

【讨论】:

  • 我会说这是Java 8 编译器中的推理问题。举个例子:final Set&lt;String&gt; expectedDaoData3 = Collections.unmodifiableSet( Arrays.asList("a", "b", "c").stream().collect(Collectors.toCollection(LinkedHashSet::new))); 用 Eclipse Compiler for Java 编译,而不是 OpenJDK 编译,报错:required: java.util.Set&lt;? extends T&gt; found: java.util.Collection&lt;java.lang.String&gt; reason: cannot infer type-variable(s) E
【解决方案2】:

向编译器提供足够类型信息的一种方法是声明 lambda 参数之一的显式类型。这与your answer 的精神相同,但更紧凑一些,因为您只需提供参数的类型,而不是整个函数。

对于一级地图来说,这看起来还不错:

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

两级地图就在怪诞的边界上,然而:

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());

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-03-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多