Java 啥时候需要显式类型参数?

Posted

技术标签:

【中文标题】Java 啥时候需要显式类型参数?【英文标题】:When does Java require explicit type parameters?Java 什么时候需要显式类型参数? 【发布时间】:2015-09-30 22:26:24 【问题描述】:

给定:

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

编译器错误可以通过提供显式类型参数&lt;String, String, Integer&gt;来解决。

    Java 8 何时需要显式类型参数?意思是,是否存在破坏类型推断的已知模式? 是否可以更改 toImmutableMap()MapCollectorBuilder 以避免显式类型参数,而不会失去使用 Builder 来配置收集器?

更新

    为什么涉及map3 的语句有效?与涉及map2的声明有何不同?

【问题讨论】:

【参考方案1】:

回答您的问题“意思是,是否存在破坏类型推断的已知模式?”很快:当然,有一个模式,而且还有一个huge specification 用于 Java 编程语言的整个行为。

但是关于类型推断和方法调用类型的章节非常详尽且难以理解。最能说明这一点的是,在出现意外行为的情况下,通常会根据规范对预期行为进行大量讨论。

但有些点对于程序员来说是可以解释和记住的。

有两种方法可以推断类型参数,通过传递给方法或组成表达式的部分的参数或通过表达式的目标类型,即预期的调用参数的类型,分配的变量或返回语句的方法的返回类型。

目标类型可以通过嵌套方法调用传播,例如 in

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&lt;String&gt;collect 方法提供分配给map1 的结果,因此流类型Stream&lt;String&gt;目标类型ImmutableMap&lt;String, Integer&gt; 都是已知的,可用于类型推断表达式。在表达式上:

Testcase.toImmutableMap(keyMapper, valueMapper).build() 这是一个链式调用,因此目标类型以build() 为已知,但toImmutableMap 不为已知。但是,toImmutableMap 的参数是具有已知确切类型的局部变量,因此类型推断可以使用它们来推断toImmutableMap 的结果类型并检查它是否符合.build() 的预期

Testcase.toImmutableMap(i -&gt; i, Integer::valueOf).build() 这又是一个链式调用,但现在参数i - &gt; i 的类型不完整,并且缺少目标类型。在不知道目标类型的情况下猜测 i -&gt; i 的类型的尝试失败。

Testcase.toImmutableMap2(i -&gt; i, Integer::valueOf)不是链式调用,因此目标类型可用于toImmutableMap2 调用(相对于collect 调用,它是嵌套调用) .因此,toImmutableMap2 的目标类型允许推断参数的目标类型,因此适用于i -&gt; i lambda 表达式。使用正确的目标类型,可以推断出正确的功能签名。

【讨论】:

所以如果我理解正确的话,链式调用允许在链中向前传递信息,但不能向后传递。给定TargetType x = A.of(ArgumentType).buildB().buildC(),我们可以将ArgumentType 向前传递,从A 传递到C,但我们不能将TargetType 向后传递,从C 传递到A。对吗?【参考方案2】:

lambda 表达式的目标类型完全由上下文确定,as discussed in the Java tutorial。因此,lambdas 对类型参数推断没有贡献;相反,他们依赖。方法引用“紧凑、易于阅读的 lambda 表达式,用于已有名称的方法”(Oracle Java 教程;已添加重点),因此没有区别当涉及到它们时,它们会对类型分析进行不同的着色。

当您将 lambda / 方法引用分配给变量时,该变量的类型为推断类型参数提供了上下文。但是,当您将它们直接传递给泛型方法时,您需要一些其他机制来推断它们的类型。在某些情况下,该方法的其他参数可能会达到这个目的。在您的特定情况下,您可能需要显式类型参数:

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

更新

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

然而,在涉及build() 的情况下,我认为Java 将推断泛型方法toImmutableMap() 的类型参数的问题与在其返回值上调用build() 的返回值类型的问题分开。换句话说,它想先确定toImmutableMap()返回的对象的类型,然后再考虑通过对该值调用方法获得的值的类型。

【讨论】:

我认为问题出在.collect(Testcase.toImmutableMap(**i -&gt; i**, Integer::valueOf)。泛型类型不会传播到其他类。在这种情况下,Java 不知道 i 只能是 String 类型,因此假定为 Object。在第一种情况下,您有一个 Function&lt;String, String&gt; 实例。这个实例携带信息,它从String 映射到String,Java 可以正确推断类型。 @Turing85,据我所知,问题出在 both lambdas 上,包括也是方法引用的那个。但是,特别是关于i -&gt; i,您只是简单地重申了我在回答中写的内容。 如果方法引用可用于推断类型参数,那么 Java 应该能够从中推断出 TV,但编译器抱怨 TK 都没有, 或V 可以推断出来。 谢谢。我添加了一个相关语句 (map3),我希望它会触发编译器错误,但不会。你能更新你的答案来解决这个问题吗?【参考方案3】:

还有另一种方法可以解决此问题。您可以提示编译器显式指定标识 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 中编译良好。

【讨论】:

你刚刚证明的有趣的事情是类型推断只对&lt;T&gt; 失败。一旦我们提供了一个明确的类型,剩下的变量就会被正确地推断出来。

以上是关于Java 啥时候需要显式类型参数?的主要内容,如果未能解决你的问题,请参考以下文章

我啥时候需要在 Rust 中指定显式生命周期?

java问题 啥时候用int,啥时候用integer

Spring MVC Converter 啥时候执行转换

Oracle sql中的隐式和显式数据类型转换有啥区别

JNI 啥时候需要复制原始类型的数组?

PL/PgSQL:没有函数匹配给定的名称和参数类型。您可能需要添加显式类型转换