Java泛型:通配符

7

我在阅读有关泛型的资料,尤其是与通配符相关的内容,以便重新熟悉这些概念。由于我几乎从未使用过或遇到过通配符,因此无法理解为什么要使用它们。我一直看到的一个例子是下面这个。

void printCollection( Collection<?> c ) {
  for (Object o : c){
    System.out.println(o);
  }
}

为什么不这样写:

<T> void printCollection( Collection<T> c ) {
    for(T o : c) {
        System.out.println(o);
    }
}

来自Oracle网站的另一个例子:

public static double sumOfList(List<? extends Number> list) {
    double s = 0.0;
    for (Number n : list)
        s += n.doubleValue();
    return s;
}

为什么这不是写成:
public static <T extends Number> double sumOfList(List<T> list) {
    double s = 0.0;
    for (Number n : list)
        s += n.doubleValue();
    return s;
}

我有什么遗漏吗?

可能 重复 - Jayamohan
1
@Jayamohan 我不同意。 - Matt Ball
3个回答

3

为什么要把事情搞得比必须的更复杂?这些示例展示了可能的最简单解决方案 - 这些示例并不试图说明通用方法。

Why would you not write this as:

<T> void printCollection( Collection<T> c ) {
    for(T o : c) {
        System.out.println(o);
    }
}
因为 System.out.println() 可以接受对象,所以不需要更具体的内容。

Why is this not written as

public static <T extends Number> double sumOfList(List<T> list) {
    double s = 0.0;
    for (Number n : list)
        s += n.doubleValue();
    return s;
}

再次强调,因为您不需要针对每个不同的T extends Number进行不同的参数化。一个接受List<? extends Number>的非泛型方法就足够了。


我认为我可能只是误解了这两者之间的区别,它们是相同的吗?在什么情况下,我会遇到无法使用T而必须使用?来解决的问题呢?我有一种感觉,自己完全没有理解通配符的意义。 - Jon Taylor
2
@JonTaylor:实际上,你可以用 T 替代 ? 来解决任何情况,但关键是 ? 不需要名称,并表示您真的不关心类型是什么。 - Louis Wasserman
@LouisWasserman Class.forName 返回一个 Class<?> 可能是只有通配符才合适的一个例子。 - Paul Bellora

3

确实,如果方法参数类型具有带有上界的第一级通配符,它可以被类型参数替换。

反例(通配符无法被类型参数替换)

    List<?> foo()  // wildcard in return type  

    void foo(List<List<?>> arg)   // deeper level of wildcard

    void foo(List<? super Number> arg)   // wildcard with lower bound

现在对于可以通过通配符或类型参数解决的情况。
        void foo1(List<?> arg)

    <T> void foo2(List<T> arg)

一般认为foo1()foo2()更加时尚。这可能有点主观。我个人认为foo1()的签名更容易理解。而且,工业界普遍采用foo1(),所以最好遵循惯例。
此外,foo1()arg的处理也更加抽象,因为你不能轻易地在foo1()中执行arg.add(something)。当然,这可以很容易地解决(即将arg传递给foo2()!)。公共方法通常看起来像foo1(),内部转发到私有的foo2()
还有一些情况下,通配符无法满足需求,需要使用类型参数:
    <T> void foo(List<T> foo1, List<T> foo2);  // can't use 2 wildcards.

到目前为止,这个讨论是关于方法签名的。在其他地方,通配符可能是不可或缺的,因为类型参数不能被引入。


+1 因为提到类型参数不能有下界(lower bounds)。 - Paul Bellora

3

来自Oracle

一个问题是:我何时应该使用泛型方法,何时应该使用通配符类型?为了理解答案,让我们来看一下Collection库中的几个方法。

interface Collection<E> {
     public boolean containsAll(Collection<?> c);
     public boolean addAll(Collection<? extends E> c);
 }

我们可以在这里使用通用方法代替:
interface Collection<E> {
     public <T> boolean containsAll(Collection<T> c);
     public <T extends E> boolean addAll(Collection<T> c);
     // Hey, type variables can have bounds too!
 }

然而,在containsAll和addAll中,类型参数T只使用了一次。返回类型不依赖于类型参数,方法的任何其他参数也不依赖于它(在这种情况下,仅存在一个参数)。这告诉我们类型参数正在用于多态性;它的唯一效果是允许在不同的调用站点使用各种实际参数类型。如果是这种情况,应该使用通配符。通配符旨在支持灵活的子类型化,这就是我们试图表达的内容。
因此,对于第一个示例,这是因为操作不依赖于类型。
对于第二个示例,这是因为它仅依赖于Number类。

谢谢,所有的解释都很好,但是这个解释出奇地帮助我最理解它。 - Jon Taylor

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