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

时间:2015-06-10 18:00:07

标签: java generics wildcard

参考: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)如果要对不同类型的方法参数强制实施某些关系,则不能使用通配符,必须使用类型参数。

但是,具有辅助函数的通配符实际上并不是这样吗?是不是通过设置和获取未知值来强制执行不同类型的方法参数的关系?

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

这似乎是一种结合通配符的hacky方式。

4 个答案:

答案 0 :(得分:7)

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

如果您没有辅助方法,编译器不知道?的{​​{1}}是否相同,List<?>的返回是否相同,因此您会收到编译错误:

get(int)

使用帮助器方法,你告诉编译器,类型是相同的,我只是不知道类型是什么。

那么为什么要使用非辅助方法?

在Java 5之前没有引入泛型,因此有很多代码出现在泛型之前。 Java 5之前The method set(int, capture#1-of ?) in the type List<capture#1-of ?> is not applicable for the arguments (int, capture#2-of ?) 现在是List所以如果您尝试在通用感知编译器中编译旧代码,如果无法更改方法签名,则必须添加这些辅助方法

答案 1 :(得分:4)

你是对的,我们不必使用通配符版本。

归结为哪个API看起来/感觉“更好”,这是主观的

img {
    width: 100px;
}
li {
    display: inline-block;
}
ul {
    list-style: none;
    text-align: center;
}

我会说第一个版本更好。

如果有界限

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

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

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

JDK方法定义中有一个 lot 通配符,特别是在java8引入lambda / Stream之后。不可否认,它们非常难看,因为我们没有变化类型。但是想想如果我们扩展所有通配符来输入变量,它会变得多么丑陋。

答案 2 :(得分:3)

我同意:删除帮助方法并输入公共API。没有理由没有理由,也没有理由。

总结一下使用通配符版本的帮助程序的需求:虽然对我们来说很明显,编译器并不知道从l.get(0)返回的未知类型是< em>相同列表本身的未知类型。即它没有考虑到set()调用的参数来自与目标相同的列表对象,因此它必须是安全的操作。它只注意到从get()返回的类型是未知的,并且目标列表的类型是未知的,并且两个未知数不能保证是相同的类型。

答案 3 :(得分:0)

Java 14 Language Specification, Section 5.1.10 (PDF)专门介绍了一些段落,说明了为什么在使用通用方法public ly时更愿意提供通配符方法private ly。具体来说,他们说(是public通用方法的意思):

这是不希望的,因为它会将实现信息公开给调用者。

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

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

因此,泛型<T> void foo(List<T> i)实际上在语言级别采用了两个参数:列表的元素类型和列表本身。我们修改了方法协定只是为了节省自己在实现方面的时间!

很容易想到<?>只是更明确的泛型语法的简写,但是我认为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.的意思是“存在一些T”,这基本上是我在上一句话中所说的-尽管我们仍然不知道该叫什么,但我们只是给该类型起了一个名字。是的。

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

第三行发生了奇怪的事情!在Java方面,看起来我们只是简单地命名了通配符(对于它来说,这不是不好的 intuition )。在类型论方面,我们已经说了一些与表面上非常相似的内容:对于呼叫者选择的任何类型,我们将接受该类型的列表 。 (实际上,{∀T.读作“对于所有T”。)但是T scope 现在完全不同了,方括号已移至包括输出类型!这很关键:如果没有更大的范围,我们就无法编写类似<T> List<T> reverse(List<T> l)的东西。

但是,如果我们不需要更广泛的范围来描述函数的约定,那么减小变量的范围(是的,甚至是类型级别的变量),就可以更轻松地推断出这些变量。该方法的存在形式使调用者很清楚地知道列表元素类型的相关性没有扩展到列表本身。