Java需要显式类型参数的时候是什么时候?

13

给定:

import com.google.common.collect.ImmutableMap;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Stream;

public class Testcase
{
    public static <T, K, V> MapCollectorBuilder<T, K, V>
        toImmutableMap(Function<? super T, ? extends K> keyMapper,
            Function<? super T, ? extends V> valueMapper)
    {
        return null;
    }

    public static final class MapCollectorBuilder<T, K, V>
    {
        public Collector<T, ?, ImmutableMap<K, V>> build()
        {
            return null;
        }
    }

    public static <T, K, V> Collector<T, ?, ImmutableMap<K, V>> toImmutableMap2(
        Function<? super T, ? extends K> keyMapper,
        Function<? super T, ? extends V> valueMapper)
    {
        return null;
    }

    public void main(String[] args)
    {
        Function<String, String> keyMapper = i -> i;
        Function<String, Integer> valueMapper = Integer::valueOf;

        ImmutableMap<String, Integer> map1 = Stream.of("1", "2", "3")
            .collect(Testcase.toImmutableMap(keyMapper, valueMapper).build());

        ImmutableMap<String, Integer> map2 = Stream.of("1", "2", "3")
            .collect(Testcase.toImmutableMap(i -> i, Integer::valueOf).build());

        ImmutableMap<String, Integer> map3 = Stream.of("1", "2", "3")
            .collect(Testcase.toImmutableMap2(i -> i, Integer::valueOf));
    }
}

map1map3相关的语句编译正常,但是map2会出现以下错误:

Testcase.java:[41,57] incompatible types: cannot infer type-variable(s) T,K,V
    (argument mismatch; invalid method reference
      no suitable method found for valueOf(java.lang.Object)
          method java.lang.Integer.valueOf(java.lang.String) is not applicable
            (argument mismatch; java.lang.Object cannot be converted to java.lang.String)
          method java.lang.Integer.valueOf(int) is not applicable
            (argument mismatch; java.lang.Object cannot be converted to int))

通过提供显式的类型参数<String,String,Integer>可以解决编译器错误。

  1. Java 8什么时候需要显式类型参数?也就是说,是否有已知的模式会破坏类型推断?
  2. 可以更改toImmutableMap()MapCollectorBuilder以避免显式类型参数,而不失去使用构建器配置Collector的功能吗?

更新:

  1. 为什么涉及map3的语句有效?它与涉及map2的语句有何不同?
3个回答

5
简单回答你的问题“是否有已知的打破类型推断的模式”:当然有,而且整个Java编程语言的行为都有一个巨大的规范
但是关于类型推断和方法调用类型的章节非常详尽和难以理解。这最好通过以下事实来说明:在出现意外行为的情况下,通常会发生关于规范中预期行为的大量讨论。
但是对于程序员来说,有一些可以解释和记住的要点。
有两种方式可以推断类型参数,一种是通过传递给方法的参数或组成表达式的部分,另一种是通过表达式的目标类型推断,即调用参数的预期类型、被赋值的变量或返回语句中的方法的返回类型。
目标类型可以通过类似于嵌套方法调用的方式进行传播,例如:
TargetType x=foo(bar(/*target type can be used*/));

或者在条件语句中,例如:
TargetType x=condition? foo(/*target type can be used*/): foo(/*target type can be used*/);

但是,在链式调用的情况下不适用,例如:
TargetType x=foo(/*target type can NOT be used*/).foo();

现在看看你的例子:

ImmutableMap<String, Integer> map1 = Stream.of("1", "2", "3").collect( expression );

