混淆 java 泛型错误与 Map 和 Collector

Dav*_*arr 2 java eclipse generics java-8 java-stream

不久前,我发现了以下有关使用 Java 8 初始化地图的更简洁方法的信息:http : //minborgsjavapot.blogspot.com/2014/12/java-8-initializing-maps-in-smartest-way.html

使用这些指南,我在一个应用程序中实现了以下类:

public class MapUtils {
    public static <K, V> Map.Entry<K, V> entry(K key, V value) {
        return new AbstractMap.SimpleEntry<>(key, value);
    }

    public static <K, U> Collector<Map.Entry<K, U>, ?, Map<K, U>> entriesToMap() {
        return Collectors.toMap((e) -> e.getKey(), (e) -> e.getValue());
    }

    public static <K, U> Collector<Map.Entry<K, U>, ?, ConcurrentMap<K, U>> entriesToConcurrentMap() {
        return Collectors.toConcurrentMap((e) -> e.getKey(), (e) -> e.getValue());
    }
}
Run Code Online (Sandbox Code Playgroud)

在那个应用程序中,我实现了这样的代码:

public Map<String, ServiceConfig>   serviceConfigs() {
    return Collections.unmodifiableMap(Stream.of(
            entry("ActivateSubscriber", new ServiceConfig().yellowThreshold(90).redThreshold(80)),
            entry("AddAccount", new ServiceConfig().yellowThreshold(90).redThreshold(80).rank(3)),
            ...
            ).
            collect(entriesToMap()));
}
Run Code Online (Sandbox Code Playgroud)

这段代码工作得很好。

在另一个应用程序中,我将 MapUtils 类复制到一个包中,然后以与在另一个应用程序中相同的方式在一个类中导入该类。

我输入了以下内容以引用此内容:

        Map<String, USLJsonBase>    serviceRefMap   =
    Collections.unmodifiableMap(Stream.of(
            entry("CoreService", coreService),
            entry("CreditCheckService", creditCheckService),
            entry("PaymentService", paymentService),
            entry("AccountService", accountService),
            entry("OrdercreationService", orderCreationService),
            entry("ProductAndOfferService", productAndOfferService),
            entry("EquipmentService", equipmentService),
            entry("EvergentService", evergentService),
            entry("FraudCheckService", fraudCheckService)
            ).
            collect(entriesToMap()));
Run Code Online (Sandbox Code Playgroud)

在“收集”电话中,Eclipse 告诉我以下内容:

The method collect(Collector<? super Map.Entry<String,? extends USLJsonBase>,A,R>) in the type Stream<Map.Entry<String,? extends USLJsonBase>> is not applicable for the arguments (Collector<Map.Entry<Object,Object>,capture#1-of ?,Map<Object,Object>>)
Run Code Online (Sandbox Code Playgroud)

需要什么简单且完全不明显的更改才能使其工作?

更新

我认为添加类型提示可能会这样做,但我不明白为什么其他应用程序中的用法不需要​​这样做。

我更改了对此的引用,现在不会给我一个编译错误:

    Map<String, USLJsonBase>    serviceRefMap   =
    Collections.unmodifiableMap(Stream.<Map.Entry<String, USLJsonBase>>of(
            entry("CoreService", coreService),
            entry("CreditCheckService", creditCheckService),
            entry("PaymentService", paymentService),
            entry("AccountService", accountService),
            entry("OrdercreationService", orderCreationService),
            entry("ProductAndOfferService", productAndOfferService),
            entry("EquipmentService", equipmentService),
            entry("EvergentService", evergentService),
            entry("FraudCheckService", fraudCheckService)
            ).
            collect(entriesToMap()));
Run Code Online (Sandbox Code Playgroud)

同样,为什么这里需要类型提示,而在其他应用程序中不需要?唯一的区别是另一个应用程序从函数返回映射,而新代码将映射分配给局部变量。我还修改了它,而不是存储到局部变量中,而是将它传递给另一个方法(这是最初的需要)。这并没有改变添加类型提示的需要。

Hol*_*ger 5

问题是这Stream.of(…).collect(…)是一个方法调用链,目标类型没有通过这样的链传播。因此,当您将结果分配给 parameterized 时Map,这些类型参数会被考虑用于collect调用(和嵌套entriesToMap()调用),但不会用于Stream.of(…)调用。

因此,为了推断通过创建的流Stream.of(…)的类型,只考虑参数的类型。当所有参数都具有相同类型时,这很有效,例如

Map<String,Integer> map = Stream.of(entry("foo", 42), entry("bar", 100))
                                .collect(entriesToMap());
Run Code Online (Sandbox Code Playgroud)

没有问题,但是当参数具有不同类型时很少做所需的事情,例如

Map<String,Number> map = Stream.of(entry("foo", 42L), entry("bar", 100))
                               .collect(entriesToMap());
Run Code Online (Sandbox Code Playgroud)

失败,因为编译器不会NumberLongand推断为通用类型Integer,而是像“ INT#1 extends Number,Comparable<? extends INT#2> INT#2 extends Number,Comparable<?>”这样的东西

您没有发布允许我们在您的特定情况下确定参数类型的声明,但我很确定这是您的变体之间的区别,在第一个中,要么所有参数都具有相同的类型或者推断的通用超类型完全匹配您想要的结果类型,而在第二种情况下,参数具有不同类型或所需结果类型的子类型。

请注意,即使

Map<String,Number> map = Stream.of(entry("foo", 42), entry("bar", 100))
                               .collect(entriesToMap());
Run Code Online (Sandbox Code Playgroud)

不起作用,因为推断的流类型是Stream<Map.Entry<String,Integer>>您的收集器不接受的用于生成Map<String,Number>.

这导致了放宽收集器通用签名的解决方案。

public static <K, U>
Collector<Map.Entry<? extends K, ? extends U>, ?, Map<K, U>> entriesToMap() {
    return Collectors.toMap((e) -> e.getKey(), (e) -> e.getValue());
}
Run Code Online (Sandbox Code Playgroud)

这修复了两个示例,不仅接受Map.Entry<String,Integer>a Map<String,Number>,还接受编译器推断为Integerand基类型的交集类型Long


但我推荐一个替代方案,不要让每个客户都重复这个Stream.of(…).collect(…)步骤。与Java 9新工厂方法进行比较。因此,受此模式启发的重构方法将如下所示:

public static <K, V> Map.Entry<K, V> entry(K key, V value) {
    return new AbstractMap.SimpleImmutableEntry<>(key, value);
}

@SafeVarargs
public static <K, V> Map<K,V> mapOf(Map.Entry<? extends K, ? extends V>... entries) {
    return Stream.of(entries)
             .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

@SafeVarargs
public static <K, V> ConcurrentMap<K,V> concurrentMapOf(
                                        Map.Entry<? extends K, ? extends V>... entries) {
    return Stream.of(entries)
             .collect(Collectors.toConcurrentMap(Map.Entry::getKey, Map.Entry::getValue));
}
Run Code Online (Sandbox Code Playgroud)

可以更简单地使用:

Map<String,Integer> map1 = mapOf(entry("foo", 42), entry("bar", 100));
Map<String,Number>  map2 = mapOf(entry("foo", 42), entry("bar", 100));
Map<String,Number>  map3 = mapOf(entry("foo", 42L), entry("bar", 100));
Run Code Online (Sandbox Code Playgroud)

请注意,由于此用法仅包含嵌套调用(无链),因此目标类型推断适用于整个表达式,即甚至可以在没有? extends工厂方法的通用签名中的情况下工作。但是为了获得最大的灵活性,仍然建议使用这些通配符。