为什么要使用通配符捕获辅助方法?

Why use a wild card capture helper method?

参考:Wildcard Capture Helper Methods

它说要创建一个辅助方法来捕获通配符。

public void foo(List<?> i) {
    fooHelper(i);
}        
private <T> void fooHelper(List<T> l) {
    l.set(0, l.get(0));
}

单独使用下面这个函数不会产生任何编译错误,而且似乎以同样的方式工作。我不明白的是:你为什么不直接使用它而避免使用助手呢?

public <T> void foo(List<T> l) {
    l.set(0, l.get(0));
}

我以为这个问题真的可以归结为:通配符和泛型有什么区别?所以,我去了这个:difference between wildcard and generics。 它说要使用类型参数:

1) If you want to enforce some relationship on the different types of method arguments, you can't do that with wildcards, you have to use type parameters.

但是,这不正是带辅助函数的通配符的实际作用吗?它不是在不同类型的方法参数与其未知值的设置和获取之间强制建立关系吗?

我的问题是:如果你必须定义一些需要不同类型方法参数的关系,那么为什么首先使用通配符然后使用辅助函数呢?

这似乎是一种合并通配符的怪异方式。

在这种特殊情况下,这是因为 List.set(int, E) 方法要求类型与列表中的类型相同。

如果你没有辅助方法,编译器不知道 ? 对于 List<?>get(int) 的 return 是否相同,所以你得到一个编译器错误:

The method set(int, capture#1-of ?) in the type List<capture#1-of ?> is not applicable for the arguments (int, capture#2-of ?)

用辅助方法,你是在告诉编译器,类型是一样的,我只是不知道是什么类型。

那么为什么要有非辅助方法呢?

泛型直到 Java5 才被引入,所以有很多代码早于泛型。 pre-Java 5 List 现在是 List<?> 所以如果你想在一个通用的编译器中编译旧代码,如果你不能的话,你将不得不添加这些辅助方法更改方法签名。

我同意:删除辅助方法并键入 public API。没有理由不这样做,也完全有理由这样做。

只是总结通配符版本的帮助程序的需要:虽然对于我们人类来说这是显而易见的,但编译器不知道从 l.get(0) 返回的未知类型是 same 列表本身的未知类型。也就是说,它不考虑 set() 调用的参数来自与目标相同的列表对象,因此它必须是安全操作。它只注意到get()返回的类型是未知的,目标列表的类型是未知的,并且不能保证两个未知是同一类型。

你说得对,我们不必使用通配符版本。

归结为APIlooks/feels"better",这是主观的

    void foo(List<?> i) 
<T> void foo(List<T> i)

我会说第一个版本更好。

如果有界限

    void foo(List<? extends Number> i) 
<T extends Number> void foo(List<T> i)

第一个版本看起来更紧凑;类型信息都在一个地方。

此时,通配符版本是惯用的方式,程序员更熟悉。

JDK 方法定义中有 很多 通配符,特别是在 java8 引入 lambda/Stream 之后。诚然,它们非常丑陋,因为我们没有方差类型。但是想一想,如果我们将所有通配符都扩展为 vars 类型,那会有多难看。

Java 14 Language Specification, Section 5.1.10 (PDF) 专门用了一些段落来说明为什么人们更喜欢提供通配符方法 publicly,而使用通用方法 privately。具体来说,他们说(public 泛型方法):

This is undesirable, as it exposes implementation information to the caller.

这是什么意思?一个而不是另一个暴露的到底是什么?

您知道可以将类型参数直接传递给方法吗?如果你在 Foo class 上有一个静态方法 <T> Foo<T> create()——是的,这对我来说对静态工厂方法最有用——那么你可以调用它作为 Foo.<String>create().您通常不需要——或者想要——这样做,因为Java有时可以从任何提供的参数中推断出这些类型。但事实是,您 可以 明确提供这些类型。

所以泛型 <T> void foo(List<T> i) 实际上在语言级别有两个参数:列表的元素类型和列表本身。我们修改了方法契约只是为了在实现方面节省一些时间!

很容易认为 <?> 只是 shorthand 更明确的通用语法,但我认为 Java 的符号实际上 模糊 这里到底发生了什么。让我们暂时翻译成类型理论的语言:

/*                 Java *//* Type theory      */
             List<?>     ~~   ∃T. List<T>
    void foo(List<?> l)  ~~  (∃T. List<T>) -> ()
<T> void foo(List<T> l)  ~~   ∀T.(List<T>  -> ()

List<?>这样的类型被称为存在类型? 表示那里有 某种类型 ,但我们不知道它是什么。在类型理论方面,∃T. 意味着 "there exists some T",这基本上就是我在上一句话中所说的——我们刚刚给那个类型起了一个名字,尽管我们仍然不知道它是什么是。

在类型论中,函数的类型为 A -> B,其中 A 是输入类型,B 是 return 类型。 (出于愚蠢的原因,我们将 void 写成 ()。)请注意,在第二行,我们的输入类型与我们一直在讨论的存在列表相同。

第三行发生了奇怪的事情!在 Java 方面,看起来我们只是简单地命名了通配符(这在 直觉 方面不错)。在类型理论方面,我们已经说了一些与上一行表面上非常相似的内容:对于 调用者选择的任何类型,我们将接受该类型的列表。 (∀T. 实际上读作 "for all T"。)但是 Tscope 现在完全不同了——括号已经移动到包括输出类型!这很关键:如果没有更广泛的范围,我们就无法编写类似 <T> List<T> reverse(List<T> l) 的内容。

但是如果我们不需要更广泛的范围来描述函数的契约,那么减少变量的范围(是的,甚至是类型级变量)会更容易推理这些变量。该方法的存在形式使调用者非常清楚列表元素类型的相关性仅延伸到列表本身。