Java 方法参考中类型参数的使用

Use of type parameter in Java method reference

在 Java 恰好是第 3 版中,有以下代码片段:

BiConsumer<Double[], Comparator<Double>> arraySorter = Arrays::<Double>sort;

但是,我注意到即使我在 :: 之后省略 <Double>,方法引用仍然有效(由于 BiConsumer 的类型参数,这是有道理的)。

但是,我很困惑在方法引用中是否存在 ::<T> 必要的 的情况,如果是这样,一个例子是很有帮助。

Java 8 实现 Generalized Target-Type Inference (JEP 101) 允许编译器推断泛型方法的 type 参数。 在您的示例中,Java 8 编译器从赋值的右侧推断方法的类型参数sort

JEP 101 also proposed generalized target-type inference for chained method calls, but it was not implemented because of the complexity that it would have introduced to the inference algorithm (discussed here and here)。因此,chained generic-method calls 是无法推断泛型方法的类型参数的示例。

考虑以下代码片段:

class Main {

    String s = MyList.nil().head(); // Incompatible types. Required: String. Found: Object.

    static class MyList<E> {
        private E head;
        static <Z> MyList<Z> nil() { return new MyList(); }
        E head() { return head; }
    }
}

编译器无法为 String s = MyList.nil().head() 中的泛型方法 nil() 推断类型参数。因此,我们必须为推理算法提供更多信息,或者通过添加类型参数

String s = MyList.<String>nil().head();

或拆分链式调用

MyList<String> ls = MyList.nil();
String s = ls.head();

注意: chained-calls 示例不包含原始问题中对泛型方法(::<> 语法)的方法引用,但是两个示例中调用的推理技术是相同的。所以,推理的局限性也是一样的。

这是类型推断的实际应用,在大多数情况下,编译器会为您推断类型,因此您无需明确提供它们。

但是,在某些情况下您需要手动提供类型提示。

无论如何,Java 8 增强了通用参数的推断。

因此,执行以下操作:

BiConsumer<Double[], Comparator<Double>> arraySorter = Arrays::sort;

由于类型推断,完全有效。

我现在能想到的几个例子在 Java-7 中不起作用但在 Java-8 中起作用是这样的:

void exampleMethod(List<Person> people) {
      // do logic
} 

exampleMethod(Collections.emptyList())

另一个例子:

someMethodName(new HashMap<>());
...
void someMethodName(Map<String, String> values);

您之前需要明确提供类型参数。

此外,由于前面提到的类型推断,这就是我们现在可以执行类似操作的确切原因:

...
...
.collect(Collectors.toList());

而不是这个人:

...
...
.collect(Collectors.<Person>toList());

是否应该显式提供类型参数在某些情况下是偏好问题,而在其他情况下,您被迫这样做是为了帮助编译器完成工作。

我想 Java 10 的局部变量类型推断 (var name = ...;) 将是这个难题的答案。 right-hand-side 需要完全指定类型,而不是为方法引用提供类型的目标变量类型,需要在方法引用上使用类型参数 (::<T>)。

先想到门...

var arraySorter = Arrays::<Double>sort;

...但是方法引用本身并没有定义类型。它们需要由编译器转换为功能对象,编译器不会搜索已知的功能接口来寻找合适的类型,即使恰好有一个。


接下来的想法是使用方法引用作为方法的参数,returns 一个基于方法参数的类型。

class Spy {
    static <T> Function<T,T> f2(Function<T,T> f) {
        return f.andThen(f);
    }

    static <T> T identity(T t) {
        return t;
    }
}

使用它,我们可以创建局部变量,将方法引用传递给我们的方法:

Function<Double,Double> double_identity = f2(Spy::<Double>identity);

正如预期的那样,我们可以删除 ::<Double>

Function<Double,Double> double_identity = f2(Spy::identity);

出乎意料,局部变量类型推断就可以了。

var double_identity = f2(Spy::identity);             // Infers <Object>!
Object obj = null;
double_identity.apply(obj); 

但是当使用方法引用类型覆盖它时,真正令人惊讶的是。

var double_identity = f2(Spy::<Double>identity);     // Error: Double != Object

经过一番斗争,我明白了原因。我们必须将类型应用于 f2 方法本身:

var double_identity = Spy.<Double>f2(Spy::identity); // Works.

回想起来,这是有道理的。变量的类型通常为外部函数提供上下文。将结果分配给 Function<Double,Double> 变量让编译器推断 f2(...) 的类型,然后将该类型传递给参数。使用 var name = ...,没有显式类型,唯一可用的类型是 Object,因此编译器推断 Spy.<Object>f2(...),然后确定参数类型必须是 Function<Object,Object>

不幸的是,它似乎并没有从里到外解析,所以Spy::<Double>identity不会导致函数被推断为Spy.<Double>f2(...)而变量被推断为Function<Double,Double> .也许 Java 11?也许它会破坏太多,无法工作。

然而,它确实结束了我滥用 var name = ...; 来解决 OP 难题的尝试。


非常感谢@Eugene 在 Java 10 发布之前批评我之前的尝试。