在这里,Stream.of(…).collect(…)是被链接起来的,因此目标类型无法用于确定of调用的流类型,但提供给该方法的参数足以推断出类型Stream<String>collect方法提供了分配给map1的结果,因此流类型Stream<String>目标类型ImmutableMap<String, Integer>都已知并可用于表达式的类型推断。至于表达式:
  • Testcase.toImmutableMap(keyMapper, valueMapper).build() 这是一个链式调用,因此对于build()已知目标类型,但对于toImmutableMap未知。然而,toImmutableMap的参数是局部变量,具有已知的确切类型,因此类型推断可以使用它们来推断toImmutableMap的结果类型,并检查它是否符合.build()的预期。

  • Testcase.toImmutableMap(i -> i, Integer::valueOf).build() 这也是一个链式调用,但现在参数i -> i具有不完整的类型,并且缺乏目标类型。尝试在没有关于目标类型的知识的情况下猜测i -> i的类型失败。

  • Testcase.toImmutableMap2(i -> i, Integer::valueOf) 这不是一个链式调用,因此对于toImmutableMap2调用的目标类型是可用的(就collect调用而言,它是一个嵌套调用)。因此,toImmutableMap2的目标类型允许推断参数的目标类型,因此对于i -> i lambda表达式也是如此。有了正确的目标类型,可以推断出正确的函数签名。


所以如果我理解你的意思正确的话,链式调用允许向前传递信息,但不允许在链中向后传递。给定 TargetType x = A.of(ArgumentType).buildB().buildC(),我们能够将 ArgumentTypeA 传递到 C,但我们无法将 TargetTypeC 传递回 A。这是正确的吗? - Gili

3
一个Lambda表达式的目标类型完全由上下文决定,正如Java教程中所讨论的。因此,Lambda表达式不会对类型参数推断产生影响;相反,它们依赖于它。方法引用“已经有名称的方法的紧凑、易读的Lambda表达式”(Oracle Java教程;强调添加),因此当它们涉及到时,在类型分析方面没有区别。
当您将Lambda表达式/方法引用分配给变量时,该变量的类型提供了推断类型参数的上下文。然而,当您直接将它们传递给泛型方法时,您需要其他机制来推断它们的类型。在某些情况下,方法的其他参数可能会起到这个作用。在您的特定情况下,看起来您可能需要明确的类型参数:
ImmutableMap<String, Integer> map2 = Stream.of("1", "2", "3").collect(
        Testcase.<String, String, Integer>toImmutableMap(i -> i, Integer::valueOf).build());

更新

关于更新后的问题,看起来在map3的情况下Java可以正确推断类型,部分原因是因为没有调用MapCollectorBuilder.build()方法而使得判断不复杂。没有build()时,map3的类型提供了上下文来确定Stream.collect()的第一个类型参数,从而给出了KV。类型参数T可以从(推断出的)Stream类型中推断出。

然而,当涉及到build()时,我认为Java将推断泛型方法toImmutableMap()的类型参数与在其返回值上调用build()方法的返回值类型的问题分开考虑。换句话说,它想要在考虑调用该值上的方法的值类型之前确定由toImmutableMap()返回的对象的类型。


我认为问题出在 .collect(Testcase.toImmutableMap(**i -> i**, Integer::valueOf) 这一行。泛型类型无法传播到其他类中。在这种情况下,Java 不知道 i 只能是 String 类型,因此假定为 Object。在第一个案例中,您有一个 Function<String, String> 实例。该实例携带了从 StringString 的映射信息,Java 可以正确地推断类型。 - Turing85
1
@Turing85,据我所知,问题出在两个lambda表达式上,包括那个也是方法引用的表达式。特别是关于i -> i,你只是重新陈述了我在答案中写的内容。 - John Bollinger
1
如果方法引用可以用于推断类型参数,那么Java应该能够从中推断出TV,但编译器却抱怨无法推断出TKV - John Bollinger
谢谢。我添加了一个相关的语句(map3),本来期望会触发编译器错误,但实际上并没有。你能否更新你的答案来解决这个问题? - Gili

1
有另外一种解决方法。您可以给编译器一个提示,明确指定身份 lambda 的参数类型:
ImmutableMap<String, Integer> map2 = Stream.of("1", "2", "3")
    .collect(Testcase.toImmutableMap((String i) -> i, Integer::valueOf).build());

在Javac 1.8.0_25和ECJ 3.10.2中编译正常。


你刚刚证明的有趣的事情是类型推断仅在 <T> 失败。一旦我们提供了显式类型,剩余的变量就会被正确推断。 - Gili

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接